feat: implement tabbed interface with side-by-side markdown code editor and live preview for proposals
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Filament\Admin\Resources\Proposals\Pages;
|
|||||||
|
|
||||||
use App\Filament\Admin\Resources\Proposals\ProposalResource;
|
use App\Filament\Admin\Resources\Proposals\ProposalResource;
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
use Filament\Support\Enums\Width;
|
||||||
|
|
||||||
class CreateProposal extends CreateRecord
|
class CreateProposal extends CreateRecord
|
||||||
{
|
{
|
||||||
@@ -13,4 +14,10 @@ class CreateProposal extends CreateRecord
|
|||||||
{
|
{
|
||||||
return __('proposal.create');
|
return __('proposal.create');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getMaxContentWidth(): Width | string | null
|
||||||
|
{
|
||||||
|
return Width::Full;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Filament\Admin\Resources\Proposals\Pages;
|
|||||||
|
|
||||||
use App\Filament\Admin\Resources\Proposals\ProposalResource;
|
use App\Filament\Admin\Resources\Proposals\ProposalResource;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
use Filament\Support\Enums\Width;
|
||||||
|
|
||||||
class EditProposal extends EditRecord
|
class EditProposal extends EditRecord
|
||||||
{
|
{
|
||||||
@@ -13,4 +14,10 @@ class EditProposal extends EditRecord
|
|||||||
{
|
{
|
||||||
return __('proposal.edit');
|
return __('proposal.edit');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getMaxContentWidth(): Width | string | null
|
||||||
|
{
|
||||||
|
return Width::Full;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,14 @@ namespace App\Filament\Admin\Resources\Proposals\Schemas;
|
|||||||
use Filament\Forms\Components\Checkbox;
|
use Filament\Forms\Components\Checkbox;
|
||||||
use Filament\Forms\Components\DatePicker;
|
use Filament\Forms\Components\DatePicker;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Schemas\Components\Tabs;
|
||||||
|
use Filament\Schemas\Components\Tabs\Tab;
|
||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\CodeEditor;
|
||||||
|
use Filament\Forms\Components\CodeEditor\Enums\Language as CodeEditorLanguage;
|
||||||
use Filament\Schemas\Components\Section;
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Schemas\Components\View;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
|
|
||||||
class ProposalForm
|
class ProposalForm
|
||||||
@@ -15,10 +20,39 @@ class ProposalForm
|
|||||||
public static function configure(Schema $schema): Schema
|
public static function configure(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $schema
|
return $schema
|
||||||
->columns(3)
|
|
||||||
->schema([
|
->schema([
|
||||||
// Sol kolon - Ana içerik (2 sütun genişliğinde)
|
Tabs::make('Tabs')
|
||||||
|
->tabs([
|
||||||
|
Tab::make('Teklif Detayları ve İçerik')
|
||||||
|
->icon('heroicon-m-document-text')
|
||||||
|
->schema([
|
||||||
|
// Section 2: side-by-side Markdown editor (CodeMirror) + live preview
|
||||||
Section::make(__('proposal.content_section'))
|
Section::make(__('proposal.content_section'))
|
||||||
|
->description('Sol tarafta Markdown formatında içeriği düzenleyin, sağ tarafta canlı olarak nasıl görüneceğini izleyin.')
|
||||||
|
->columns(2)
|
||||||
|
->schema([
|
||||||
|
CodeEditor::make('content')
|
||||||
|
->label(__('proposal.content_field'))
|
||||||
|
->hiddenLabel()
|
||||||
|
->required()
|
||||||
|
->language(CodeEditorLanguage::Markdown)
|
||||||
|
->live(onBlur: false)
|
||||||
|
->columnSpan(1)
|
||||||
|
->extraAttributes([
|
||||||
|
'style' => 'min-height: 700px; max-height: 700px; overflow-y: auto;',
|
||||||
|
'data-field-name' => 'content',
|
||||||
|
]),
|
||||||
|
|
||||||
|
View::make('filament.components.markdown-preview')
|
||||||
|
->columnSpan(1),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
Tab::make('Durum ve Ayarlar')
|
||||||
|
->icon('heroicon-m-cog-6-tooth')
|
||||||
|
->schema([
|
||||||
|
// Section 1: compact top metadata and configuration
|
||||||
|
Section::make(__('proposal.settings_section'))
|
||||||
|
->columns(12)
|
||||||
->schema([
|
->schema([
|
||||||
TextInput::make('title')
|
TextInput::make('title')
|
||||||
->label(__('proposal.title_field'))
|
->label(__('proposal.title_field'))
|
||||||
@@ -30,7 +64,8 @@ class ProposalForm
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$set('slug', \Str::slug($state));
|
$set('slug', \Str::slug($state));
|
||||||
}),
|
})
|
||||||
|
->columnSpan(4),
|
||||||
|
|
||||||
TextInput::make('slug')
|
TextInput::make('slug')
|
||||||
->label(__('proposal.slug_field'))
|
->label(__('proposal.slug_field'))
|
||||||
@@ -38,30 +73,9 @@ class ProposalForm
|
|||||||
->maxLength(255)
|
->maxLength(255)
|
||||||
->unique(ignoreRecord: true)
|
->unique(ignoreRecord: true)
|
||||||
->rules(['alpha_dash'])
|
->rules(['alpha_dash'])
|
||||||
->helperText(__('proposal.slug_helper')),
|
->helperText(__('proposal.slug_helper'))
|
||||||
|
->columnSpan(4),
|
||||||
|
|
||||||
Textarea::make('content')
|
|
||||||
->label(__('proposal.content_field'))
|
|
||||||
->required()
|
|
||||||
->rows(18)
|
|
||||||
->extraInputAttributes(['style' => 'font-family: monospace; font-size: 0.9rem; line-height: 1.5;'])
|
|
||||||
->columnSpanFull(),
|
|
||||||
|
|
||||||
Textarea::make('client_feedback')
|
|
||||||
->label('Müşteri Geri Bildirimi / Revizyon Notu')
|
|
||||||
->rows(3)
|
|
||||||
->disabled()
|
|
||||||
->dehydrated(false)
|
|
||||||
->placeholder('Henüz geri bildirim bırakılmadı.')
|
|
||||||
->visible(fn ($record) => $record !== null && !empty($record->client_feedback))
|
|
||||||
->columnSpanFull(),
|
|
||||||
])
|
|
||||||
->columnSpan(2)
|
|
||||||
->collapsible(false),
|
|
||||||
|
|
||||||
// Sağ kolon - Durum, Fiyat, Müşteri ve Tasarım (1 sütun genişliğinde)
|
|
||||||
Section::make(__('proposal.settings_section'))
|
|
||||||
->schema([
|
|
||||||
Select::make('status')
|
Select::make('status')
|
||||||
->label(__('proposal.status_field'))
|
->label(__('proposal.status_field'))
|
||||||
->options([
|
->options([
|
||||||
@@ -73,19 +87,25 @@ class ProposalForm
|
|||||||
])
|
])
|
||||||
->default('draft')
|
->default('draft')
|
||||||
->required()
|
->required()
|
||||||
->columnSpanFull(),
|
->columnSpan(4),
|
||||||
|
|
||||||
TextInput::make('client_name')
|
TextInput::make('client_name')
|
||||||
->label(__('proposal.client_name_field'))
|
->label(__('proposal.client_name_field'))
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255)
|
->maxLength(255)
|
||||||
->columnSpanFull(),
|
->columnSpan(4),
|
||||||
|
|
||||||
TextInput::make('client_email')
|
TextInput::make('client_email')
|
||||||
->label(__('proposal.client_email_field'))
|
->label(__('proposal.client_email_field'))
|
||||||
->email()
|
->email()
|
||||||
->maxLength(255)
|
->maxLength(255)
|
||||||
->columnSpanFull(),
|
->columnSpan(4),
|
||||||
|
|
||||||
|
DatePicker::make('valid_until')
|
||||||
|
->label(__('proposal.valid_until_field'))
|
||||||
|
->native(false)
|
||||||
|
->displayFormat('d.m.Y')
|
||||||
|
->columnSpan(4),
|
||||||
|
|
||||||
TextInput::make('total_price')
|
TextInput::make('total_price')
|
||||||
->label(__('proposal.total_price_field'))
|
->label(__('proposal.total_price_field'))
|
||||||
@@ -95,7 +115,7 @@ class ProposalForm
|
|||||||
'EUR' => '€',
|
'EUR' => '€',
|
||||||
default => '₺',
|
default => '₺',
|
||||||
})
|
})
|
||||||
->columnSpan(2),
|
->columnSpan(3),
|
||||||
|
|
||||||
Select::make('currency')
|
Select::make('currency')
|
||||||
->label(__('proposal.currency_field'))
|
->label(__('proposal.currency_field'))
|
||||||
@@ -109,12 +129,6 @@ class ProposalForm
|
|||||||
->live()
|
->live()
|
||||||
->columnSpan(1),
|
->columnSpan(1),
|
||||||
|
|
||||||
DatePicker::make('valid_until')
|
|
||||||
->label(__('proposal.valid_until_field'))
|
|
||||||
->native(false)
|
|
||||||
->displayFormat('d.m.Y')
|
|
||||||
->columnSpanFull(),
|
|
||||||
|
|
||||||
Select::make('meta.accent_color')
|
Select::make('meta.accent_color')
|
||||||
->label(__('proposal.accent_color_field'))
|
->label(__('proposal.accent_color_field'))
|
||||||
->options([
|
->options([
|
||||||
@@ -125,16 +139,26 @@ class ProposalForm
|
|||||||
'amber' => 'Amber Executive (Kehribar/Altın)',
|
'amber' => 'Amber Executive (Kehribar/Altın)',
|
||||||
])
|
])
|
||||||
->default('indigo')
|
->default('indigo')
|
||||||
->columnSpanFull(),
|
->live()
|
||||||
|
->columnSpan(4),
|
||||||
|
|
||||||
Checkbox::make('meta.show_calculator')
|
Checkbox::make('meta.show_calculator')
|
||||||
->label(__('proposal.show_calculator_field'))
|
->label(__('proposal.show_calculator_field'))
|
||||||
->default(true)
|
->default(true)
|
||||||
|
->columnSpan(4),
|
||||||
|
|
||||||
|
// Client feedback inside this section — only visible when present
|
||||||
|
Textarea::make('client_feedback')
|
||||||
|
->label('Müşteri Geri Bildirimi / Revizyon Notu')
|
||||||
|
->rows(3)
|
||||||
|
->disabled()
|
||||||
|
->dehydrated(false)
|
||||||
|
->placeholder('Henüz geri bildirim bırakılmadı.')
|
||||||
|
->visible(fn ($record) => $record !== null && !empty($record->client_feedback))
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
])
|
]),
|
||||||
->columns(3)
|
]),
|
||||||
->columnSpan(1)
|
])->columnSpanFull()
|
||||||
->collapsible(false),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,344 @@
|
|||||||
|
@php
|
||||||
|
// Server-side: get initial values for the header metadata
|
||||||
|
$accent = $get('meta.accent_color') ?? 'indigo';
|
||||||
|
$accentColor = match($accent) {
|
||||||
|
'emerald' => '#059669',
|
||||||
|
'cyberpunk' => '#ec4899',
|
||||||
|
'coral' => '#ea580c',
|
||||||
|
'amber' => '#d97706',
|
||||||
|
default => '#4f46e5',
|
||||||
|
};
|
||||||
|
$accentGradient = match($accent) {
|
||||||
|
'emerald' => 'linear-gradient(135deg, #059669 0%, #10b981 100%)',
|
||||||
|
'cyberpunk' => 'linear-gradient(135deg, #ec4899 0%, #f43f5e 100%)',
|
||||||
|
'coral' => 'linear-gradient(135deg, #ea580c 0%, #f97316 100%)',
|
||||||
|
'amber' => 'linear-gradient(135deg, #d97706 0%, #f59e0b 100%)',
|
||||||
|
default => 'linear-gradient(135deg, #4f46e5 0%, #6366f1 100%)',
|
||||||
|
};
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
{{-- Google Fonts --}}
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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);
|
||||||
|
|
||||||
|
// 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 --}}
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
<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 --}}
|
||||||
|
<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 --}}
|
||||||
|
<div style="height: 4px; background: {{ $accentGradient }};"></div>
|
||||||
|
|
||||||
|
{{-- Document Content --}}
|
||||||
|
<div style="padding: 32px 40px;">
|
||||||
|
|
||||||
|
{{-- Document 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>
|
||||||
|
@if($get('client_name'))
|
||||||
|
<span style="font-size: 11px; background: #f8fafc; color: #64748b; font-weight: 600; padding: 3px 8px; border-radius: 4px; border: 1px solid #e2e8f0;">{{ $get('client_name') }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<h1 style="font-family: 'Outfit', sans-serif; font-size: 22px; font-weight: 800; color: #0f172a; margin: 0 0 8px 0; line-height: 1.3;">{{ $get('title') ?: 'Teklif Başlığı' }}</h1>
|
||||||
|
<div style="font-size: 11px; color: #94a3b8; font-weight: 500; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
|
<span>REF: {{ $get('slug') ?: '—' }}</span>
|
||||||
|
<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>
|
||||||
|
</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;"
|
||||||
|
>
|
||||||
|
✏️ Sol taraftaki editöre Markdown yazın, canlı önizleme burada görünecektir.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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; }
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user