Compare commits
10 Commits
3cd5ee9e99
...
910e912b05
| Author | SHA1 | Date | |
|---|---|---|---|
| 910e912b05 | |||
| ba98e17d2d | |||
| abeed5ae88 | |||
| c28450b97d | |||
| 40fbb750db | |||
| e9ddc3d332 | |||
| 98355a5286 | |||
| 5fc0462ab1 | |||
| 81b94e8635 | |||
| 92af30f61b |
@@ -3,8 +3,11 @@
|
||||
namespace App\Filament\Admin\Resources\Proposals\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Proposals\ProposalResource;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Support\Enums\Width;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EditProposal extends EditRecord
|
||||
{
|
||||
@@ -19,5 +22,73 @@ class EditProposal extends EditRecord
|
||||
{
|
||||
return Width::Full;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
// Teklif linkini tarayıcı panosuna kopyala
|
||||
Action::make('copy_link')
|
||||
->label(__('proposal.copy_link'))
|
||||
->icon('heroicon-o-clipboard-document-check')
|
||||
->color('info')
|
||||
->action(function () {
|
||||
$url = route('proposals.show', $this->record->slug);
|
||||
|
||||
// Livewire v3: sunucu tarafında JS enjekte et (user-gesture gerektirmez)
|
||||
$escaped = addslashes($url);
|
||||
$this->js("(function(){var t=document.createElement('textarea');t.value='{$escaped}';t.style.position='fixed';t.style.opacity='0';document.body.appendChild(t);t.focus();t.select();try{document.execCommand('copy')}catch(e){}document.body.removeChild(t);})();");
|
||||
|
||||
Notification::make()
|
||||
->title(__('proposal.link_copied'))
|
||||
->body($url)
|
||||
->success()
|
||||
->duration(6000)
|
||||
->send();
|
||||
})
|
||||
->visible(fn () => $this->record !== null),
|
||||
|
||||
// Yeni rastgele benzersiz slug üret ve kaydet
|
||||
Action::make('regenerate_slug')
|
||||
->label(__('proposal.regenerate_slug'))
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('warning')
|
||||
->requiresConfirmation()
|
||||
->modalHeading(__('proposal.regenerate_slug_confirm_heading'))
|
||||
->modalDescription(__('proposal.regenerate_slug_confirm_body'))
|
||||
->modalSubmitActionLabel(__('proposal.regenerate_slug_confirm_button'))
|
||||
->action(function () {
|
||||
$newSlug = 'teklif-' . strtolower(Str::random(4)) . '-' . date('ymd') . '-' . strtolower(Str::random(4));
|
||||
|
||||
// Benzersizliği garanti et
|
||||
while (\App\Models\Proposal::where('slug', $newSlug)->where('id', '!=', $this->record->id)->exists()) {
|
||||
$newSlug = 'teklif-' . strtolower(Str::random(4)) . '-' . date('ymd') . '-' . strtolower(Str::random(4));
|
||||
}
|
||||
|
||||
$this->record->update(['slug' => $newSlug]);
|
||||
|
||||
$newUrl = route('proposals.show', $newSlug);
|
||||
|
||||
Notification::make()
|
||||
->title(__('proposal.regenerate_slug_success'))
|
||||
->body($newUrl)
|
||||
->success()
|
||||
->duration(8000)
|
||||
->send();
|
||||
|
||||
// Formu yenile
|
||||
$this->fillForm();
|
||||
})
|
||||
->visible(fn () => $this->record !== null),
|
||||
|
||||
// Teklifi yeni sekmede önizle
|
||||
Action::make('preview')
|
||||
->label(__('proposal.preview'))
|
||||
->icon('heroicon-o-eye')
|
||||
->color('gray')
|
||||
->url(fn () => $this->record ? route('proposals.show', $this->record->slug) : '#')
|
||||
->openUrlInNewTab()
|
||||
->visible(fn () => $this->record !== null),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,4 +53,9 @@ return [
|
||||
'copy_link' => 'Copy Proposal Link',
|
||||
'link_copied' => 'Proposal link copied to clipboard!',
|
||||
'preview' => 'Preview Proposal',
|
||||
'regenerate_slug' => 'Generate New Link',
|
||||
'regenerate_slug_confirm_heading' => 'Generate New Random Link',
|
||||
'regenerate_slug_confirm_body' => 'The current URL will be replaced and the old link will no longer work. Are you sure?',
|
||||
'regenerate_slug_confirm_button' => 'Yes, Generate New Link',
|
||||
'regenerate_slug_success' => 'New proposal link generated!',
|
||||
];
|
||||
|
||||
@@ -53,4 +53,9 @@ return [
|
||||
'copy_link' => 'Teklif Linkini Kopyala',
|
||||
'link_copied' => 'Teklif bağlantısı panoya kopyalandı!',
|
||||
'preview' => 'Teklifi Önizle',
|
||||
'regenerate_slug' => 'Yeni Link Üret',
|
||||
'regenerate_slug_confirm_heading' => 'Yeni Rastgele Link Oluştur',
|
||||
'regenerate_slug_confirm_body' => 'Mevcut bağlantı adresi değiştirilecek ve eski link artık çalışmayacak. Onaylıyor musunuz?',
|
||||
'regenerate_slug_confirm_button' => 'Evet, Yeni Link Üret',
|
||||
'regenerate_slug_success' => 'Yeni teklif linki oluşturuldu!',
|
||||
];
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@php
|
||||
// Server-side: get initial values for the header metadata
|
||||
$accent = $get('meta.accent_color') ?? 'indigo';
|
||||
$accentColor = match($accent) {
|
||||
'emerald' => '#059669',
|
||||
@@ -15,6 +14,7 @@
|
||||
'amber' => 'linear-gradient(135deg, #d97706 0%, #f59e0b 100%)',
|
||||
default => 'linear-gradient(135deg, #4f46e5 0%, #6366f1 100%)',
|
||||
};
|
||||
$uid = 'mdprev_' . uniqid();
|
||||
@endphp
|
||||
|
||||
{{-- Google Fonts --}}
|
||||
@@ -22,107 +22,43 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
{{-- Marked.js for client-side markdown rendering --}}
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
{{-- Marked.js CDN --}}
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@9/marked.min.js"></script>
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
renderedHtml: '',
|
||||
init() {
|
||||
this.renderMarkdown();
|
||||
// Watch for CodeEditor changes via MutationObserver + polling
|
||||
this.startWatching();
|
||||
},
|
||||
getContentValue() {
|
||||
// Try multiple methods to read the CodeEditor content
|
||||
// Method 1: Livewire $wire
|
||||
if (typeof $wire !== 'undefined' && $wire.data && $wire.data.content) {
|
||||
return $wire.data.content;
|
||||
}
|
||||
// Method 2: Find Monaco/CodeMirror editor instances
|
||||
const editorWrappers = document.querySelectorAll('[data-field-name=\"content\"]');
|
||||
for (const wrapper of editorWrappers) {
|
||||
// CodeMirror 6
|
||||
const cmEl = wrapper.querySelector('.cm-content');
|
||||
if (cmEl) {
|
||||
return cmEl.textContent || '';
|
||||
}
|
||||
}
|
||||
// Method 3: hidden input/textarea
|
||||
const hiddenInputs = document.querySelectorAll('textarea[wire\\:model\\.live=\"data.content\"], textarea[wire\\:model=\"data.content\"], input[name=\"data.content\"]');
|
||||
for (const inp of hiddenInputs) {
|
||||
if (inp.value) return inp.value;
|
||||
}
|
||||
// Method 4: any textarea with content field name
|
||||
const anyTextarea = document.querySelector('[x-ref=\"content\"], textarea[id*=\"content\"]');
|
||||
if (anyTextarea && anyTextarea.value) return anyTextarea.value;
|
||||
return '';
|
||||
},
|
||||
renderMarkdown() {
|
||||
const raw = this.getContentValue();
|
||||
if (raw && raw.trim()) {
|
||||
try {
|
||||
this.renderedHtml = marked.parse(raw, { gfm: true, breaks: true });
|
||||
} catch(e) {
|
||||
this.renderedHtml = '<p style=\"color:red;\">Markdown parse error</p>';
|
||||
}
|
||||
} else {
|
||||
this.renderedHtml = '';
|
||||
}
|
||||
},
|
||||
startWatching() {
|
||||
// Poll every 500ms to pick up CodeEditor changes
|
||||
setInterval(() => { this.renderMarkdown(); }, 500);
|
||||
<div id="{{ $uid }}" style="font-family: 'Inter', system-ui, sans-serif;">
|
||||
|
||||
// Also observe Livewire morphs
|
||||
if (typeof Livewire !== 'undefined') {
|
||||
Livewire.hook('morph.updated', () => {
|
||||
this.$nextTick(() => this.renderMarkdown());
|
||||
});
|
||||
}
|
||||
}
|
||||
}"
|
||||
class="md-preview-root"
|
||||
style="min-height: 500px; font-family: 'Inter', system-ui, -apple-system, sans-serif;"
|
||||
>
|
||||
{{-- Browser Chrome Mockup --}}
|
||||
{{-- Browser Chrome --}}
|
||||
<div style="border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 24px rgba(0,0,0,0.08); background: #f8fafc; height: 700px; display: flex; flex-direction: column;">
|
||||
|
||||
|
||||
{{-- Title Bar --}}
|
||||
<div style="background: #f1f5f9; border-bottom: 1px solid #e2e8f0; padding: 10px 16px; display: flex; align-items: center; gap: 12px; flex-shrink: 0;">
|
||||
{{-- Traffic Lights --}}
|
||||
<div style="display: flex; gap: 6px; flex-shrink: 0;">
|
||||
<span style="width: 12px; height: 12px; border-radius: 50%; background: #ff5f57;"></span>
|
||||
<span style="width: 12px; height: 12px; border-radius: 50%; background: #febc2e;"></span>
|
||||
<span style="width: 12px; height: 12px; border-radius: 50%; background: #28c840;"></span>
|
||||
<span style="width: 12px; height: 12px; border-radius: 50%; background: #ff5f57; display:inline-block;"></span>
|
||||
<span style="width: 12px; height: 12px; border-radius: 50%; background: #febc2e; display:inline-block;"></span>
|
||||
<span style="width: 12px; height: 12px; border-radius: 50%; background: #28c840; display:inline-block;"></span>
|
||||
</div>
|
||||
|
||||
{{-- URL Bar --}}
|
||||
<div style="flex: 1; max-width: 480px; margin: 0 auto; background: white; border: 1px solid #e2e8f0; border-radius: 6px; padding: 5px 10px; display: flex; align-items: center; gap: 6px; font-size: 12px; color: #94a3b8;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="#22c55e" style="width: 14px; height: 14px; flex-shrink: 0;">
|
||||
<path fill-rule="evenodd" d="M10 1a4.5 4.5 0 00-4.5 4.5V9H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-.5V5.5A4.5 4.5 0 0010 1zm3 8V5.5a3 3 0 10-6 0V9h6z" clip-rule="evenodd" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="#22c55e" style="width:14px;height:14px;flex-shrink:0;">
|
||||
<path fill-rule="evenodd" d="M10 1a4.5 4.5 0 00-4.5 4.5V9H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-.5V5.5A4.5 4.5 0 0010 1zm3 8V5.5a3 3 0 10-6 0V9h6z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span style="font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
truncgil.com/teklif/<strong style="color: #334155;">{{ $get('slug') ?: '...' }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style="width: 60px; flex-shrink: 0;"></div>
|
||||
</div>
|
||||
|
||||
{{-- Page Body with scroll --}}
|
||||
{{-- Page Body --}}
|
||||
<div style="flex: 1; overflow-y: auto; background: #f1f5f9; padding: 24px;">
|
||||
|
||||
{{-- Document Sheet --}}
|
||||
<div style="max-width: 800px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #e5e7eb; overflow: hidden;">
|
||||
|
||||
{{-- Accent top stripe --}}
|
||||
|
||||
{{-- Accent stripe --}}
|
||||
<div style="height: 4px; background: {{ $accentGradient }};"></div>
|
||||
|
||||
{{-- Document Content --}}
|
||||
{{-- Document --}}
|
||||
<div style="padding: 32px 40px;">
|
||||
|
||||
{{-- Document Header --}}
|
||||
|
||||
{{-- Header --}}
|
||||
<div style="margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid #f1f5f9;">
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
||||
<span style="font-size: 9px; text-transform: uppercase; letter-spacing: 1.5px; font-weight: 800; padding: 3px 8px; border-radius: 4px; color: white; background: {{ $accentColor }};">TEKNİK VE MALİ TEKLİF</span>
|
||||
@@ -136,209 +72,165 @@
|
||||
<span>•</span>
|
||||
<span style="font-weight: 700; color: #334155;">{{ $get('total_price') ? number_format((float) $get('total_price'), 2, ',', '.') : '—' }} {{ $get('currency') ?: 'TRY' }}</span>
|
||||
<span>•</span>
|
||||
<span>Son Geçerlilik: {{ $get('valid_until') ? date('d.m.Y', strtotime($get('valid_until'))) : '—' }}</span>
|
||||
<span>Geçerlilik: {{ $get('valid_until') ? date('d.m.Y', strtotime($get('valid_until'))) : '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Live Rendered Markdown --}}
|
||||
<div
|
||||
class="md-preview-body"
|
||||
x-show="renderedHtml"
|
||||
x-html="renderedHtml"
|
||||
></div>
|
||||
<div
|
||||
x-show="!renderedHtml"
|
||||
style="text-align:center;padding:60px 0;color:#94a3b8;font-style:italic;font-size:14px;"
|
||||
>
|
||||
|
||||
{{-- Markdown Output (plain div, updated by vanilla JS) --}}
|
||||
<div id="{{ $uid }}_placeholder" style="text-align:center; padding: 60px 0; color: #94a3b8; font-style: italic; font-size: 14px;">
|
||||
✏️ Sol taraftaki editöre Markdown yazın, canlı önizleme burada görünecektir.
|
||||
</div>
|
||||
<div id="{{ $uid }}_body" class="mdprev-body" style="display:none;"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var uid = '{{ $uid }}';
|
||||
var bodyEl = null;
|
||||
var phEl = null;
|
||||
var lastContent = null;
|
||||
|
||||
function getBodyEl() {
|
||||
if (!bodyEl) bodyEl = document.getElementById(uid + '_body');
|
||||
return bodyEl;
|
||||
}
|
||||
function getPhEl() {
|
||||
if (!phEl) phEl = document.getElementById(uid + '_placeholder');
|
||||
return phEl;
|
||||
}
|
||||
|
||||
function readEditorContent() {
|
||||
// Method 1: Read from CodeMirror 6 .cm-content
|
||||
var cmContents = document.querySelectorAll('.cm-content');
|
||||
for (var i = 0; i < cmContents.length; i++) {
|
||||
var text = cmContents[i].innerText || cmContents[i].textContent;
|
||||
if (text && text.trim().length > 0) return text;
|
||||
}
|
||||
// Method 2: Livewire data via wire:model textarea (hidden)
|
||||
var textareas = document.querySelectorAll('textarea[wire\\:model], textarea[wire\\:model\\.live]');
|
||||
for (var j = 0; j < textareas.length; j++) {
|
||||
var ta = textareas[j];
|
||||
if ((ta.name && ta.name.indexOf('content') !== -1) || (ta.id && ta.id.indexOf('content') !== -1)) {
|
||||
if (ta.value && ta.value.trim()) return ta.value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function renderMarkdown(content) {
|
||||
var body = getBodyEl();
|
||||
var ph = getPhEl();
|
||||
if (!body || !ph) return;
|
||||
|
||||
if (!content || !content.trim()) {
|
||||
body.style.display = 'none';
|
||||
ph.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
if (content === lastContent) return;
|
||||
lastContent = content;
|
||||
|
||||
if (typeof marked === 'undefined') return;
|
||||
|
||||
try {
|
||||
var html = marked.parse(content, { gfm: true, breaks: true });
|
||||
body.innerHTML = html;
|
||||
body.style.display = 'block';
|
||||
ph.style.display = 'none';
|
||||
} catch(e) {
|
||||
body.innerHTML = '<p style="color:red;">Markdown parse error: ' + e.message + '</p>';
|
||||
body.style.display = 'block';
|
||||
ph.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function poll() {
|
||||
renderMarkdown(readEditorContent());
|
||||
}
|
||||
|
||||
// Start polling once DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setInterval(poll, 400);
|
||||
});
|
||||
} else {
|
||||
setInterval(poll, 400);
|
||||
}
|
||||
|
||||
// Also hook into Livewire updates
|
||||
document.addEventListener('livewire:morph', function() {
|
||||
setTimeout(poll, 100);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* ===== Premium Markdown Preview Typography ===== */
|
||||
.md-preview-body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.md-preview-body h1 {
|
||||
font-family: 'Outfit', 'Inter', sans-serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
margin: 2rem 0 0.8rem 0;
|
||||
line-height: 1.25;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.md-preview-body h2 {
|
||||
font-family: 'Outfit', 'Inter', sans-serif;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 2rem 0 0.7rem 0;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.md-preview-body h3 {
|
||||
font-family: 'Outfit', 'Inter', sans-serif;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
margin: 1.5rem 0 0.5rem 0;
|
||||
}
|
||||
.md-preview-body h4 {
|
||||
font-family: 'Outfit', 'Inter', sans-serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
margin: 1.2rem 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.md-preview-body p {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #475569;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.md-preview-body a {
|
||||
color: {{ $accentColor }};
|
||||
text-decoration: underline;
|
||||
text-decoration-color: {{ $accentColor }}40;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.md-preview-body a:hover {
|
||||
text-decoration-color: {{ $accentColor }};
|
||||
}
|
||||
|
||||
.md-preview-body strong {
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.md-preview-body img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 1rem 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.md-preview-body hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background: #e2e8f0;
|
||||
margin: 1.8rem 0;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
.md-preview-body ul, .md-preview-body ol {
|
||||
padding-left: 1.5rem;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
.md-preview-body ul { list-style-type: disc; }
|
||||
.md-preview-body ol { list-style-type: decimal; }
|
||||
.md-preview-body li {
|
||||
margin-bottom: 0.3rem;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.md-preview-body table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.5rem 0;
|
||||
font-size: 13px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.md-preview-body th {
|
||||
background: #f8fafc;
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
}
|
||||
.md-preview-body td {
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
.md-preview-body tr:nth-child(even) td {
|
||||
background: #fafbfc;
|
||||
}
|
||||
.md-preview-body tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Code */
|
||||
.md-preview-body code {
|
||||
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 0.85em;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: #f1f5f9;
|
||||
color: #e11d48;
|
||||
}
|
||||
.md-preview-body pre {
|
||||
background: #0f172a;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.md-preview-body pre code {
|
||||
background: transparent;
|
||||
color: #e2e8f0;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Blockquotes */
|
||||
.md-preview-body blockquote {
|
||||
border-left: 3px solid {{ $accentColor }};
|
||||
margin: 1rem 0;
|
||||
padding: 0.5rem 0 0.5rem 1rem;
|
||||
color: #64748b;
|
||||
background: #f8fafc;
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
.md-preview-body blockquote p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Centered divs */
|
||||
.md-preview-body [align="center"],
|
||||
.md-preview-body div[align="center"] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Dark mode overrides */
|
||||
.dark .md-preview-root > div { background: #1e293b !important; border-color: #334155 !important; }
|
||||
.dark .md-preview-root > div > div:first-child { background: #0f172a !important; border-color: #334155 !important; }
|
||||
.dark .md-preview-root > div > div:first-child > div:nth-child(2) { background: #0f172a !important; border-color: #334155 !important; }
|
||||
.dark .md-preview-root > div > div:last-child { background: #0f172a !important; }
|
||||
.dark .md-preview-root > div > div:last-child > div { background: #1e293b !important; border-color: #334155 !important; }
|
||||
.dark .md-preview-body { color: #cbd5e1; }
|
||||
.dark .md-preview-body h1, .dark .md-preview-body h2 { color: #f1f5f9; }
|
||||
.dark .md-preview-body h2 { border-color: #334155; }
|
||||
.dark .md-preview-body h3, .dark .md-preview-body h4 { color: #e2e8f0; }
|
||||
.dark .md-preview-body p, .dark .md-preview-body li { color: #94a3b8; }
|
||||
.dark .md-preview-body strong { color: #f1f5f9; }
|
||||
.dark .md-preview-body th { background: #0f172a; color: #f1f5f9; border-color: #334155; }
|
||||
.dark .md-preview-body td { border-color: #1e293b; color: #94a3b8; }
|
||||
.dark .md-preview-body tr:nth-child(even) td { background: #0f172a40; }
|
||||
.dark .md-preview-body code { background: #334155; color: #fb7185; }
|
||||
.dark .md-preview-body hr { background: #334155; }
|
||||
.dark .md-preview-body blockquote { background: #0f172a; color: #94a3b8; }
|
||||
.mdprev-body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #334155;
|
||||
}
|
||||
.mdprev-body h1 {
|
||||
font-family: 'Outfit', 'Inter', sans-serif;
|
||||
font-size: 1.5rem; font-weight: 800; color: #0f172a;
|
||||
margin: 2rem 0 0.8rem; line-height: 1.25; letter-spacing: -0.02em;
|
||||
}
|
||||
.mdprev-body h2 {
|
||||
font-family: 'Outfit', 'Inter', sans-serif;
|
||||
font-size: 1.25rem; font-weight: 700; color: #1e293b;
|
||||
margin: 1.8rem 0 0.7rem; padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #f1f5f9; line-height: 1.3;
|
||||
}
|
||||
.mdprev-body h3 {
|
||||
font-family: 'Outfit', 'Inter', sans-serif;
|
||||
font-size: 1.1rem; font-weight: 600; color: #334155;
|
||||
margin: 1.5rem 0 0.5rem;
|
||||
}
|
||||
.mdprev-body h4 {
|
||||
font-size: 1rem; font-weight: 600; color: #475569;
|
||||
margin: 1.2rem 0 0.4rem;
|
||||
}
|
||||
.mdprev-body p { margin: 0 0 1rem; color: #475569; }
|
||||
.mdprev-body a { color: {{ $accentColor }}; text-underline-offset: 2px; }
|
||||
.mdprev-body strong { font-weight: 700; color: #1e293b; }
|
||||
.mdprev-body em { font-style: italic; }
|
||||
.mdprev-body img { max-width: 100%; border-radius: 8px; margin: 1rem 0; display: block; }
|
||||
.mdprev-body hr { border: 0; height: 1px; background: #e2e8f0; margin: 1.8rem 0; }
|
||||
.mdprev-body ul { list-style-type: disc; padding-left: 1.5rem; margin: 0 0 1rem; }
|
||||
.mdprev-body ol { list-style-type: decimal; padding-left: 1.5rem; margin: 0 0 1rem; }
|
||||
.mdprev-body li { margin-bottom: 0.3rem; color: #475569; line-height: 1.6; }
|
||||
.mdprev-body table {
|
||||
width: 100%; border-collapse: collapse; margin: 1.5rem 0;
|
||||
font-size: 13px; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden;
|
||||
}
|
||||
.mdprev-body th {
|
||||
background: #f8fafc; color: #0f172a; font-weight: 600;
|
||||
text-align: left; padding: 10px 14px; border-bottom: 2px solid #e2e8f0;
|
||||
}
|
||||
.mdprev-body td { padding: 10px 14px; border-bottom: 1px solid #f1f5f9; color: #475569; }
|
||||
.mdprev-body tr:nth-child(even) td { background: #fafbfc; }
|
||||
.mdprev-body tr:last-child td { border-bottom: none; }
|
||||
.mdprev-body code {
|
||||
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 0.85em; padding: 2px 6px; border-radius: 4px;
|
||||
background: #f1f5f9; color: #e11d48;
|
||||
}
|
||||
.mdprev-body pre {
|
||||
background: #0f172a; border-radius: 8px;
|
||||
padding: 16px; overflow-x: auto; margin: 1rem 0;
|
||||
}
|
||||
.mdprev-body pre code { background: transparent; color: #e2e8f0; padding: 0; font-size: 13px; }
|
||||
.mdprev-body blockquote {
|
||||
border-left: 3px solid {{ $accentColor }}; margin: 1rem 0;
|
||||
padding: 0.5rem 0 0.5rem 1rem; color: #64748b;
|
||||
background: #f8fafc; border-radius: 0 6px 6px 0;
|
||||
}
|
||||
.mdprev-body blockquote p { margin: 0; }
|
||||
</style>
|
||||
|
||||
@@ -174,13 +174,11 @@
|
||||
.mermaid-container {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
margin: 2rem 0;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow-x: auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.dark .mermaid-container {
|
||||
background: #0f172a;
|
||||
@@ -188,6 +186,160 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Mermaid zoom toolbar */
|
||||
.mermaid-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(248, 250, 252, 0.9);
|
||||
backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 16px 16px 0 0;
|
||||
position: relative;
|
||||
}
|
||||
.dark .mermaid-toolbar {
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.mermaid-toolbar-label {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #94a3b8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mermaid-toolbar-hint {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.dark .mermaid-toolbar-hint {
|
||||
color: #94a3b8;
|
||||
}
|
||||
.mermaid-container.mermaid-active .mermaid-toolbar-hint {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.mermaid-toolbar-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mermaid-zoom-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
transition: all 0.15s ease;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dark .mermaid-zoom-btn {
|
||||
background: #1e293b;
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
color: #94a3b8;
|
||||
}
|
||||
.mermaid-zoom-btn:hover {
|
||||
background: var(--accent-color, #4f46e5);
|
||||
border-color: var(--accent-color, #4f46e5);
|
||||
color: #fff;
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 8px rgba(79, 70, 229, 0.3);
|
||||
}
|
||||
.mermaid-zoom-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.mermaid-zoom-pct {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
min-width: 38px;
|
||||
text-align: center;
|
||||
font-family: 'Inter', monospace;
|
||||
}
|
||||
.dark .mermaid-zoom-pct { color: #64748b; }
|
||||
|
||||
/* Mermaid svg canvas area */
|
||||
.mermaid-canvas {
|
||||
width: 100%;
|
||||
height: 520px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mermaid-canvas.mermaid-active {
|
||||
cursor: grab;
|
||||
}
|
||||
.mermaid-canvas.mermaid-active:active { cursor: grabbing; }
|
||||
.mermaid-canvas .mermaid {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.mermaid-canvas svg {
|
||||
display: block;
|
||||
}
|
||||
.mermaid-container.mermaid-active {
|
||||
box-shadow: 0 0 0 2px var(--accent-color, #4f46e5);
|
||||
}
|
||||
|
||||
/* Fullscreen overlay */
|
||||
.mermaid-fullscreen-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
background: rgba(2, 6, 23, 0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.mermaid-fullscreen-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
.mermaid-fullscreen-inner {
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
width: 92vw;
|
||||
max-width: 1400px;
|
||||
height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 40px 80px rgba(0,0,0,0.5);
|
||||
}
|
||||
.dark .mermaid-fullscreen-inner {
|
||||
background: #0f172a;
|
||||
}
|
||||
.mermaid-fullscreen-canvas {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: grab;
|
||||
}
|
||||
.mermaid-fullscreen-canvas:active { cursor: grabbing; }
|
||||
.mermaid-fullscreen-canvas svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Bullet lists */
|
||||
.proposal-content ul, .proposal-content ol {
|
||||
padding-left: 1.5rem;
|
||||
@@ -682,6 +834,8 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<!-- Mermaid.js for drawing diagrams -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.8.0/dist/mermaid.min.js"></script>
|
||||
<!-- SVG Pan Zoom for interactive Mermaid diagrams -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
|
||||
<!-- Confetti for approval celebration -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
|
||||
<!-- Lucide Premium Icons -->
|
||||
@@ -819,6 +973,62 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Mermaid zoom state store
|
||||
const mermaidZoomInstances = {};
|
||||
let mermaidActiveIndex = null;
|
||||
|
||||
function setMermaidActive(index) {
|
||||
if (mermaidActiveIndex !== null && mermaidActiveIndex !== index) {
|
||||
deactivateMermaid(mermaidActiveIndex);
|
||||
}
|
||||
mermaidActiveIndex = index;
|
||||
|
||||
const wrapper = document.getElementById('mermaid-wrapper-' + index);
|
||||
const canvas = document.getElementById('mermaid-canvas-' + index);
|
||||
const pz = mermaidZoomInstances[index];
|
||||
if (!wrapper || !canvas || !pz) return;
|
||||
|
||||
wrapper.classList.add('mermaid-active');
|
||||
canvas.classList.add('mermaid-active');
|
||||
pz.enablePan();
|
||||
}
|
||||
|
||||
function deactivateMermaid(index) {
|
||||
const wrapper = document.getElementById('mermaid-wrapper-' + index);
|
||||
const canvas = document.getElementById('mermaid-canvas-' + index);
|
||||
const pz = mermaidZoomInstances[index];
|
||||
if (wrapper) wrapper.classList.remove('mermaid-active');
|
||||
if (canvas) canvas.classList.remove('mermaid-active');
|
||||
if (pz) pz.disablePan();
|
||||
if (mermaidActiveIndex === index) mermaidActiveIndex = null;
|
||||
}
|
||||
|
||||
function deactivateAllMermaid() {
|
||||
if (mermaidActiveIndex !== null) {
|
||||
deactivateMermaid(mermaidActiveIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function setupMermaidActivation(index) {
|
||||
const canvas = document.getElementById('mermaid-canvas-' + index);
|
||||
if (!canvas || canvas.dataset.activationBound) return;
|
||||
canvas.dataset.activationBound = '1';
|
||||
|
||||
canvas.addEventListener('click', function() {
|
||||
setMermaidActive(index);
|
||||
});
|
||||
|
||||
canvas.addEventListener('wheel', function(e) {
|
||||
if (!canvas.classList.contains('mermaid-active')) return;
|
||||
e.preventDefault();
|
||||
const pz = mermaidZoomInstances[index];
|
||||
if (!pz) return;
|
||||
if (e.deltaY < 0) pz.zoomIn();
|
||||
else pz.zoomOut();
|
||||
updatePct(index, pz.getZoom());
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
// Scan code blocks for mermaid and convert them
|
||||
const codeElements = contentDiv.querySelectorAll('pre code.language-mermaid');
|
||||
codeElements.forEach((codeEl, index) => {
|
||||
@@ -829,26 +1039,320 @@
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'mermaid-container no-print transition-all duration-300';
|
||||
wrapper.id = 'mermaid-wrapper-' + index;
|
||||
wrapper.dataset.mermaidSource = rawMermaid;
|
||||
|
||||
// Zoom Toolbar
|
||||
const toolbar = document.createElement('div');
|
||||
toolbar.className = 'mermaid-toolbar';
|
||||
toolbar.innerHTML = `
|
||||
<span class="mermaid-toolbar-label">Diyagram Görünümü</span>
|
||||
<span class="mermaid-toolbar-hint">Zoom ve kaydırma için tıklayın</span>
|
||||
<div class="mermaid-toolbar-controls">
|
||||
<button class="mermaid-zoom-btn" title="Yakınlaştır (Zoom In)" onclick="mermaidZoomIn(${index})">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<span class="mermaid-zoom-pct" id="mermaid-pct-${index}">100%</span>
|
||||
<button class="mermaid-zoom-btn" title="Uzaklaştır (Zoom Out)" onclick="mermaidZoomOut(${index})">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<button class="mermaid-zoom-btn" title="Tam Boyut (1:1)" onclick="mermaidZoomReset(${index})">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
|
||||
</button>
|
||||
<button class="mermaid-zoom-btn" title="Tam Ekran" onclick="mermaidFullscreen(${index})">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Canvas wrapper
|
||||
const canvas = document.createElement('div');
|
||||
canvas.className = 'mermaid-canvas';
|
||||
canvas.id = 'mermaid-canvas-' + index;
|
||||
|
||||
const graphDiv = document.createElement('div');
|
||||
graphDiv.className = 'mermaid w-full';
|
||||
graphDiv.id = 'mermaid-graph-' + index;
|
||||
graphDiv.textContent = rawMermaid;
|
||||
|
||||
wrapper.appendChild(graphDiv);
|
||||
canvas.appendChild(graphDiv);
|
||||
wrapper.appendChild(toolbar);
|
||||
wrapper.appendChild(canvas);
|
||||
preContainer.parentNode.replaceChild(wrapper, preContainer);
|
||||
});
|
||||
|
||||
// Fullscreen overlay (singleton)
|
||||
const fsOverlay = document.createElement('div');
|
||||
fsOverlay.className = 'mermaid-fullscreen-overlay';
|
||||
fsOverlay.id = 'mermaid-fs-overlay';
|
||||
fsOverlay.innerHTML = `
|
||||
<div class="mermaid-fullscreen-inner" id="mermaid-fs-inner">
|
||||
<div class="mermaid-toolbar" style="border-radius:20px 20px 0 0;">
|
||||
<span class="mermaid-toolbar-label">Tam Ekran Görünüm</span>
|
||||
<button class="mermaid-zoom-btn" title="Yakınlaştır" onclick="mermaidFsZoom('in')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<span class="mermaid-zoom-pct" id="mermaid-fs-pct">100%</span>
|
||||
<button class="mermaid-zoom-btn" title="Uzaklaştır" onclick="mermaidFsZoom('out')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<button class="mermaid-zoom-btn" title="1:1 Sıfırla" onclick="mermaidFsZoom('reset')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
|
||||
</button>
|
||||
<button class="mermaid-zoom-btn" title="Kapat" onclick="closeMermaidFullscreen()" style="background:#ef4444;border-color:#ef4444;color:#fff;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mermaid-fullscreen-canvas" id="mermaid-fs-canvas"></div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(fsOverlay);
|
||||
fsOverlay.addEventListener('click', function(e) { if (e.target === fsOverlay) closeMermaidFullscreen(); });
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('.mermaid-container')) {
|
||||
deactivateAllMermaid();
|
||||
}
|
||||
});
|
||||
|
||||
// Run mermaid parser
|
||||
setTimeout(() => {
|
||||
mermaid.run().then(() => {
|
||||
// Re-init lucide icons inside alerts after marked rendering completes
|
||||
lucide.createIcons();
|
||||
|
||||
// Initialize svg-pan-zoom for each mermaid diagram
|
||||
codeElements.forEach((_, index) => {
|
||||
initMermaidZoom(index);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error("Mermaid drawing error:", err);
|
||||
});
|
||||
}, 100);
|
||||
|
||||
// Init svg-pan-zoom for a given mermaid index
|
||||
function initMermaidZoom(index) {
|
||||
const canvas = document.getElementById('mermaid-canvas-' + index);
|
||||
if (!canvas) return;
|
||||
const svg = canvas.querySelector('svg');
|
||||
if (!svg) return;
|
||||
|
||||
// Ensure SVG has an ID for svg-pan-zoom
|
||||
if (!svg.id) svg.id = 'mermaid-svg-' + index;
|
||||
|
||||
// Compute a good canvas height from SVG viewBox aspect ratio
|
||||
const canvasW = canvas.clientWidth || canvas.offsetWidth || 800;
|
||||
let canvasH = 520;
|
||||
const vb = svg.getAttribute('viewBox');
|
||||
if (vb) {
|
||||
const parts = vb.split(/[\s,]+/);
|
||||
if (parts.length >= 4) {
|
||||
const svgW = parseFloat(parts[2]);
|
||||
const svgH = parseFloat(parts[3]);
|
||||
if (svgW > 0 && svgH > 0) {
|
||||
canvasH = Math.max(400, Math.min(Math.round(svgH * (canvasW / svgW)), 900));
|
||||
}
|
||||
}
|
||||
}
|
||||
canvas.style.height = canvasH + 'px';
|
||||
|
||||
// svg-pan-zoom needs explicit width/height on the SVG element
|
||||
svg.setAttribute('width', canvasW);
|
||||
svg.setAttribute('height', canvasH);
|
||||
svg.style.width = canvasW + 'px';
|
||||
svg.style.height = canvasH + 'px';
|
||||
|
||||
try {
|
||||
const panZoom = svgPanZoom('#' + svg.id, {
|
||||
zoomEnabled: true,
|
||||
panEnabled: false,
|
||||
controlIconsEnabled: false,
|
||||
fit: true,
|
||||
center: true,
|
||||
minZoom: 0.3,
|
||||
maxZoom: 8,
|
||||
zoomScaleSensitivity: 0.3,
|
||||
mouseWheelZoomEnabled: false,
|
||||
onZoom: (zoom) => {
|
||||
const pctEl = document.getElementById('mermaid-pct-' + index);
|
||||
if (pctEl) pctEl.textContent = Math.round(zoom * 100) + '%';
|
||||
}
|
||||
});
|
||||
mermaidZoomInstances[index] = panZoom;
|
||||
setupMermaidActivation(index);
|
||||
} catch(e) {
|
||||
console.warn('svg-pan-zoom init failed for mermaid-' + index, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Zoom controls
|
||||
function mermaidZoomIn(index) {
|
||||
setMermaidActive(index);
|
||||
const pz = mermaidZoomInstances[index];
|
||||
if (pz) { pz.zoomIn(); updatePct(index, pz.getZoom()); }
|
||||
}
|
||||
function mermaidZoomOut(index) {
|
||||
setMermaidActive(index);
|
||||
const pz = mermaidZoomInstances[index];
|
||||
if (pz) { pz.zoomOut(); updatePct(index, pz.getZoom()); }
|
||||
}
|
||||
function mermaidZoomReset(index) {
|
||||
setMermaidActive(index);
|
||||
const pz = mermaidZoomInstances[index];
|
||||
if (pz) { pz.resetZoom(); pz.center(); updatePct(index, pz.getZoom()); }
|
||||
}
|
||||
function updatePct(index, zoom) {
|
||||
const pctEl = document.getElementById('mermaid-pct-' + index);
|
||||
if (pctEl) pctEl.textContent = Math.round(zoom * 100) + '%';
|
||||
}
|
||||
|
||||
window.mermaidZoomIn = mermaidZoomIn;
|
||||
window.mermaidZoomOut = mermaidZoomOut;
|
||||
window.mermaidZoomReset = mermaidZoomReset;
|
||||
|
||||
// Fullscreen — independent render; original diagram is never moved or destroyed
|
||||
let fsActivePanZoom = null;
|
||||
let fsActiveIndex = null;
|
||||
|
||||
function uniquifySvgIds(svgRoot, prefix) {
|
||||
const idMap = {};
|
||||
svgRoot.querySelectorAll('[id]').forEach((el) => {
|
||||
const oldId = el.id;
|
||||
const newId = prefix + '-' + oldId;
|
||||
idMap[oldId] = newId;
|
||||
el.id = newId;
|
||||
});
|
||||
|
||||
const urlAttrs = ['fill', 'stroke', 'marker-end', 'marker-start', 'marker-mid', 'clip-path', 'mask', 'filter', 'href', 'xlink:href'];
|
||||
svgRoot.querySelectorAll('*').forEach((el) => {
|
||||
urlAttrs.forEach((attr) => {
|
||||
const val = el.getAttribute(attr);
|
||||
if (!val || !val.includes('url(#')) return;
|
||||
const updated = val.replace(/url\(#([^)]+)\)/g, (match, id) => (
|
||||
idMap[id] ? 'url(#' + idMap[id] + ')' : match
|
||||
));
|
||||
if (updated !== val) el.setAttribute(attr, updated);
|
||||
});
|
||||
|
||||
const style = el.getAttribute('style');
|
||||
if (style && style.includes('url(#')) {
|
||||
const updatedStyle = style.replace(/url\(#([^)]+)\)/g, (match, id) => (
|
||||
idMap[id] ? 'url(#' + idMap[id] + ')' : match
|
||||
));
|
||||
if (updatedStyle !== style) el.setAttribute('style', updatedStyle);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initMermaidFsPanZoom(index, svgId) {
|
||||
const fsCanvas = document.getElementById('mermaid-fs-canvas');
|
||||
const svg = document.getElementById(svgId);
|
||||
if (!svg || !fsCanvas) return;
|
||||
|
||||
const fsW = fsCanvas.clientWidth || fsCanvas.offsetWidth || 1000;
|
||||
const fsH = fsCanvas.clientHeight || fsCanvas.offsetHeight || 600;
|
||||
|
||||
svg.setAttribute('width', fsW);
|
||||
svg.setAttribute('height', fsH);
|
||||
svg.style.width = fsW + 'px';
|
||||
svg.style.height = fsH + 'px';
|
||||
|
||||
try {
|
||||
if (fsActivePanZoom) {
|
||||
fsActivePanZoom.destroy();
|
||||
fsActivePanZoom = null;
|
||||
}
|
||||
fsActivePanZoom = svgPanZoom('#' + svgId, {
|
||||
zoomEnabled: true,
|
||||
panEnabled: true,
|
||||
controlIconsEnabled: false,
|
||||
fit: true,
|
||||
center: true,
|
||||
minZoom: 0.2,
|
||||
maxZoom: 10,
|
||||
zoomScaleSensitivity: 0.3,
|
||||
mouseWheelZoomEnabled: true,
|
||||
onZoom: (zoom) => {
|
||||
const pctEl = document.getElementById('mermaid-fs-pct');
|
||||
if (pctEl) pctEl.textContent = Math.round(zoom * 100) + '%';
|
||||
}
|
||||
});
|
||||
const pctEl = document.getElementById('mermaid-fs-pct');
|
||||
if (pctEl) pctEl.textContent = Math.round(fsActivePanZoom.getZoom() * 100) + '%';
|
||||
} catch (e) {
|
||||
console.warn('fs pan-zoom init error', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function mermaidFullscreen(index) {
|
||||
deactivateAllMermaid();
|
||||
const wrapper = document.getElementById('mermaid-wrapper-' + index);
|
||||
if (!wrapper) return;
|
||||
const rawMermaid = wrapper.dataset.mermaidSource;
|
||||
if (!rawMermaid) return;
|
||||
|
||||
const fsCanvas = document.getElementById('mermaid-fs-canvas');
|
||||
fsCanvas.innerHTML = '';
|
||||
|
||||
fsActiveIndex = index;
|
||||
const renderId = 'mermaid-fs-' + index + '-' + Date.now();
|
||||
|
||||
document.getElementById('mermaid-fs-overlay').classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
try {
|
||||
const { svg, bindFunctions } = await mermaid.render(renderId, rawMermaid);
|
||||
fsCanvas.innerHTML = svg;
|
||||
if (typeof bindFunctions === 'function') {
|
||||
bindFunctions(fsCanvas);
|
||||
}
|
||||
|
||||
const svgEl = fsCanvas.querySelector('svg');
|
||||
if (!svgEl) return;
|
||||
|
||||
uniquifySvgIds(svgEl, renderId);
|
||||
const svgId = renderId + '-svg';
|
||||
svgEl.id = svgId;
|
||||
|
||||
// Wait for overlay layout before pan-zoom init
|
||||
setTimeout(() => initMermaidFsPanZoom(index, svgId), 100);
|
||||
} catch (err) {
|
||||
console.warn('fs mermaid render error', err);
|
||||
closeMermaidFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
function closeMermaidFullscreen() {
|
||||
document.getElementById('mermaid-fs-overlay').classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
|
||||
if (fsActivePanZoom) {
|
||||
try { fsActivePanZoom.destroy(); } catch (e) {}
|
||||
fsActivePanZoom = null;
|
||||
}
|
||||
|
||||
const fsCanvas = document.getElementById('mermaid-fs-canvas');
|
||||
if (fsCanvas) fsCanvas.innerHTML = '';
|
||||
|
||||
fsActiveIndex = null;
|
||||
}
|
||||
|
||||
function mermaidFsZoom(action) {
|
||||
if (!fsActivePanZoom) return;
|
||||
if (action === 'in') fsActivePanZoom.zoomIn();
|
||||
else if (action === 'out') fsActivePanZoom.zoomOut();
|
||||
else if (action === 'reset') { fsActivePanZoom.resetZoom(); fsActivePanZoom.center(); }
|
||||
const pctEl = document.getElementById('mermaid-fs-pct');
|
||||
if (pctEl) pctEl.textContent = Math.round(fsActivePanZoom.getZoom() * 100) + '%';
|
||||
}
|
||||
|
||||
window.mermaidFullscreen = mermaidFullscreen;
|
||||
window.closeMermaidFullscreen = closeMermaidFullscreen;
|
||||
window.mermaidFsZoom = mermaidFsZoom;
|
||||
// Keyboard ESC closes fullscreen
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeMermaidFullscreen();
|
||||
});
|
||||
|
||||
// 3. Dynamic Sidebar TOC Generator
|
||||
const tocNav = document.getElementById('toc-nav');
|
||||
const headings = contentDiv.querySelectorAll('h2, h3');
|
||||
|
||||
Reference in New Issue
Block a user