retold-content-system 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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/build-codejar-bundle.js +29 -0
  4. package/build-codemirror-bundle.js +29 -0
  5. package/codejar-entry.js +10 -0
  6. package/codemirror-entry.js +16 -0
  7. package/content/Dogs.txt.md +2 -0
  8. package/content/README.md +35 -0
  9. package/content/_sidebar.md +3 -0
  10. package/content/_topbar.md +1 -0
  11. package/content/cover.md +12 -0
  12. package/content/getting-started.md +73 -0
  13. package/css/content-system.css +42 -0
  14. package/css/github.css +118 -0
  15. package/docs/.nojekyll +0 -0
  16. package/docs/README.md +24 -0
  17. package/docs/_sidebar.md +16 -0
  18. package/docs/_topbar.md +6 -0
  19. package/docs/cli.md +119 -0
  20. package/docs/cover.md +16 -0
  21. package/docs/css/docuserve.css +73 -0
  22. package/docs/editor-guide.md +137 -0
  23. package/docs/getting-started.md +73 -0
  24. package/docs/index.html +39 -0
  25. package/docs/keyboard-shortcuts.md +40 -0
  26. package/docs/retold-catalog.json +81 -0
  27. package/docs/retold-keyword-index.json +19 -0
  28. package/docs/topics.md +83 -0
  29. package/html/codejar-bundle.js +16 -0
  30. package/html/codemirror-bundle.js +29982 -0
  31. package/html/edit.html +25 -0
  32. package/html/index.html +25 -0
  33. package/html/preview.html +19 -0
  34. package/package.json +70 -0
  35. package/server.js +43 -0
  36. package/source/Pict-Application-ContentEditor-Configuration.json +15 -0
  37. package/source/Pict-Application-ContentEditor.js +1361 -0
  38. package/source/Pict-Application-ContentReader-Configuration.json +15 -0
  39. package/source/Pict-Application-ContentReader.js +91 -0
  40. package/source/Pict-ContentSystem-Bundle.js +21 -0
  41. package/source/cli/ContentSystem-CLI-Program.js +15 -0
  42. package/source/cli/ContentSystem-CLI-Run.js +3 -0
  43. package/source/cli/ContentSystem-Server-Setup.js +405 -0
  44. package/source/cli/commands/ContentSystem-Command-Serve.js +104 -0
  45. package/source/providers/Pict-Provider-ContentEditor.js +198 -0
  46. package/source/views/PictView-Editor-CodeEditor.js +271 -0
  47. package/source/views/PictView-Editor-Layout.js +1194 -0
  48. package/source/views/PictView-Editor-MarkdownEditor.js +115 -0
  49. package/source/views/PictView-Editor-MarkdownReference.js +801 -0
  50. package/source/views/PictView-Editor-SettingsPanel.js +563 -0
  51. package/source/views/PictView-Editor-TopBar.js +366 -0
  52. package/source/views/PictView-Editor-Topics.js +1025 -0
@@ -0,0 +1,1194 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ const _ViewConfiguration =
4
+ {
5
+ ViewIdentifier: "ContentEditor-Layout",
6
+
7
+ DefaultRenderable: "ContentEditor-Layout-Shell",
8
+ DefaultDestinationAddress: "#ContentEditor-Application-Container",
9
+
10
+ AutoRender: false,
11
+
12
+ CSS: /*css*/`
13
+ #ContentEditor-Application-Container
14
+ {
15
+ display: flex;
16
+ flex-direction: column;
17
+ height: 100vh;
18
+ background: #F5F3EE;
19
+ }
20
+ #ContentEditor-TopBar-Container
21
+ {
22
+ flex-shrink: 0;
23
+ }
24
+ .content-editor-body
25
+ {
26
+ display: flex;
27
+ flex: 1;
28
+ min-height: 0;
29
+ overflow: hidden;
30
+ }
31
+ /* Sidebar wrapper holds the sidebar content + collapse toggle */
32
+ .content-editor-sidebar-wrap
33
+ {
34
+ display: flex;
35
+ flex-shrink: 0;
36
+ position: relative;
37
+ transition: width 0.2s ease;
38
+ }
39
+ /* Inner wrapper: vertical flex for tab bar + panes */
40
+ .content-editor-sidebar-inner
41
+ {
42
+ display: flex;
43
+ flex-direction: column;
44
+ flex: 1;
45
+ min-width: 0;
46
+ min-height: 0;
47
+ overflow: hidden;
48
+ }
49
+ /* Sidebar tab bar */
50
+ .content-editor-sidebar-tabs
51
+ {
52
+ display: flex;
53
+ flex-shrink: 0;
54
+ border-bottom: 1px solid #DDD6CA;
55
+ background: #F5F0EA;
56
+ }
57
+ .content-editor-sidebar-tab
58
+ {
59
+ flex: 1;
60
+ padding: 7px 0;
61
+ border: none;
62
+ background: transparent;
63
+ font-size: 0.78rem;
64
+ font-weight: 600;
65
+ color: #8A7F72;
66
+ cursor: pointer;
67
+ border-bottom: 2px solid transparent;
68
+ transition: color 0.15s, border-color 0.15s;
69
+ }
70
+ .content-editor-sidebar-tab:hover
71
+ {
72
+ color: #3D3229;
73
+ }
74
+ .content-editor-sidebar-tab.active
75
+ {
76
+ color: #2E7D74;
77
+ border-bottom-color: #2E7D74;
78
+ }
79
+ .content-editor-sidebar-addfile
80
+ {
81
+ flex-shrink: 0;
82
+ width: 30px;
83
+ border: none;
84
+ background: transparent;
85
+ font-size: 1.1rem;
86
+ font-weight: 400;
87
+ color: #8A7F72;
88
+ cursor: pointer;
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ border-bottom: 2px solid transparent;
93
+ transition: color 0.15s, background 0.15s;
94
+ }
95
+ .content-editor-sidebar-addfile:hover
96
+ {
97
+ color: #2E7D74;
98
+ background: #EDE9E3;
99
+ }
100
+ /* Sidebar panes */
101
+ .content-editor-sidebar-pane
102
+ {
103
+ flex: 1;
104
+ overflow-y: auto;
105
+ overflow-x: hidden;
106
+ min-width: 0;
107
+ min-height: 0;
108
+ }
109
+ #ContentEditor-Sidebar-Container
110
+ {
111
+ background: #FAF8F4;
112
+ }
113
+ /* Collapsed state */
114
+ .content-editor-sidebar-wrap.collapsed
115
+ {
116
+ width: 0 !important;
117
+ }
118
+ .content-editor-sidebar-wrap.collapsed .content-editor-sidebar-inner
119
+ {
120
+ visibility: hidden;
121
+ }
122
+ .content-editor-sidebar-wrap.collapsed .content-editor-resize-handle
123
+ {
124
+ display: none;
125
+ }
126
+ /* Collapse / expand toggle */
127
+ .content-editor-sidebar-toggle
128
+ {
129
+ position: absolute;
130
+ top: 8px;
131
+ right: -20px;
132
+ width: 20px;
133
+ height: 28px;
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: center;
137
+ background: #FAF8F4;
138
+ border: 1px solid #DDD6CA;
139
+ border-left: none;
140
+ border-radius: 0 4px 4px 0;
141
+ cursor: pointer;
142
+ z-index: 10;
143
+ color: #8A7F72;
144
+ font-size: 11px;
145
+ line-height: 1;
146
+ transition: color 0.15s;
147
+ }
148
+ .content-editor-sidebar-toggle:hover
149
+ {
150
+ color: #3D3229;
151
+ }
152
+ .content-editor-sidebar-wrap.collapsed .content-editor-sidebar-toggle
153
+ {
154
+ right: -20px;
155
+ }
156
+ /* Resize handle */
157
+ .content-editor-resize-handle
158
+ {
159
+ flex-shrink: 0;
160
+ width: 5px;
161
+ cursor: col-resize;
162
+ background: transparent;
163
+ border-right: 1px solid #DDD6CA;
164
+ transition: background 0.15s;
165
+ }
166
+ .content-editor-resize-handle:hover,
167
+ .content-editor-resize-handle.dragging
168
+ {
169
+ background: #2E7D74;
170
+ border-right-color: #2E7D74;
171
+ }
172
+ /* File browser layout overrides for sidebar use */
173
+ #ContentEditor-Sidebar-Container .pict-filebrowser
174
+ {
175
+ border: none;
176
+ border-radius: 0;
177
+ background: transparent;
178
+ }
179
+ #ContentEditor-Sidebar-Container .pict-filebrowser-browse-pane
180
+ {
181
+ display: none;
182
+ }
183
+ #ContentEditor-Sidebar-Container .pict-filebrowser-view-pane
184
+ {
185
+ display: none;
186
+ }
187
+ /* Hide size/date columns — the sidebar is too narrow for them */
188
+ #ContentEditor-Sidebar-Container .pict-fb-detail-col-size,
189
+ #ContentEditor-Sidebar-Container .pict-fb-detail-col-modified,
190
+ #ContentEditor-Sidebar-Container .pict-fb-detail-size,
191
+ #ContentEditor-Sidebar-Container .pict-fb-detail-modified
192
+ {
193
+ display: none;
194
+ }
195
+ /* Hide the column header bar in sidebar mode */
196
+ #ContentEditor-Sidebar-Container .pict-fb-detail-header
197
+ {
198
+ display: none;
199
+ }
200
+ #ContentEditor-Editor-Container
201
+ {
202
+ flex: 1;
203
+ overflow-y: auto;
204
+ padding: 44px 16px 16px 16px;
205
+ }
206
+ /* Code editor: fill the container and remove outer border */
207
+ #ContentEditor-Editor-Container .pict-code-editor-wrap
208
+ {
209
+ height: calc(100% - 4px);
210
+ border: none;
211
+ border-radius: 0;
212
+ }
213
+ #ContentEditor-Editor-Container .pict-code-editor
214
+ {
215
+ min-height: unset;
216
+ height: 100%;
217
+ background: #FAFAFA;
218
+ }
219
+ /* Binary file preview */
220
+ .binary-preview-image-wrap
221
+ {
222
+ margin-bottom: 20px;
223
+ }
224
+ .binary-preview-image
225
+ {
226
+ display: inline-block;
227
+ background: #FFF;
228
+ border: 1px solid #DDD6CA;
229
+ border-radius: 6px;
230
+ padding: 24px;
231
+ }
232
+ .binary-preview-image img
233
+ {
234
+ display: block;
235
+ max-width: 100%;
236
+ max-height: 400px;
237
+ object-fit: contain;
238
+ border-radius: 4px;
239
+ }
240
+ .binary-preview-card
241
+ {
242
+ display: flex;
243
+ align-items: center;
244
+ gap: 20px;
245
+ background: #FFF;
246
+ border: 1px solid #DDD6CA;
247
+ border-radius: 6px;
248
+ padding: 24px;
249
+ max-width: 600px;
250
+ }
251
+ .binary-preview-icon
252
+ {
253
+ flex-shrink: 0;
254
+ width: 64px;
255
+ height: 64px;
256
+ display: flex;
257
+ align-items: center;
258
+ justify-content: center;
259
+ background: #F0EDE8;
260
+ border-radius: 8px;
261
+ font-size: 0.75rem;
262
+ font-weight: 700;
263
+ color: #5E5549;
264
+ letter-spacing: 0.5px;
265
+ }
266
+ .binary-preview-info
267
+ {
268
+ flex: 1;
269
+ min-width: 0;
270
+ }
271
+ .binary-preview-name
272
+ {
273
+ font-size: 1rem;
274
+ font-weight: 600;
275
+ color: #3D3229;
276
+ margin-bottom: 6px;
277
+ word-break: break-all;
278
+ }
279
+ .binary-preview-meta
280
+ {
281
+ font-size: 0.8rem;
282
+ color: #8A7F72;
283
+ line-height: 1.6;
284
+ }
285
+ .binary-preview-actions
286
+ {
287
+ flex-shrink: 0;
288
+ display: flex;
289
+ flex-direction: column;
290
+ gap: 8px;
291
+ }
292
+ .binary-preview-btn
293
+ {
294
+ display: inline-block;
295
+ padding: 8px 16px;
296
+ border-radius: 4px;
297
+ font-size: 0.8rem;
298
+ font-weight: 600;
299
+ text-decoration: none;
300
+ text-align: center;
301
+ cursor: pointer;
302
+ background: #2E7D74;
303
+ color: #FFF;
304
+ }
305
+ .binary-preview-btn:hover
306
+ {
307
+ background: #3A9E92;
308
+ }
309
+ .binary-preview-btn-secondary
310
+ {
311
+ background: transparent;
312
+ color: #5E5549;
313
+ border: 1px solid #DDD6CA;
314
+ }
315
+ .binary-preview-btn-secondary:hover
316
+ {
317
+ border-color: #8A7F72;
318
+ color: #3D3229;
319
+ }
320
+ .binary-preview-btn-preview
321
+ {
322
+ padding: 10px 20px;
323
+ font-size: 0.85rem;
324
+ border: 1px solid #DDD6CA;
325
+ background: #FAF8F4;
326
+ color: #3D3229;
327
+ cursor: pointer;
328
+ border-radius: 6px;
329
+ transition: background 0.15s, border-color 0.15s;
330
+ }
331
+ .binary-preview-btn-preview:hover
332
+ {
333
+ background: #F0EDE8;
334
+ border-color: #8A7F72;
335
+ }
336
+ #ContentEditor-MediaPreviewPlaceholder
337
+ {
338
+ margin-bottom: 20px;
339
+ }
340
+ .binary-preview-media-wrap
341
+ {
342
+ margin-bottom: 20px;
343
+ }
344
+ .binary-preview-video
345
+ {
346
+ display: block;
347
+ max-width: 100%;
348
+ max-height: 500px;
349
+ border-radius: 6px;
350
+ border: 1px solid #DDD6CA;
351
+ background: #000;
352
+ }
353
+ .binary-preview-audio
354
+ {
355
+ display: block;
356
+ width: 100%;
357
+ max-width: 500px;
358
+ }
359
+ /* Image upload overlay */
360
+ .content-editor-upload-overlay
361
+ {
362
+ display: none;
363
+ position: fixed;
364
+ top: 0;
365
+ left: 0;
366
+ right: 0;
367
+ bottom: 0;
368
+ z-index: 1099;
369
+ background: rgba(0, 0, 0, 0.35);
370
+ }
371
+ .content-editor-upload-overlay.open
372
+ {
373
+ display: flex;
374
+ align-items: center;
375
+ justify-content: center;
376
+ }
377
+ .content-editor-upload-panel
378
+ {
379
+ background: #FFF;
380
+ border: 1px solid #DDD6CA;
381
+ border-radius: 10px;
382
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
383
+ width: 420px;
384
+ max-width: 90vw;
385
+ overflow: hidden;
386
+ }
387
+ .content-editor-upload-header
388
+ {
389
+ display: flex;
390
+ align-items: center;
391
+ justify-content: space-between;
392
+ padding: 14px 18px;
393
+ border-bottom: 1px solid #EDE9E3;
394
+ }
395
+ .content-editor-upload-title
396
+ {
397
+ font-size: 0.95rem;
398
+ font-weight: 600;
399
+ color: #3D3229;
400
+ }
401
+ .content-editor-upload-close
402
+ {
403
+ background: transparent;
404
+ border: none;
405
+ font-size: 1.2rem;
406
+ color: #8A7F72;
407
+ cursor: pointer;
408
+ padding: 2px 6px;
409
+ line-height: 1;
410
+ border-radius: 4px;
411
+ }
412
+ .content-editor-upload-close:hover
413
+ {
414
+ color: #3D3229;
415
+ background: #F0EDE8;
416
+ }
417
+ .content-editor-upload-body
418
+ {
419
+ padding: 18px;
420
+ }
421
+ .content-editor-upload-dropzone
422
+ {
423
+ border: 2px dashed #DDD6CA;
424
+ border-radius: 8px;
425
+ padding: 28px 16px;
426
+ text-align: center;
427
+ cursor: pointer;
428
+ transition: border-color 0.15s, background 0.15s;
429
+ background: #FAF8F4;
430
+ }
431
+ .content-editor-upload-dropzone:hover,
432
+ .content-editor-upload-dropzone.dragover
433
+ {
434
+ border-color: #2E7D74;
435
+ background: #F0FAF8;
436
+ }
437
+ .content-editor-upload-dropzone-icon
438
+ {
439
+ font-size: 2rem;
440
+ color: #8A7F72;
441
+ margin-bottom: 6px;
442
+ }
443
+ .content-editor-upload-dropzone-text
444
+ {
445
+ font-size: 0.82rem;
446
+ color: #5E5549;
447
+ }
448
+ .content-editor-upload-dropzone-hint
449
+ {
450
+ font-size: 0.72rem;
451
+ color: #8A7F72;
452
+ margin-top: 4px;
453
+ }
454
+ .content-editor-upload-file-input
455
+ {
456
+ display: none;
457
+ }
458
+ .content-editor-upload-status
459
+ {
460
+ margin-top: 12px;
461
+ font-size: 0.82rem;
462
+ color: #5E5549;
463
+ min-height: 20px;
464
+ }
465
+ .content-editor-upload-status-error
466
+ {
467
+ color: #D9534F;
468
+ }
469
+ .content-editor-upload-status-success
470
+ {
471
+ color: #2E7D74;
472
+ }
473
+ .content-editor-upload-result
474
+ {
475
+ margin-top: 12px;
476
+ padding: 10px 12px;
477
+ background: #F0EDE8;
478
+ border: 1px solid #DDD6CA;
479
+ border-radius: 6px;
480
+ }
481
+ .content-editor-upload-result-label
482
+ {
483
+ font-size: 0.72rem;
484
+ color: #8A7F72;
485
+ margin-bottom: 4px;
486
+ }
487
+ .content-editor-upload-result-url
488
+ {
489
+ display: flex;
490
+ align-items: center;
491
+ gap: 6px;
492
+ }
493
+ .content-editor-upload-result-text
494
+ {
495
+ flex: 1;
496
+ font-family: monospace;
497
+ font-size: 0.78rem;
498
+ color: #3D3229;
499
+ word-break: break-all;
500
+ }
501
+ .content-editor-upload-result-copy
502
+ {
503
+ flex-shrink: 0;
504
+ background: #2E7D74;
505
+ color: #FFF;
506
+ border: none;
507
+ border-radius: 4px;
508
+ padding: 4px 10px;
509
+ font-size: 0.72rem;
510
+ font-weight: 600;
511
+ cursor: pointer;
512
+ }
513
+ .content-editor-upload-result-copy:hover
514
+ {
515
+ background: #3A9E92;
516
+ }
517
+ .content-editor-upload-kbd
518
+ {
519
+ display: inline-block;
520
+ padding: 1px 5px;
521
+ font-size: 0.68rem;
522
+ font-family: monospace;
523
+ background: #F0EDE8;
524
+ border: 1px solid #DDD6CA;
525
+ border-radius: 3px;
526
+ color: #5E5549;
527
+ }
528
+ .content-editor-upload-footer
529
+ {
530
+ padding: 10px 18px;
531
+ border-top: 1px solid #EDE9E3;
532
+ font-size: 0.72rem;
533
+ color: #8A7F72;
534
+ text-align: center;
535
+ }
536
+ `,
537
+
538
+ Templates:
539
+ [
540
+ {
541
+ Hash: "ContentEditor-Layout-Shell-Template",
542
+ Template: /*html*/`
543
+ <div id="ContentEditor-TopBar-Container"></div>
544
+ <div class="content-editor-body">
545
+ <div class="content-editor-sidebar-wrap" id="ContentEditor-SidebarWrap" style="width:250px">
546
+ <div class="content-editor-sidebar-inner">
547
+ <div class="content-editor-sidebar-tabs">
548
+ <button class="content-editor-sidebar-tab active" id="ContentEditor-SidebarTab-Files"
549
+ onclick="{~P~}.views['ContentEditor-Layout'].switchSidebarTab('files')">Files</button>
550
+ <button class="content-editor-sidebar-tab" id="ContentEditor-SidebarTab-Reference"
551
+ onclick="{~P~}.views['ContentEditor-Layout'].switchSidebarTab('reference')">Reference</button>
552
+ <button class="content-editor-sidebar-tab" id="ContentEditor-SidebarTab-Topics"
553
+ onclick="{~P~}.views['ContentEditor-Layout'].switchSidebarTab('topics')">Topics</button>
554
+ <button class="content-editor-sidebar-addfile" title="New file"
555
+ onclick="{~P~}.PictApplication.promptNewFile()">+</button>
556
+ </div>
557
+ <div id="ContentEditor-Sidebar-Container" class="content-editor-sidebar-pane"></div>
558
+ <div id="ContentEditor-SidebarReference-Container" class="content-editor-sidebar-pane" style="display:none"></div>
559
+ <div id="ContentEditor-SidebarTopics-Container" class="content-editor-sidebar-pane" style="display:none"></div>
560
+ </div>
561
+ <div class="content-editor-resize-handle" id="ContentEditor-ResizeHandle"></div>
562
+ <div class="content-editor-sidebar-toggle" id="ContentEditor-SidebarToggle">&#x25C0;</div>
563
+ </div>
564
+ <div id="ContentEditor-Editor-Container"></div>
565
+ </div>
566
+ <div class="content-editor-upload-overlay" id="ContentEditor-UploadOverlay"
567
+ onclick="{~P~}.views['ContentEditor-Layout'].onUploadOverlayClick(event)">
568
+ <div class="content-editor-upload-panel">
569
+ <div class="content-editor-upload-header">
570
+ <span class="content-editor-upload-title">Upload Image</span>
571
+ <button class="content-editor-upload-close"
572
+ onclick="{~P~}.views['ContentEditor-Layout'].toggleUploadForm()">&times;</button>
573
+ </div>
574
+ <div class="content-editor-upload-body">
575
+ <div class="content-editor-upload-dropzone" id="ContentEditor-UploadDropzone"
576
+ onclick="document.getElementById('ContentEditor-UploadFileInput').click()">
577
+ <div class="content-editor-upload-dropzone-icon">&#x1F4F7;</div>
578
+ <div class="content-editor-upload-dropzone-text">Drop an image here or click to browse</div>
579
+ <div class="content-editor-upload-dropzone-hint">PNG, JPG, GIF, WebP, SVG, BMP</div>
580
+ </div>
581
+ <input type="file" class="content-editor-upload-file-input" id="ContentEditor-UploadFileInput"
582
+ accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml,image/bmp"
583
+ onchange="{~P~}.views['ContentEditor-Layout'].onUploadFileSelected(this)">
584
+ <div class="content-editor-upload-status" id="ContentEditor-UploadStatus"></div>
585
+ <div id="ContentEditor-UploadResult"></div>
586
+ </div>
587
+ <div class="content-editor-upload-footer">
588
+ <span class="content-editor-upload-kbd">F3</span> or
589
+ <span class="content-editor-upload-kbd">Ctrl+Shift+U</span> to toggle
590
+ </div>
591
+ </div>
592
+ </div>
593
+ `
594
+ }
595
+ ],
596
+
597
+ Renderables:
598
+ [
599
+ {
600
+ RenderableHash: "ContentEditor-Layout-Shell",
601
+ TemplateHash: "ContentEditor-Layout-Shell-Template",
602
+ DestinationAddress: "#ContentEditor-Application-Container",
603
+ RenderMethod: "replace"
604
+ }
605
+ ]
606
+ };
607
+
608
+ class ContentEditorLayoutView extends libPictView
609
+ {
610
+ constructor(pFable, pOptions, pServiceHash)
611
+ {
612
+ super(pFable, pOptions, pServiceHash);
613
+
614
+ this._minSidebarWidth = 140;
615
+ this._maxSidebarWidth = 600;
616
+ }
617
+
618
+ onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent)
619
+ {
620
+ // Render child views
621
+ this.pict.views['ContentEditor-TopBar'].render();
622
+
623
+ // Show welcome message in editor area if no file loaded
624
+ let tmpEditorContainer = this.pict.ContentAssignment.getElement('#ContentEditor-Editor-Container');
625
+ if (tmpEditorContainer && tmpEditorContainer[0] && !this.pict.AppData.ContentEditor.CurrentFile)
626
+ {
627
+ tmpEditorContainer[0].innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#8A7F72;font-size:1.1em;">Select a file from the sidebar to begin editing</div>';
628
+ }
629
+
630
+ // Inject CSS
631
+ this.pict.CSSMap.injectCSS();
632
+
633
+ // Apply persisted sidebar state
634
+ let tmpSettings = this.pict.AppData.ContentEditor;
635
+ let tmpWrap = document.getElementById('ContentEditor-SidebarWrap');
636
+ let tmpToggle = document.getElementById('ContentEditor-SidebarToggle');
637
+ if (tmpWrap)
638
+ {
639
+ tmpWrap.style.width = tmpSettings.SidebarWidth + 'px';
640
+ if (tmpSettings.SidebarCollapsed)
641
+ {
642
+ tmpWrap.classList.add('collapsed');
643
+ if (tmpToggle) tmpToggle.innerHTML = '&#x25B6;';
644
+ }
645
+ }
646
+
647
+ // Wire up sidebar toggle
648
+ let tmpSelf = this;
649
+ if (tmpToggle)
650
+ {
651
+ tmpToggle.addEventListener('click', () =>
652
+ {
653
+ tmpSelf.toggleSidebar();
654
+ });
655
+ }
656
+
657
+ // Wire up resize handle
658
+ this._wireResizeHandle();
659
+
660
+ // Listen for hash changes
661
+ window.addEventListener('hashchange', () =>
662
+ {
663
+ tmpSelf.pict.PictApplication.resolveHash();
664
+ });
665
+
666
+ // Keyboard shortcuts
667
+ window.addEventListener('keydown', (pEvent) =>
668
+ {
669
+ // Cmd+S (Mac) / Ctrl+S (Windows/Linux) to save
670
+ if ((pEvent.metaKey || pEvent.ctrlKey) && pEvent.key === 's')
671
+ {
672
+ pEvent.preventDefault();
673
+ tmpSelf.pict.PictApplication.saveCurrentFile();
674
+ return;
675
+ }
676
+
677
+ // F1 — Toggle between Reference and Files; open sidebar if collapsed
678
+ if (pEvent.key === 'F1')
679
+ {
680
+ pEvent.preventDefault();
681
+ tmpSelf._handleF1();
682
+ return;
683
+ }
684
+
685
+ // F2 — Toggle sidebar collapsed/expanded
686
+ if (pEvent.key === 'F2')
687
+ {
688
+ pEvent.preventDefault();
689
+ tmpSelf.toggleSidebar();
690
+ return;
691
+ }
692
+
693
+ // F3 or Cmd+Shift+U / Ctrl+Shift+U — Toggle image upload form
694
+ if (pEvent.key === 'F3')
695
+ {
696
+ pEvent.preventDefault();
697
+ tmpSelf.toggleUploadForm();
698
+ return;
699
+ }
700
+ if ((pEvent.metaKey || pEvent.ctrlKey) && pEvent.shiftKey && (pEvent.key === 'u' || pEvent.key === 'U'))
701
+ {
702
+ pEvent.preventDefault();
703
+ tmpSelf.toggleUploadForm();
704
+ return;
705
+ }
706
+
707
+ // F4 — Add topic from cursor / toggle Topics tab
708
+ if (pEvent.key === 'F4')
709
+ {
710
+ pEvent.preventDefault();
711
+ tmpSelf.pict.PictApplication.handleF4TopicAction();
712
+ return;
713
+ }
714
+
715
+ // Cmd+Shift+T / Ctrl+Shift+T — Toggle Topics tab
716
+ if ((pEvent.metaKey || pEvent.ctrlKey) && pEvent.shiftKey && (pEvent.key === 't' || pEvent.key === 'T'))
717
+ {
718
+ pEvent.preventDefault();
719
+ tmpSelf.pict.PictApplication.handleF4TopicAction();
720
+ return;
721
+ }
722
+
723
+ // Escape — Close the current file (if no overlay is open)
724
+ if (pEvent.key === 'Escape')
725
+ {
726
+ // Don't close if the upload overlay is open (let it close that first)
727
+ let tmpUploadOverlay = document.getElementById('ContentEditor-UploadOverlay');
728
+ if (tmpUploadOverlay && tmpUploadOverlay.classList.contains('open'))
729
+ {
730
+ tmpSelf.closeUploadForm();
731
+ return;
732
+ }
733
+
734
+ // Don't interfere if the confirmation dialog is open
735
+ // (its own Y/N/Esc handler takes precedence)
736
+ let tmpConfirmOverlay = document.getElementById('ContentEditor-ConfirmOverlay');
737
+ if (tmpConfirmOverlay && tmpConfirmOverlay.classList.contains('open'))
738
+ {
739
+ return;
740
+ }
741
+
742
+ // Close the current file
743
+ if (tmpSelf.pict.AppData.ContentEditor.CurrentFile)
744
+ {
745
+ pEvent.preventDefault();
746
+ tmpSelf.pict.PictApplication.closeCurrentFile();
747
+ return;
748
+ }
749
+ }
750
+ });
751
+
752
+ return super.onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent);
753
+ }
754
+
755
+ /**
756
+ * Toggle the sidebar collapsed/expanded state.
757
+ */
758
+ toggleSidebar()
759
+ {
760
+ let tmpWrap = document.getElementById('ContentEditor-SidebarWrap');
761
+ let tmpToggle = document.getElementById('ContentEditor-SidebarToggle');
762
+ if (!tmpWrap)
763
+ {
764
+ return;
765
+ }
766
+
767
+ let tmpSettings = this.pict.AppData.ContentEditor;
768
+ tmpSettings.SidebarCollapsed = !tmpSettings.SidebarCollapsed;
769
+
770
+ if (tmpSettings.SidebarCollapsed)
771
+ {
772
+ tmpWrap.classList.add('collapsed');
773
+ if (tmpToggle) tmpToggle.innerHTML = '&#x25B6;';
774
+ }
775
+ else
776
+ {
777
+ tmpWrap.classList.remove('collapsed');
778
+ tmpWrap.style.width = tmpSettings.SidebarWidth + 'px';
779
+ if (tmpToggle) tmpToggle.innerHTML = '&#x25C0;';
780
+ }
781
+
782
+ this.pict.PictApplication.saveSettings();
783
+ }
784
+
785
+ /**
786
+ * Switch the active sidebar tab.
787
+ *
788
+ * @param {string} pTab - 'files', 'reference', or 'topics'
789
+ */
790
+ switchSidebarTab(pTab)
791
+ {
792
+ let tmpPanes =
793
+ {
794
+ files: document.getElementById('ContentEditor-Sidebar-Container'),
795
+ reference: document.getElementById('ContentEditor-SidebarReference-Container'),
796
+ topics: document.getElementById('ContentEditor-SidebarTopics-Container')
797
+ };
798
+
799
+ let tmpTabs =
800
+ {
801
+ files: document.getElementById('ContentEditor-SidebarTab-Files'),
802
+ reference: document.getElementById('ContentEditor-SidebarTab-Reference'),
803
+ topics: document.getElementById('ContentEditor-SidebarTab-Topics')
804
+ };
805
+
806
+ // Hide all panes and deactivate all tabs
807
+ for (let tmpKey in tmpPanes)
808
+ {
809
+ if (tmpPanes[tmpKey]) tmpPanes[tmpKey].style.display = 'none';
810
+ if (tmpTabs[tmpKey]) tmpTabs[tmpKey].classList.remove('active');
811
+ }
812
+
813
+ // Show the selected pane and activate the selected tab
814
+ if (tmpPanes[pTab]) tmpPanes[pTab].style.display = '';
815
+ if (tmpTabs[pTab]) tmpTabs[pTab].classList.add('active');
816
+
817
+ // Lazy-render the Reference view on first switch
818
+ if (pTab === 'reference')
819
+ {
820
+ let tmpRefView = this.pict.views['ContentEditor-MarkdownReference'];
821
+ if (tmpRefView && !tmpRefView._hasRendered)
822
+ {
823
+ tmpRefView.render();
824
+ }
825
+ }
826
+
827
+ // Lazy-render the Topics view on first switch
828
+ if (pTab === 'topics')
829
+ {
830
+ let tmpTopicsView = this.pict.views['ContentEditor-Topics'];
831
+ if (tmpTopicsView && !tmpTopicsView._hasRendered)
832
+ {
833
+ tmpTopicsView.render();
834
+ }
835
+ }
836
+ }
837
+
838
+ /**
839
+ * Handle F1: toggle between Reference and Files sidebar tabs.
840
+ * If the sidebar is collapsed, open it and switch to Reference.
841
+ */
842
+ _handleF1()
843
+ {
844
+ let tmpSettings = this.pict.AppData.ContentEditor;
845
+
846
+ // If sidebar is collapsed, expand it and go to Reference
847
+ if (tmpSettings.SidebarCollapsed)
848
+ {
849
+ this.toggleSidebar();
850
+ this.switchSidebarTab('reference');
851
+ return;
852
+ }
853
+
854
+ // Determine which tab is currently active
855
+ let tmpRefTab = document.getElementById('ContentEditor-SidebarTab-Reference');
856
+ let tmpIsOnRef = tmpRefTab && tmpRefTab.classList.contains('active');
857
+
858
+ // Toggle: if on Reference, go to Files; otherwise go to Reference
859
+ if (tmpIsOnRef)
860
+ {
861
+ this.switchSidebarTab('files');
862
+ }
863
+ else
864
+ {
865
+ this.switchSidebarTab('reference');
866
+ }
867
+ }
868
+
869
+ /**
870
+ * Return the identifier of the currently active sidebar tab.
871
+ *
872
+ * @returns {string} 'files', 'reference', or 'topics'
873
+ */
874
+ getActiveSidebarTab()
875
+ {
876
+ let tmpRefTab = document.getElementById('ContentEditor-SidebarTab-Reference');
877
+ let tmpTopicsTab = document.getElementById('ContentEditor-SidebarTab-Topics');
878
+
879
+ if (tmpRefTab && tmpRefTab.classList.contains('active')) return 'reference';
880
+ if (tmpTopicsTab && tmpTopicsTab.classList.contains('active')) return 'topics';
881
+ return 'files';
882
+ }
883
+
884
+ /**
885
+ * Toggle the image upload form open/closed.
886
+ */
887
+ toggleUploadForm()
888
+ {
889
+ let tmpOverlay = document.getElementById('ContentEditor-UploadOverlay');
890
+ if (!tmpOverlay)
891
+ {
892
+ return;
893
+ }
894
+
895
+ if (tmpOverlay.classList.contains('open'))
896
+ {
897
+ this.closeUploadForm();
898
+ }
899
+ else
900
+ {
901
+ this.openUploadForm();
902
+ }
903
+ }
904
+
905
+ /**
906
+ * Open the image upload form.
907
+ */
908
+ openUploadForm()
909
+ {
910
+ let tmpOverlay = document.getElementById('ContentEditor-UploadOverlay');
911
+ if (tmpOverlay)
912
+ {
913
+ tmpOverlay.classList.add('open');
914
+ }
915
+
916
+ // Wire up drag-drop on the dropzone
917
+ this._wireUploadDropzone();
918
+ }
919
+
920
+ /**
921
+ * Close the image upload form and reset its state.
922
+ */
923
+ closeUploadForm()
924
+ {
925
+ let tmpOverlay = document.getElementById('ContentEditor-UploadOverlay');
926
+ if (tmpOverlay)
927
+ {
928
+ tmpOverlay.classList.remove('open');
929
+ }
930
+
931
+ // Reset the file input so the same file can be re-selected
932
+ let tmpInput = document.getElementById('ContentEditor-UploadFileInput');
933
+ if (tmpInput)
934
+ {
935
+ tmpInput.value = '';
936
+ }
937
+
938
+ // Clear status and result
939
+ let tmpStatus = document.getElementById('ContentEditor-UploadStatus');
940
+ if (tmpStatus) tmpStatus.innerHTML = '';
941
+ let tmpResult = document.getElementById('ContentEditor-UploadResult');
942
+ if (tmpResult) tmpResult.innerHTML = '';
943
+ }
944
+
945
+ /**
946
+ * Close overlay if the background (not the panel) is clicked.
947
+ *
948
+ * @param {MouseEvent} pEvent
949
+ */
950
+ onUploadOverlayClick(pEvent)
951
+ {
952
+ if (pEvent.target.id === 'ContentEditor-UploadOverlay')
953
+ {
954
+ this.closeUploadForm();
955
+ }
956
+ }
957
+
958
+ /**
959
+ * Handle file selection from the file input.
960
+ *
961
+ * @param {HTMLInputElement} pInput
962
+ */
963
+ onUploadFileSelected(pInput)
964
+ {
965
+ if (pInput.files && pInput.files.length > 0)
966
+ {
967
+ this._uploadFile(pInput.files[0]);
968
+ }
969
+ }
970
+
971
+ /**
972
+ * Wire drag-and-drop events on the upload dropzone.
973
+ */
974
+ _wireUploadDropzone()
975
+ {
976
+ let tmpDropzone = document.getElementById('ContentEditor-UploadDropzone');
977
+ if (!tmpDropzone || tmpDropzone._wired)
978
+ {
979
+ return;
980
+ }
981
+ tmpDropzone._wired = true;
982
+
983
+ let tmpSelf = this;
984
+
985
+ tmpDropzone.addEventListener('dragover', (pEvent) =>
986
+ {
987
+ pEvent.preventDefault();
988
+ pEvent.stopPropagation();
989
+ tmpDropzone.classList.add('dragover');
990
+ });
991
+
992
+ tmpDropzone.addEventListener('dragleave', (pEvent) =>
993
+ {
994
+ pEvent.preventDefault();
995
+ pEvent.stopPropagation();
996
+ tmpDropzone.classList.remove('dragover');
997
+ });
998
+
999
+ tmpDropzone.addEventListener('drop', (pEvent) =>
1000
+ {
1001
+ pEvent.preventDefault();
1002
+ pEvent.stopPropagation();
1003
+ tmpDropzone.classList.remove('dragover');
1004
+
1005
+ if (pEvent.dataTransfer && pEvent.dataTransfer.files && pEvent.dataTransfer.files.length > 0)
1006
+ {
1007
+ tmpSelf._uploadFile(pEvent.dataTransfer.files[0]);
1008
+ }
1009
+ });
1010
+ }
1011
+
1012
+ /**
1013
+ * Upload a file using the ContentEditor provider.
1014
+ *
1015
+ * @param {File} pFile - The image file to upload
1016
+ */
1017
+ _uploadFile(pFile)
1018
+ {
1019
+ let tmpStatus = document.getElementById('ContentEditor-UploadStatus');
1020
+ let tmpResult = document.getElementById('ContentEditor-UploadResult');
1021
+
1022
+ if (!pFile)
1023
+ {
1024
+ return;
1025
+ }
1026
+
1027
+ // Validate it's an image
1028
+ if (!pFile.type.startsWith('image/'))
1029
+ {
1030
+ if (tmpStatus)
1031
+ {
1032
+ tmpStatus.innerHTML = '<span class="content-editor-upload-status-error">Only image files are supported.</span>';
1033
+ }
1034
+ return;
1035
+ }
1036
+
1037
+ if (tmpStatus)
1038
+ {
1039
+ tmpStatus.innerHTML = 'Uploading <strong>' + pFile.name + '</strong>...';
1040
+ }
1041
+ if (tmpResult)
1042
+ {
1043
+ tmpResult.innerHTML = '';
1044
+ }
1045
+
1046
+ let tmpSelf = this;
1047
+ let tmpProvider = this.pict.providers['ContentEditor-Provider'];
1048
+
1049
+ if (!tmpProvider)
1050
+ {
1051
+ if (tmpStatus)
1052
+ {
1053
+ tmpStatus.innerHTML = '<span class="content-editor-upload-status-error">Provider not available.</span>';
1054
+ }
1055
+ return;
1056
+ }
1057
+
1058
+ tmpProvider.uploadImage(pFile, (pError, pURL) =>
1059
+ {
1060
+ if (pError)
1061
+ {
1062
+ if (tmpStatus)
1063
+ {
1064
+ tmpStatus.innerHTML = '<span class="content-editor-upload-status-error">Upload failed: ' + pError + '</span>';
1065
+ }
1066
+ return;
1067
+ }
1068
+
1069
+ if (tmpStatus)
1070
+ {
1071
+ tmpStatus.innerHTML = '<span class="content-editor-upload-status-success">Uploaded successfully!</span>';
1072
+ }
1073
+
1074
+ let tmpMarkdown = '![' + pFile.name + '](' + pURL + ')';
1075
+
1076
+ if (tmpResult)
1077
+ {
1078
+ tmpResult.innerHTML =
1079
+ '<div class="content-editor-upload-result">' +
1080
+ '<div class="content-editor-upload-result-label">Markdown</div>' +
1081
+ '<div class="content-editor-upload-result-url">' +
1082
+ '<span class="content-editor-upload-result-text">' + tmpMarkdown + '</span>' +
1083
+ '<button class="content-editor-upload-result-copy" onclick="' +
1084
+ "navigator.clipboard.writeText('" + tmpMarkdown.replace(/'/g, "\\'") + "').then(function(){this.textContent='Copied!'}.bind(this))" +
1085
+ '">Copy</button>' +
1086
+ '</div>' +
1087
+ '<div class="content-editor-upload-result-label" style="margin-top:8px">URL</div>' +
1088
+ '<div class="content-editor-upload-result-url">' +
1089
+ '<span class="content-editor-upload-result-text">' + pURL + '</span>' +
1090
+ '<button class="content-editor-upload-result-copy" onclick="' +
1091
+ "navigator.clipboard.writeText('" + pURL.replace(/'/g, "\\'") + "').then(function(){this.textContent='Copied!'}.bind(this))" +
1092
+ '">Copy</button>' +
1093
+ '</div>' +
1094
+ '</div>';
1095
+ }
1096
+
1097
+ // Refresh the file list so the uploaded file shows
1098
+ tmpSelf.pict.PictApplication.loadFileList();
1099
+ });
1100
+ }
1101
+
1102
+ /**
1103
+ * Wire up the drag-to-resize handle for the sidebar.
1104
+ */
1105
+ _wireResizeHandle()
1106
+ {
1107
+ let tmpHandle = document.getElementById('ContentEditor-ResizeHandle');
1108
+ let tmpWrap = document.getElementById('ContentEditor-SidebarWrap');
1109
+ if (!tmpHandle || !tmpWrap)
1110
+ {
1111
+ return;
1112
+ }
1113
+
1114
+ let tmpSelf = this;
1115
+ let tmpDragging = false;
1116
+ let tmpStartX = 0;
1117
+ let tmpStartWidth = 0;
1118
+
1119
+ function onMouseDown(pEvent)
1120
+ {
1121
+ if (tmpSelf.pict.AppData.ContentEditor.SidebarCollapsed)
1122
+ {
1123
+ return;
1124
+ }
1125
+ pEvent.preventDefault();
1126
+ tmpDragging = true;
1127
+ tmpStartX = pEvent.clientX;
1128
+ tmpStartWidth = tmpWrap.offsetWidth;
1129
+ tmpHandle.classList.add('dragging');
1130
+
1131
+ // Disable transitions while dragging for snappy feel
1132
+ tmpWrap.style.transition = 'none';
1133
+
1134
+ // Prevent text selection while dragging
1135
+ document.body.style.userSelect = 'none';
1136
+ document.body.style.cursor = 'col-resize';
1137
+
1138
+ document.addEventListener('mousemove', onMouseMove);
1139
+ document.addEventListener('mouseup', onMouseUp);
1140
+ }
1141
+
1142
+ function onMouseMove(pEvent)
1143
+ {
1144
+ if (!tmpDragging)
1145
+ {
1146
+ return;
1147
+ }
1148
+ let tmpDelta = pEvent.clientX - tmpStartX;
1149
+ let tmpNewWidth = tmpStartWidth + tmpDelta;
1150
+
1151
+ // Clamp
1152
+ if (tmpNewWidth < tmpSelf._minSidebarWidth)
1153
+ {
1154
+ tmpNewWidth = tmpSelf._minSidebarWidth;
1155
+ }
1156
+ if (tmpNewWidth > tmpSelf._maxSidebarWidth)
1157
+ {
1158
+ tmpNewWidth = tmpSelf._maxSidebarWidth;
1159
+ }
1160
+
1161
+ tmpWrap.style.width = tmpNewWidth + 'px';
1162
+ }
1163
+
1164
+ function onMouseUp()
1165
+ {
1166
+ if (!tmpDragging)
1167
+ {
1168
+ return;
1169
+ }
1170
+ tmpDragging = false;
1171
+ tmpHandle.classList.remove('dragging');
1172
+
1173
+ // Restore transitions
1174
+ tmpWrap.style.transition = '';
1175
+
1176
+ // Restore body
1177
+ document.body.style.userSelect = '';
1178
+ document.body.style.cursor = '';
1179
+
1180
+ // Persist the width to AppData and localStorage
1181
+ tmpSelf.pict.AppData.ContentEditor.SidebarWidth = tmpWrap.offsetWidth;
1182
+ tmpSelf.pict.PictApplication.saveSettings();
1183
+
1184
+ document.removeEventListener('mousemove', onMouseMove);
1185
+ document.removeEventListener('mouseup', onMouseUp);
1186
+ }
1187
+
1188
+ tmpHandle.addEventListener('mousedown', onMouseDown);
1189
+ }
1190
+ }
1191
+
1192
+ module.exports = ContentEditorLayoutView;
1193
+
1194
+ module.exports.default_configuration = _ViewConfiguration;