@salism3/dsh-latex-editor 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js ADDED
@@ -0,0 +1,871 @@
1
+ window.__ModuleLoader__.load({
2
+ id: '@local/dsh-latex-editor',
3
+ factory(require) {
4
+ const React = require('react');
5
+ const h = React.createElement;
6
+
7
+ const STYLE_ID = 'dsh-latex-editor-styles';
8
+
9
+ function injectStyles() {
10
+ if (typeof document === 'undefined') return;
11
+ if (document.getElementById(STYLE_ID)) return;
12
+
13
+ const style = document.createElement('style');
14
+ style.id = STYLE_ID;
15
+ style.dataset.plugin = '@local/dsh-latex-editor';
16
+ style.textContent = `
17
+ /* ==========================================================
18
+ 1. LATEX STUDIO SPLIT PANE STYLES
19
+ ========================================================== */
20
+ .latex-editor-container {
21
+ display: flex;
22
+ flex-direction: column;
23
+ height: 100%;
24
+ width: 100%;
25
+ background: var(--dsw-alias-bg-base, #181825);
26
+ color: var(--dsw-alias-text-primary, #cdd6f4);
27
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
28
+ overflow: hidden;
29
+ box-sizing: border-box;
30
+ border-left: 1px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12));
31
+ }
32
+
33
+ .latex-editor-header {
34
+ display: flex;
35
+ align-items: center;
36
+ justify-content: space-between;
37
+ padding: 8px 12px;
38
+ gap: 8px;
39
+ background: var(--dsw-alias-bg-elevated, #1e1e2e);
40
+ border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.1));
41
+ flex-shrink: 0;
42
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
43
+ }
44
+
45
+ .latex-header-left {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 8px;
49
+ min-width: 0;
50
+ }
51
+
52
+ .latex-brand-icon {
53
+ width: 22px;
54
+ height: 22px;
55
+ flex-shrink: 0;
56
+ }
57
+
58
+ .latex-header-title {
59
+ font-size: 13px;
60
+ font-weight: 700;
61
+ letter-spacing: 0.2px;
62
+ color: var(--dsw-alias-text-primary, #cdd6f4);
63
+ white-space: nowrap;
64
+ }
65
+
66
+ .latex-entry-chip {
67
+ font-size: 11px;
68
+ font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
69
+ background: rgba(99, 102, 241, 0.18);
70
+ color: #a5b4fc;
71
+ padding: 2px 6px;
72
+ border-radius: 4px;
73
+ border: 1px solid rgba(99, 102, 241, 0.3);
74
+ white-space: nowrap;
75
+ }
76
+
77
+ .latex-badge {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ gap: 4px;
81
+ font-size: 11px;
82
+ font-weight: 600;
83
+ padding: 2px 8px;
84
+ border-radius: 12px;
85
+ white-space: nowrap;
86
+ }
87
+
88
+ .latex-badge-success {
89
+ background: rgba(16, 185, 129, 0.15);
90
+ color: #34d399;
91
+ border: 1px solid rgba(16, 185, 129, 0.35);
92
+ }
93
+
94
+ .latex-badge-warning {
95
+ background: rgba(245, 158, 11, 0.15);
96
+ color: #fbbf24;
97
+ border: 1px solid rgba(245, 158, 11, 0.35);
98
+ }
99
+
100
+ .latex-badge-error {
101
+ background: rgba(239, 68, 68, 0.15);
102
+ color: #f87171;
103
+ border: 1px solid rgba(239, 68, 68, 0.35);
104
+ }
105
+
106
+ .latex-badge-compiling {
107
+ background: rgba(99, 102, 241, 0.2);
108
+ color: #818cf8;
109
+ border: 1px solid rgba(99, 102, 241, 0.4);
110
+ animation: pulseGlow 1.5s infinite;
111
+ }
112
+
113
+ .latex-badge-idle {
114
+ background: rgba(148, 163, 184, 0.12);
115
+ color: #94a3b8;
116
+ border: 1px solid rgba(148, 163, 184, 0.25);
117
+ }
118
+
119
+ @keyframes pulseGlow {
120
+ 0%, 100% { opacity: 1; }
121
+ 50% { opacity: 0.6; }
122
+ }
123
+
124
+ .latex-header-actions {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 6px;
128
+ flex-shrink: 0;
129
+ }
130
+
131
+ .latex-select-engine {
132
+ background: var(--dsw-alias-bg-surface, #252538);
133
+ color: var(--dsw-alias-text-primary, #cdd6f4);
134
+ border: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.15));
135
+ border-radius: 6px;
136
+ padding: 4px 6px;
137
+ font-size: 11px;
138
+ font-weight: 500;
139
+ cursor: pointer;
140
+ outline: none;
141
+ }
142
+
143
+ .latex-select-engine:focus {
144
+ border-color: #6366f1;
145
+ }
146
+
147
+ .latex-btn-compile {
148
+ display: inline-flex;
149
+ align-items: center;
150
+ gap: 5px;
151
+ background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
152
+ color: #ffffff;
153
+ border: none;
154
+ border-radius: 6px;
155
+ padding: 5px 12px;
156
+ font-size: 12px;
157
+ font-weight: 600;
158
+ cursor: pointer;
159
+ transition: all 0.2s ease;
160
+ box-shadow: 0 2px 6px rgba(79, 70, 229, 0.35);
161
+ outline: none;
162
+ }
163
+
164
+ .latex-btn-compile:hover:not(:disabled) {
165
+ transform: translateY(-1px);
166
+ box-shadow: 0 4px 10px rgba(79, 70, 229, 0.5);
167
+ filter: brightness(1.08);
168
+ }
169
+
170
+ .latex-btn-compile:active:not(:disabled) {
171
+ transform: translateY(0);
172
+ }
173
+
174
+ .latex-btn-compile:disabled {
175
+ opacity: 0.65;
176
+ cursor: not-allowed;
177
+ filter: grayscale(0.2);
178
+ }
179
+
180
+ .latex-btn-icon {
181
+ display: inline-flex;
182
+ align-items: center;
183
+ justify-content: center;
184
+ gap: 5px;
185
+ background: var(--dsw-alias-bg-surface, #252538);
186
+ color: var(--dsw-alias-text-primary, #cdd6f4);
187
+ border: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.15));
188
+ border-radius: 6px;
189
+ padding: 5px 12px;
190
+ font-size: 12px;
191
+ font-weight: 500;
192
+ cursor: pointer;
193
+ transition: all 0.15s ease;
194
+ }
195
+
196
+ .latex-btn-icon:hover {
197
+ background: rgba(255, 255, 255, 0.08);
198
+ border-color: rgba(255, 255, 255, 0.25);
199
+ }
200
+
201
+ .latex-log-drawer {
202
+ background: #0f172a;
203
+ color: #e2e8f0;
204
+ border-bottom: 1px solid #334155;
205
+ max-height: 180px;
206
+ overflow-y: auto;
207
+ padding: 10px 14px;
208
+ font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
209
+ font-size: 11px;
210
+ line-height: 1.5;
211
+ white-space: pre-wrap;
212
+ box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.3);
213
+ }
214
+
215
+ .latex-log-header {
216
+ display: flex;
217
+ justify-content: space-between;
218
+ align-items: center;
219
+ font-weight: 600;
220
+ margin-bottom: 6px;
221
+ color: #94a3b8;
222
+ border-bottom: 1px dashed #334155;
223
+ padding-bottom: 4px;
224
+ }
225
+
226
+ .latex-preview-body {
227
+ flex: 1;
228
+ display: flex;
229
+ flex-direction: column;
230
+ position: relative;
231
+ min-height: 0;
232
+ background: #525659;
233
+ }
234
+
235
+ .latex-scroll {
236
+ flex: 1;
237
+ min-height: 0;
238
+ overflow-y: auto;
239
+ overflow-x: auto;
240
+ padding: 14px 14px 22px;
241
+ background: #525659;
242
+ display: flex;
243
+ flex-direction: column;
244
+ align-items: center;
245
+ gap: 14px;
246
+ scrollbar-width: thin;
247
+ }
248
+
249
+ .latex-scroll::-webkit-scrollbar {
250
+ width: 10px;
251
+ }
252
+
253
+ .latex-scroll::-webkit-scrollbar-thumb {
254
+ background: rgba(255, 255, 255, 0.28);
255
+ border-radius: 6px;
256
+ }
257
+
258
+ .latex-page-wrap {
259
+ width: 100%;
260
+ max-width: 1100px;
261
+ background: #ffffff;
262
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.45);
263
+ flex: 0 0 auto;
264
+ position: relative;
265
+ }
266
+
267
+ .latex-page-img {
268
+ display: block;
269
+ width: 100%;
270
+ height: 100%;
271
+ user-select: none;
272
+ -webkit-user-drag: none;
273
+ }
274
+
275
+ .latex-page-indicator {
276
+ position: absolute;
277
+ bottom: 10px;
278
+ left: 50%;
279
+ transform: translateX(-50%);
280
+ background: rgba(15, 23, 42, 0.85);
281
+ color: #e2e8f0;
282
+ border: 1px solid rgba(148, 163, 184, 0.35);
283
+ border-radius: 999px;
284
+ padding: 3px 12px;
285
+ font-size: 11px;
286
+ pointer-events: none;
287
+ z-index: 2;
288
+ }
289
+
290
+ .latex-empty-state {
291
+ display: flex;
292
+ flex-direction: column;
293
+ align-items: center;
294
+ justify-content: center;
295
+ flex: 1;
296
+ padding: 40px 24px;
297
+ text-align: center;
298
+ background: var(--dsw-alias-bg-base, #181825);
299
+ color: var(--dsw-alias-text-secondary, #a6adc8);
300
+ }
301
+
302
+ .latex-empty-icon {
303
+ width: 56px;
304
+ height: 56px;
305
+ margin-bottom: 16px;
306
+ opacity: 0.85;
307
+ }
308
+
309
+ .latex-empty-title {
310
+ font-size: 16px;
311
+ font-weight: 700;
312
+ color: var(--dsw-alias-text-primary, #cdd6f4);
313
+ margin-bottom: 8px;
314
+ }
315
+
316
+ .latex-empty-desc {
317
+ font-size: 13px;
318
+ line-height: 1.5;
319
+ max-width: 380px;
320
+ margin-bottom: 20px;
321
+ }
322
+
323
+ .latex-empty-code {
324
+ background: rgba(99, 102, 241, 0.12);
325
+ color: #818cf8;
326
+ padding: 2px 5px;
327
+ border-radius: 4px;
328
+ font-family: monospace;
329
+ }
330
+
331
+ .latex-footer-bar {
332
+ display: flex;
333
+ justify-content: space-between;
334
+ align-items: center;
335
+ padding: 4px 12px;
336
+ background: var(--dsw-alias-bg-elevated, #1e1e2e);
337
+ border-top: 1px solid var(--dsw-alias-border-l3, rgba(255, 255, 255, 0.08));
338
+ font-size: 10.5px;
339
+ color: var(--dsw-alias-text-tertiary, #6c7086);
340
+ flex-shrink: 0;
341
+ }
342
+
343
+ .latex-spinner {
344
+ display: inline-block;
345
+ width: 12px;
346
+ height: 12px;
347
+ border: 2px solid rgba(255, 255, 255, 0.3);
348
+ border-radius: 50%;
349
+ border-top-color: #ffffff;
350
+ animation: spin 0.8s linear infinite;
351
+ }
352
+
353
+ @keyframes spin {
354
+ to { transform: rotate(360deg); }
355
+ }
356
+
357
+ /* ==========================================================
358
+ 2. PEMBERSIHAN FITUR NON-ESSENTIAL
359
+ (Hanya sisakan plugin, workspace, chat, session; sisanya hapus)
360
+ ========================================================== */
361
+ /* Sembunyikan kontrol Plan di Chat Composer */
362
+ button[aria-label*="plan" i],
363
+ button[aria-label*="rencana" i],
364
+ [data-slot-id*="conversation.input.plan"],
365
+ [class*="planButton"] {
366
+ display: none !important;
367
+ }
368
+
369
+ /* Sembunyikan kontrol Goal di Chat Composer & Header */
370
+ button[aria-label*="goal" i],
371
+ button[aria-label*="target" i],
372
+ [data-slot-id*="goal"],
373
+ [class*="goalBadge"],
374
+ [class*="goalButton"] {
375
+ display: none !important;
376
+ }
377
+
378
+ /* Sembunyikan kontrol Skill di Chat Composer & Sidebar */
379
+ button[aria-label*="skill" i],
380
+ button[aria-label*="keahlian" i],
381
+ [data-slot-id*="skill"],
382
+ [data-panel-id*="skill"],
383
+ [class*="skillButton"] {
384
+ display: none !important;
385
+ }
386
+
387
+ /* Sembunyikan kontrol Schedule / Reminder */
388
+ button[aria-label*="schedule" i],
389
+ button[aria-label*="jadwal" i],
390
+ button[aria-label*="pengingat" i],
391
+ [data-slot-id*="schedule"],
392
+ [data-panel-id*="schedule"] {
393
+ display: none !important;
394
+ }
395
+
396
+ /* Sembunyikan Workflow / Terminal / Browser tambahan */
397
+ button[aria-label*="workflow" i],
398
+ [data-slot-id*="workflow"],
399
+ [data-panel-id*="workflow"],
400
+ [data-panel-id*="browser"],
401
+ [data-panel-id*="terminal"] {
402
+ display: none !important;
403
+ }
404
+
405
+ /* Workspace/proyek tetap normal: pemilih folder dan tombol tambah
406
+ workspace TIDAK disembunyikan, agar alur tambah workspace berfungsi. */
407
+
408
+ /* Dialog pemilih folder tidak dipakai: tambah workspace langsung
409
+ membuat folder baru tanpa popup. */
410
+ [data-slot-id="sidebar.workspaces.directoryFlow"] {
411
+ display: none !important;
412
+ }
413
+
414
+ /* File Explorer dihilangkan (diganti panel LaTeX di kolom kanan). */
415
+ button[aria-label*="file explorer" i],
416
+ button[title*="file explorer" i],
417
+ button[aria-label*="penjelajah berkas" i],
418
+ button[aria-label*="explorer" i],
419
+ [data-tab-kind="files"] {
420
+ display: none !important;
421
+ }
422
+
423
+ /* Menu/pengaturan Plugin disembunyikan (mode editor LaTeX). */
424
+ [data-slot-id*="plugins."],
425
+ [data-slot-id*="plugin-manager"],
426
+ button[aria-label*="plugin" i],
427
+ a[href*="plugin" i] {
428
+ display: none !important;
429
+ }
430
+
431
+ /* Tombol "Open in App" (File Explorer bawaan, VS Code, Git Bash) di header sesi */
432
+ [data-slot-id*="open-in-app"],
433
+ button[aria-label*="open in" i],
434
+ button[title*="open in" i],
435
+ button[aria-label*="buka di" i],
436
+ button[title*="buka di" i],
437
+ [class*="openInApp"],
438
+ [class*="open-in-app"] {
439
+ display: none !important;
440
+ }
441
+
442
+ /* Peringatan login / modal autentikasi DeepSeek & launcher akun */
443
+ [data-slot-id*="desktop-onboarding"],
444
+ [data-slot-id*="sign-in"],
445
+ [data-slot-id*="settings.launcher"],
446
+ [data-slot-id*="settings.account"],
447
+ [data-slot-id*="quota-notice"],
448
+ [class*="onboardingModal"],
449
+ [class*="accountLauncher"],
450
+ [class*="signInPrompt"],
451
+ [class*="loginNotice"] {
452
+ display: none !important;
453
+ }
454
+
455
+ /* Pastikan track rightbar memiliki lebar yang pas untuk editor 50/50 */
456
+ [data-rightbar-col] {
457
+ min-width: 320px;
458
+ }
459
+ `;
460
+ document.head.appendChild(style);
461
+ }
462
+
463
+ const SCROLL_STORE_KEY = 'dsh-latex-preview-scroll';
464
+
465
+ function readStoredRatio() {
466
+ try {
467
+ const value = Number.parseFloat(window.sessionStorage.getItem(SCROLL_STORE_KEY) || '0');
468
+ if (!Number.isFinite(value)) return 0;
469
+ return Math.min(1, Math.max(0, value));
470
+ } catch (e) {
471
+ return 0;
472
+ }
473
+ }
474
+
475
+ function storeRatio(ratio) {
476
+ try {
477
+ window.sessionStorage.setItem(SCROLL_STORE_KEY, String(ratio));
478
+ } catch (e) {
479
+ // ignore storage restrictions
480
+ }
481
+ }
482
+
483
+ // Workspace aktif = workspace milik sesi yang sedang ditampilkan di kolom utama.
484
+ function useActiveWorkspacePath(props) {
485
+ const useSessions = props && props.useSessions;
486
+ if (typeof useSessions !== 'function') return undefined;
487
+ try {
488
+ return useSessions((sessions) => {
489
+ const byId = sessions && sessions.byId;
490
+ if (!byId) return undefined;
491
+ for (const session of Object.values(byId)) {
492
+ if (!session || !session.cwd) continue;
493
+ if ((session.retainedBy && (session.retainedBy.mainView ?? 0)) > 0) return session.cwd;
494
+ }
495
+ return undefined;
496
+ });
497
+ } catch (error) {
498
+ return undefined;
499
+ }
500
+ }
501
+
502
+ function LatexPdfPreviewPane(props) {
503
+ const [compiling, setCompiling] = React.useState(false);
504
+ const [engine, setEngine] = React.useState('xelatex');
505
+ const [status, setStatus] = React.useState(null);
506
+ const [showLog, setShowLog] = React.useState(false);
507
+ const [lastError, setLastError] = React.useState(null);
508
+ const [currentPage, setCurrentPage] = React.useState(1);
509
+
510
+ const scrollRef = React.useRef(null);
511
+ const ratioRef = React.useRef(readStoredRatio());
512
+ const activeWorkspace = useActiveWorkspacePath(props);
513
+
514
+ const withWorkspace = React.useCallback(
515
+ (path, separator = '?') => (activeWorkspace ? `${path}${separator}ws=${encodeURIComponent(activeWorkspace)}` : path),
516
+ [activeWorkspace]
517
+ );
518
+
519
+ const fetchStatus = React.useCallback(async () => {
520
+ try {
521
+ const res = await fetch(withWorkspace('/api/latex/status'));
522
+ if (res.ok) {
523
+ const data = await res.json();
524
+ setStatus(data);
525
+ if (data.lastSuccess === false && data.lastLog) {
526
+ setLastError(data.lastLog);
527
+ }
528
+ }
529
+ } catch (e) {
530
+ // ignore network glitch
531
+ }
532
+ }, [withWorkspace]);
533
+
534
+ const doCompile = React.useCallback(
535
+ async (chosenEngine = engine) => {
536
+ setCompiling(true);
537
+ setLastError(null);
538
+ try {
539
+ const res = await fetch('/api/latex/compile', {
540
+ method: 'POST',
541
+ headers: { 'Content-Type': 'application/json' },
542
+ body: JSON.stringify({ engine: chosenEngine, ws: activeWorkspace }),
543
+ });
544
+ const data = await res.json();
545
+ if (data.ok) {
546
+ setLastError(null);
547
+ } else {
548
+ setLastError(data.log || 'Kompilasi gagal. Periksa sintaksis main.tex.');
549
+ setShowLog(true);
550
+ }
551
+ } catch (err) {
552
+ setLastError(`Kompilasi gagal: ${err.message}`);
553
+ setShowLog(true);
554
+ } finally {
555
+ setCompiling(false);
556
+ fetchStatus();
557
+ }
558
+ },
559
+ [engine, fetchStatus, activeWorkspace]
560
+ );
561
+
562
+ const createStarterTemplate = React.useCallback(async () => {
563
+ try {
564
+ await fetch(withWorkspace('/api/latex/init'), {
565
+ method: 'POST',
566
+ headers: { 'Content-Type': 'application/json' },
567
+ body: JSON.stringify({ ws: activeWorkspace }),
568
+ });
569
+ await doCompile(engine);
570
+ } catch (err) {
571
+ alert(`Gagal membuat template: ${err.message}`);
572
+ }
573
+ }, [doCompile, engine, withWorkspace, activeWorkspace]);
574
+
575
+ // Polling berkala untuk mendeteksi perubahan main.tex
576
+ React.useEffect(() => {
577
+ fetchStatus();
578
+ const timer = setInterval(fetchStatus, 3000);
579
+ return () => clearInterval(timer);
580
+ }, [fetchStatus]);
581
+
582
+ const pageCount = status?.pageCount || 0;
583
+ const pdfMtime = Math.round(status?.pdfMtime || 0);
584
+ const pageAspect = `${status?.widthPt || 595.28} / ${status?.heightPt || 841.89}`;
585
+ const hasPdf = Boolean(status?.mainPdfExists && pageCount > 0);
586
+ const workspaceLabel = (() => {
587
+ const full = status?.workspace || activeWorkspace || '';
588
+ if (!full) return '';
589
+ const parts = String(full).split(/[\\/]/).filter(Boolean);
590
+ return parts.length > 0 ? parts[parts.length - 1] : '';
591
+ })();
592
+
593
+ const handleScroll = React.useCallback(() => {
594
+ const el = scrollRef.current;
595
+ if (!el) return;
596
+
597
+ const max = el.scrollHeight - el.clientHeight;
598
+ const ratio = max > 0 ? el.scrollTop / max : 0;
599
+ ratioRef.current = ratio;
600
+ storeRatio(ratio);
601
+
602
+ const wraps = el.querySelectorAll('.latex-page-wrap');
603
+ if (wraps.length > 0) {
604
+ const containerTop = el.getBoundingClientRect().top;
605
+ let visible = 1;
606
+ for (let i = 0; i < wraps.length; i += 1) {
607
+ const rect = wraps[i].getBoundingClientRect();
608
+ if (rect.top - containerTop <= 4) visible = i + 1;
609
+ }
610
+ setCurrentPage((prev) => (prev === visible ? prev : visible));
611
+ }
612
+ }, []);
613
+
614
+ // Pertahankan posisi baca: pulihkan rasio scroll setiap dokumen berganti
615
+ // (setelah compile, setelah refresh manual, maupun saat panel dipasang ulang).
616
+ React.useLayoutEffect(() => {
617
+ const el = scrollRef.current;
618
+ if (!el || !hasPdf) return;
619
+
620
+ const applyRatio = () => {
621
+ const max = el.scrollHeight - el.clientHeight;
622
+ if (max <= 0) return false;
623
+ el.scrollTop = Math.round(ratioRef.current * max);
624
+ return true;
625
+ };
626
+
627
+ applyRatio();
628
+
629
+ // Ulangi sekali setelah gambar selesai dimuat, jaga-jaga bila tinggi
630
+ // halaman belum final saat efek pertama berjalan.
631
+ const raf = window.requestAnimationFrame(applyRatio);
632
+ return () => window.cancelAnimationFrame(raf);
633
+ }, [hasPdf, pageCount, pdfMtime, activeWorkspace]);
634
+
635
+ return h('div', { className: 'latex-editor-container' }, [
636
+ // 1. Header Toolbar (Hanya compile, log, dan refresh)
637
+ h('div', { className: 'latex-editor-header', key: 'header' }, [
638
+ // Compile Button
639
+ h(
640
+ 'button',
641
+ {
642
+ className: 'latex-btn-compile',
643
+ onClick: () => doCompile(engine),
644
+ disabled: compiling,
645
+ title: 'Kompilasi berkas main.tex menjadi PDF',
646
+ key: 'btn-compile',
647
+ },
648
+ compiling
649
+ ? [h('span', { className: 'latex-spinner', key: 'spin' }), ' Mengompilasi...']
650
+ : ['▶ Compile']
651
+ ),
652
+
653
+ // Log Button
654
+ h(
655
+ 'button',
656
+ {
657
+ className: 'latex-btn-icon',
658
+ onClick: () => setShowLog((prev) => !prev),
659
+ title: 'Tampilkan/Sembunyikan Log Kompilasi',
660
+ key: 'btn-log',
661
+ },
662
+ showLog ? '✕ Tutup Log' : '📋 Log'
663
+ ),
664
+
665
+ // Refresh Button (status saja; posisi baca tidak di-reset)
666
+ h(
667
+ 'button',
668
+ {
669
+ className: 'latex-btn-icon',
670
+ onClick: () => fetchStatus(),
671
+ title: 'Muat ulang pratinjau tanpa mereset posisi halaman',
672
+ key: 'btn-refresh',
673
+ },
674
+ '🔄 Refresh'
675
+ ),
676
+ ]),
677
+
678
+ // 2. Log Drawer (jika terbuka atau kompilasi gagal)
679
+ showLog || (lastError && compiling === false)
680
+ ? h('div', { className: 'latex-log-drawer', key: 'log-drawer' }, [
681
+ h('div', { className: 'latex-log-header' }, [
682
+ h('span', null, '📋 Log Kompilasi LaTeX (Terakhir)'),
683
+ h(
684
+ 'button',
685
+ {
686
+ className: 'latex-btn-icon',
687
+ style: { padding: '2px 6px', fontSize: '10px' },
688
+ onClick: () => setShowLog(false),
689
+ },
690
+ 'Tutup ✕'
691
+ ),
692
+ ]),
693
+ h('div', null, lastError || status?.lastLog || 'Belum ada log kompilasi.'),
694
+ ])
695
+ : null,
696
+
697
+ // 3. Area Pratinjau PDF
698
+ h(
699
+ 'div',
700
+ { className: 'latex-preview-body', key: 'preview-body' },
701
+ hasPdf
702
+ ? [
703
+ h(
704
+ 'div',
705
+ {
706
+ className: 'latex-scroll',
707
+ key: 'scroll',
708
+ ref: scrollRef,
709
+ onScroll: handleScroll,
710
+ },
711
+ Array.from({ length: pageCount }, (_, index) =>
712
+ h(
713
+ 'div',
714
+ {
715
+ className: 'latex-page-wrap',
716
+ key: `page-${activeWorkspace || 'default'}-${pdfMtime}-${index + 1}`,
717
+ style: { aspectRatio: pageAspect },
718
+ },
719
+ h('img', {
720
+ className: 'latex-page-img',
721
+ src: withWorkspace(`/api/latex/page?n=${index + 1}&t=${pdfMtime}`, '&'),
722
+ alt: `Halaman ${index + 1}`,
723
+ loading: index < 3 ? 'eager' : 'lazy',
724
+ draggable: false,
725
+ })
726
+ )
727
+ )
728
+ ),
729
+ h(
730
+ 'div',
731
+ { className: 'latex-page-indicator', key: 'page-indicator' },
732
+ `Hal ${Math.min(currentPage, pageCount)} / ${pageCount}`
733
+ ),
734
+ ]
735
+ : h('div', { className: 'latex-empty-state' }, [
736
+ h(
737
+ 'svg',
738
+ {
739
+ className: 'latex-empty-icon',
740
+ viewBox: '0 0 24 24',
741
+ fill: 'none',
742
+ stroke: '#6366f1',
743
+ strokeWidth: '1.5',
744
+ strokeLinecap: 'round',
745
+ strokeLinejoin: 'round',
746
+ },
747
+ [
748
+ h('path', { d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z', key: 'e1' }),
749
+ h('polyline', { points: '14 2 14 8 20 8', key: 'e2' }),
750
+ h('circle', { cx: '12', cy: '14', r: '3', key: 'e3' }),
751
+ ]
752
+ ),
753
+ h('div', { className: 'latex-empty-title' }, 'Belum Ada Hasil Kompilasi main.pdf'),
754
+ h(
755
+ 'div',
756
+ { className: 'latex-empty-desc' },
757
+ h(React.Fragment, null, [
758
+ 'Entry point dokumen adalah ',
759
+ h('span', { className: 'latex-empty-code' }, 'main.tex'),
760
+ '. Minta AI di kolom chat sebelah kiri untuk menulis dokumen, atau klik tombol di bawah untuk membuat template awal.',
761
+ ])
762
+ ),
763
+ h(
764
+ 'button',
765
+ {
766
+ className: 'latex-btn-compile',
767
+ style: { padding: '8px 18px', fontSize: '13px' },
768
+ onClick: createStarterTemplate,
769
+ disabled: compiling,
770
+ },
771
+ compiling ? 'Membuat & Mengompilasi...' : '📄 Buat Template main.tex & Compile'
772
+ ),
773
+ ])
774
+ ),
775
+
776
+ // 4. Footer Bar
777
+ h('div', { className: 'latex-footer-bar', key: 'footer' }, [
778
+ h(
779
+ 'span',
780
+ null,
781
+ status?.lastCompileTime
782
+ ? `Kompilasi terakhir: ${new Date(status.lastCompileTime).toLocaleTimeString()} (${engine})`
783
+ : `Engine: ${engine} • Entry: main.tex`
784
+ ),
785
+ h('span', null, workspaceLabel ? `Proyek: ${workspaceLabel}` : 'DeepSeek Harness LaTeX Editor AI'),
786
+ ]),
787
+ ]);
788
+ }
789
+
790
+ return {
791
+ inject: ['slots', 'layout'],
792
+ apply(ctx) {
793
+ // 1. Suntikkan stylesheet kustom untuk layout split & pembersihan fitur non-essential
794
+ injectStyles();
795
+
796
+ // 2. Pastikan Rightbar selalu terbuka dengan track terpasang (Split Screen)
797
+ if (ctx.layout && typeof ctx.layout.openRightbar === 'function') {
798
+ const ensureSplit = () => {
799
+ try {
800
+ ctx.layout.openRightbar(true, false);
801
+ } catch (e) {}
802
+ };
803
+
804
+ ensureSplit();
805
+
806
+ // Pertahankan split screen aktif saat terjadi perpindahan view atau session
807
+ const interval = setInterval(ensureSplit, 1500);
808
+ ctx.effect(() => () => clearInterval(interval));
809
+ }
810
+
811
+ // 3. Daftarkan panel pratinjau PDF ke dalam slot 'rightbar' dengan prioritas tertinggi (prioritas angka terendah menang)
812
+ if (ctx.slots && typeof ctx.slots.inject === 'function') {
813
+ ctx.slots.inject('root', () =>
814
+ ctx.slots.register(
815
+ {
816
+ name: 'rightbar',
817
+ priority: -100,
818
+ },
819
+ LatexPdfPreviewPane
820
+ )
821
+ );
822
+ }
823
+
824
+ // 4. Tambah workspace: sekali klik langsung membuat workspace + folder,
825
+ // tanpa membuka dialog pemilih folder.
826
+ if (typeof document !== 'undefined') {
827
+ ctx.effect(() => {
828
+ let busy = false;
829
+ const ADD_PATTERN = /(add workspace|tambah ruang kerja|add project|tambah proyek|workspace baru|proyek baru)/i;
830
+
831
+ const onClick = (event) => {
832
+ const target = event.target;
833
+ if (!target || typeof target.closest !== 'function') return;
834
+
835
+ const control = target.closest('button, [role="menuitem"], [role="option"], a, [role="button"]');
836
+ if (!control) return;
837
+ if (control.closest('[data-slot-id="sidebar.workspaces.directoryFlow"]')) return;
838
+
839
+ const label = `${control.getAttribute('aria-label') || ''} ${control.getAttribute('title') || ''} ${control.textContent || ''}`;
840
+ if (!ADD_PATTERN.test(label)) return;
841
+
842
+ event.preventDefault();
843
+ event.stopPropagation();
844
+ event.stopImmediatePropagation();
845
+
846
+ if (busy) return;
847
+ busy = true;
848
+
849
+ fetch('/api/latex/workspace/new', { method: 'POST' })
850
+ .then((response) => response.json())
851
+ .then((data) => {
852
+ if (!data || data.ok !== true) {
853
+ throw new Error((data && data.error) || 'gagal membuat workspace');
854
+ }
855
+ })
856
+ .catch((error) => {
857
+ console.warn('[latex-editor] gagal membuat workspace:', error);
858
+ })
859
+ .finally(() => {
860
+ busy = false;
861
+ });
862
+ };
863
+
864
+ document.addEventListener('click', onClick, true);
865
+ return () => document.removeEventListener('click', onClick, true);
866
+ });
867
+ }
868
+ },
869
+ };
870
+ },
871
+ });