egregore-artifacts 0.5.0 → 0.9.5
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/bin/cli.js +12 -2
- package/lib/artifact-id.js +28 -0
- package/lib/comments.js +248 -0
- package/lib/components.js +2 -2
- package/lib/index.js +34 -5
- package/lib/markdown.js +53 -5
- package/lib/parsers/handoff-v1.js +65 -0
- package/lib/parsers/handoff.js +30 -11
- package/lib/render.js +12 -2
- package/lib/shell.js +450 -0
- package/lib/templates/handoff-v1.js +762 -0
- package/lib/templates/subgraph.js +446 -346
- package/package.json +1 -1
package/lib/render.js
CHANGED
|
@@ -4,12 +4,22 @@ import { renderToStaticMarkup } from 'react-dom/server';
|
|
|
4
4
|
import { Renderer, JSONUIProvider, createStateStore } from '@json-render/react';
|
|
5
5
|
import { registry } from './registry.js';
|
|
6
6
|
import { htmlShell } from './shell.js';
|
|
7
|
+
import { CommentSection } from './comments.js';
|
|
7
8
|
|
|
8
9
|
const h = React.createElement;
|
|
9
10
|
|
|
10
|
-
// Render a React element tree (from direct templates)
|
|
11
|
+
// Render a React element tree (from direct templates).
|
|
12
|
+
// If `options.parent` is set, append a CommentSection so every commentable
|
|
13
|
+
// artifact gets the same thread + composer treatment without per-template work.
|
|
11
14
|
export function renderToHtml(element, options = {}) {
|
|
12
|
-
const
|
|
15
|
+
const finalElement = options.parent
|
|
16
|
+
? h('div', null, element, h(CommentSection, {
|
|
17
|
+
parent: options.parent,
|
|
18
|
+
comments: options.comments || [],
|
|
19
|
+
}))
|
|
20
|
+
: element;
|
|
21
|
+
|
|
22
|
+
const bodyHtml = renderToStaticMarkup(finalElement);
|
|
13
23
|
return htmlShell(bodyHtml, {
|
|
14
24
|
title: options.title || 'Egregore Artifact',
|
|
15
25
|
type: options.type || 'artifact',
|
package/lib/shell.js
CHANGED
|
@@ -29,6 +29,7 @@ export function htmlShell(bodyHtml, { title = 'Egregore Artifact', type = 'artif
|
|
|
29
29
|
--muted: ${colors.muted};
|
|
30
30
|
--warm-gray: ${colors.warmGray};
|
|
31
31
|
--terminal-bg: ${colors.terminalBg};
|
|
32
|
+
--terminal-text: ${colors.terminalText};
|
|
32
33
|
--font-serif: ${fonts.serif};
|
|
33
34
|
--font-sans: ${fonts.sans};
|
|
34
35
|
--font-mono: ${fonts.mono};
|
|
@@ -45,6 +46,13 @@ export function htmlShell(bodyHtml, { title = 'Egregore Artifact', type = 'artif
|
|
|
45
46
|
--success-bg: #e8f5e9;
|
|
46
47
|
--success-fg: #2e7d32;
|
|
47
48
|
--green-p1: #2A7B5B;
|
|
49
|
+
/* Subgraph node fills — subtle tinted backgrounds per node type.
|
|
50
|
+
Strokes/labels use the existing strong tokens (--terracotta,
|
|
51
|
+
--blue-muted, --green-p1, --gold, etc.) for contrast. */
|
|
52
|
+
--quest-fill: rgba(42, 123, 91, 0.12);
|
|
53
|
+
--artifact-fill: rgba(176, 141, 87, 0.14);
|
|
54
|
+
--artifact-stroke: #B08D57;
|
|
55
|
+
--pr-fill: rgba(59, 45, 33, 0.04);
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
/* Dark mode overrides */
|
|
@@ -56,6 +64,7 @@ export function htmlShell(bodyHtml, { title = 'Egregore Artifact', type = 'artif
|
|
|
56
64
|
--muted: ${colors.darkTextMuted};
|
|
57
65
|
--warm-gray: ${colors.darkTextDim};
|
|
58
66
|
--terminal-bg: ${colors.darkBgCode};
|
|
67
|
+
--terminal-text: ${colors.terminalText};
|
|
59
68
|
--surface: ${colors.darkBgCard};
|
|
60
69
|
--hairline: rgba(255, 255, 255, 0.08);
|
|
61
70
|
--subtle-fill: rgba(255, 255, 255, 0.04);
|
|
@@ -67,6 +76,10 @@ export function htmlShell(bodyHtml, { title = 'Egregore Artifact', type = 'artif
|
|
|
67
76
|
--success-bg: rgba(107, 191, 107, 0.12);
|
|
68
77
|
--success-fg: #9ed9a0;
|
|
69
78
|
--green-p1: #6BBF6B;
|
|
79
|
+
--quest-fill: rgba(107, 191, 107, 0.14);
|
|
80
|
+
--artifact-fill: rgba(200, 169, 122, 0.16);
|
|
81
|
+
--artifact-stroke: #C8A97A;
|
|
82
|
+
--pr-fill: rgba(255, 255, 255, 0.04);
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -424,6 +437,240 @@ export function htmlShell(bodyHtml, { title = 'Egregore Artifact', type = 'artif
|
|
|
424
437
|
.eg-meta-row { gap: 10px; }
|
|
425
438
|
.eg-theme-toggle { top: 0.75rem; right: 0.75rem; width: 32px; height: 32px; font-size: 14px; }
|
|
426
439
|
}
|
|
440
|
+
|
|
441
|
+
/* ── Comments section ─────────────────────────────── */
|
|
442
|
+
.eg-comments-section {
|
|
443
|
+
margin-top: 3rem;
|
|
444
|
+
padding-top: 2rem;
|
|
445
|
+
border-top: 1px solid var(--hairline);
|
|
446
|
+
}
|
|
447
|
+
.eg-comments-heading {
|
|
448
|
+
display: flex;
|
|
449
|
+
align-items: baseline;
|
|
450
|
+
gap: 0.5rem;
|
|
451
|
+
font-family: var(--font-sans);
|
|
452
|
+
font-size: 18px;
|
|
453
|
+
font-weight: 600;
|
|
454
|
+
color: var(--black);
|
|
455
|
+
margin-bottom: 1.25rem;
|
|
456
|
+
}
|
|
457
|
+
.eg-comments-bullet { font-size: 16px; }
|
|
458
|
+
.eg-comments-count { color: var(--muted); font-weight: 400; font-size: 15px; }
|
|
459
|
+
.eg-comments-empty {
|
|
460
|
+
font-family: var(--font-sans);
|
|
461
|
+
color: var(--muted);
|
|
462
|
+
font-style: italic;
|
|
463
|
+
padding: 1rem 0 1.5rem;
|
|
464
|
+
}
|
|
465
|
+
.eg-comments-thread {
|
|
466
|
+
display: flex;
|
|
467
|
+
flex-direction: column;
|
|
468
|
+
gap: 1rem;
|
|
469
|
+
margin-bottom: 2rem;
|
|
470
|
+
}
|
|
471
|
+
.eg-comment {
|
|
472
|
+
background: var(--surface);
|
|
473
|
+
border: 1px solid var(--hairline);
|
|
474
|
+
border-radius: 6px;
|
|
475
|
+
padding: 0.875rem 1rem;
|
|
476
|
+
}
|
|
477
|
+
.eg-comment-meta {
|
|
478
|
+
display: flex;
|
|
479
|
+
align-items: center;
|
|
480
|
+
gap: 0.5rem;
|
|
481
|
+
font-family: var(--font-mono);
|
|
482
|
+
font-size: 12px;
|
|
483
|
+
color: var(--muted);
|
|
484
|
+
margin-bottom: 0.5rem;
|
|
485
|
+
}
|
|
486
|
+
.eg-comment-avatar {
|
|
487
|
+
display: inline-flex;
|
|
488
|
+
align-items: center;
|
|
489
|
+
justify-content: center;
|
|
490
|
+
width: 22px; height: 22px;
|
|
491
|
+
border-radius: 50%;
|
|
492
|
+
background: var(--terracotta-chip);
|
|
493
|
+
color: var(--terracotta);
|
|
494
|
+
font-family: var(--font-sans);
|
|
495
|
+
font-size: 11px;
|
|
496
|
+
font-weight: 600;
|
|
497
|
+
}
|
|
498
|
+
.eg-comment-author { color: var(--black); font-weight: 500; }
|
|
499
|
+
.eg-comment-date { font-feature-settings: "tnum"; }
|
|
500
|
+
.eg-comment-to { color: var(--blue-muted); }
|
|
501
|
+
.eg-comment-body {
|
|
502
|
+
font-family: var(--font-serif);
|
|
503
|
+
font-size: 15px;
|
|
504
|
+
line-height: 1.55;
|
|
505
|
+
color: var(--dark);
|
|
506
|
+
}
|
|
507
|
+
.eg-comment-body p { margin: 0 0 0.5rem; }
|
|
508
|
+
.eg-comment-body p:last-child { margin-bottom: 0; }
|
|
509
|
+
.eg-comment-actions {
|
|
510
|
+
margin-top: 0.5rem;
|
|
511
|
+
display: flex;
|
|
512
|
+
justify-content: flex-end;
|
|
513
|
+
}
|
|
514
|
+
.eg-comment-reply {
|
|
515
|
+
background: transparent;
|
|
516
|
+
border: none;
|
|
517
|
+
color: var(--muted);
|
|
518
|
+
font-family: var(--font-mono);
|
|
519
|
+
font-size: 11px;
|
|
520
|
+
cursor: pointer;
|
|
521
|
+
padding: 0.25rem 0.5rem;
|
|
522
|
+
border-radius: 4px;
|
|
523
|
+
}
|
|
524
|
+
.eg-comment-reply:hover { background: var(--neutral-chip); color: var(--terracotta); }
|
|
525
|
+
.eg-comment-reply.eg-comment-reply-active {
|
|
526
|
+
background: var(--terracotta-chip);
|
|
527
|
+
color: var(--terracotta);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/* Composer — a snippet builder, NOT a form that posts */
|
|
531
|
+
.eg-comments-composer {
|
|
532
|
+
background: var(--surface);
|
|
533
|
+
border: 1px solid var(--hairline);
|
|
534
|
+
border-radius: 6px;
|
|
535
|
+
padding: 1rem;
|
|
536
|
+
}
|
|
537
|
+
.eg-comments-composer .eg-card-title {
|
|
538
|
+
font-family: var(--font-sans);
|
|
539
|
+
font-size: 14px;
|
|
540
|
+
font-weight: 600;
|
|
541
|
+
color: var(--black);
|
|
542
|
+
margin-bottom: 0.625rem;
|
|
543
|
+
}
|
|
544
|
+
.eg-comment-target {
|
|
545
|
+
font-family: var(--font-mono);
|
|
546
|
+
font-size: 12px;
|
|
547
|
+
background: var(--neutral-chip);
|
|
548
|
+
color: var(--terracotta);
|
|
549
|
+
padding: 1px 6px;
|
|
550
|
+
border-radius: 3px;
|
|
551
|
+
font-weight: 500;
|
|
552
|
+
}
|
|
553
|
+
.eg-comment-textarea {
|
|
554
|
+
width: 100%;
|
|
555
|
+
box-sizing: border-box;
|
|
556
|
+
background: var(--cream);
|
|
557
|
+
border: 1px solid var(--hairline);
|
|
558
|
+
border-radius: 4px;
|
|
559
|
+
padding: 0.625rem 0.75rem;
|
|
560
|
+
font-family: var(--font-sans);
|
|
561
|
+
font-size: 14px;
|
|
562
|
+
color: var(--black);
|
|
563
|
+
resize: vertical;
|
|
564
|
+
min-height: 96px;
|
|
565
|
+
margin-bottom: 1rem;
|
|
566
|
+
}
|
|
567
|
+
.eg-comment-textarea:focus {
|
|
568
|
+
outline: none;
|
|
569
|
+
border-color: var(--terracotta);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.eg-comment-preview-label {
|
|
573
|
+
font-family: var(--font-mono);
|
|
574
|
+
font-size: 11px;
|
|
575
|
+
color: var(--muted);
|
|
576
|
+
text-transform: uppercase;
|
|
577
|
+
letter-spacing: 0.05em;
|
|
578
|
+
margin-bottom: 0.375rem;
|
|
579
|
+
}
|
|
580
|
+
.eg-comment-preview-frame {
|
|
581
|
+
position: relative;
|
|
582
|
+
background: var(--terminal-bg);
|
|
583
|
+
border: 1px solid var(--hairline);
|
|
584
|
+
border-left: 3px solid var(--terracotta);
|
|
585
|
+
border-radius: 4px;
|
|
586
|
+
overflow: hidden;
|
|
587
|
+
}
|
|
588
|
+
.eg-comment-preview {
|
|
589
|
+
margin: 0;
|
|
590
|
+
padding: 0.75rem 4.5rem 0.75rem 0.875rem;
|
|
591
|
+
font-family: var(--font-mono);
|
|
592
|
+
font-size: 12.5px;
|
|
593
|
+
line-height: 1.55;
|
|
594
|
+
color: var(--terminal-text);
|
|
595
|
+
white-space: pre-wrap;
|
|
596
|
+
word-break: break-word;
|
|
597
|
+
overflow-x: auto;
|
|
598
|
+
min-height: 1em;
|
|
599
|
+
}
|
|
600
|
+
.eg-comment-preview-code {
|
|
601
|
+
font-family: inherit;
|
|
602
|
+
font-size: inherit;
|
|
603
|
+
color: inherit;
|
|
604
|
+
background: transparent;
|
|
605
|
+
padding: 0;
|
|
606
|
+
border: 0;
|
|
607
|
+
}
|
|
608
|
+
.eg-comment-preview-code:empty::before {
|
|
609
|
+
content: '(your snippet will appear here as you type)';
|
|
610
|
+
color: rgba(255, 255, 255, 0.4);
|
|
611
|
+
font-style: italic;
|
|
612
|
+
}
|
|
613
|
+
.eg-comment-copy-btn {
|
|
614
|
+
position: absolute;
|
|
615
|
+
top: 0.5rem;
|
|
616
|
+
right: 0.5rem;
|
|
617
|
+
background: rgba(255, 255, 255, 0.08);
|
|
618
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
619
|
+
color: var(--terminal-text);
|
|
620
|
+
border-radius: 3px;
|
|
621
|
+
padding: 0.25rem 0.625rem;
|
|
622
|
+
font-family: var(--font-mono);
|
|
623
|
+
font-size: 11px;
|
|
624
|
+
cursor: pointer;
|
|
625
|
+
}
|
|
626
|
+
.eg-comment-copy-btn:hover {
|
|
627
|
+
background: var(--terracotta);
|
|
628
|
+
border-color: var(--terracotta);
|
|
629
|
+
color: white;
|
|
630
|
+
}
|
|
631
|
+
.eg-comment-copy-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
632
|
+
|
|
633
|
+
.eg-comment-status {
|
|
634
|
+
font-family: var(--font-mono);
|
|
635
|
+
font-size: 12px;
|
|
636
|
+
color: var(--muted);
|
|
637
|
+
margin-top: 0.5rem;
|
|
638
|
+
min-height: 1.2em;
|
|
639
|
+
}
|
|
640
|
+
.eg-comment-status.eg-comment-status-ok { color: var(--green-p1); }
|
|
641
|
+
.eg-comment-status.eg-comment-status-err { color: var(--terracotta); }
|
|
642
|
+
|
|
643
|
+
.eg-comment-help {
|
|
644
|
+
margin-top: 0.875rem;
|
|
645
|
+
padding-top: 0.75rem;
|
|
646
|
+
border-top: 1px dashed var(--hairline);
|
|
647
|
+
font-family: var(--font-mono);
|
|
648
|
+
font-size: 11px;
|
|
649
|
+
color: var(--muted);
|
|
650
|
+
line-height: 1.55;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/* Dual-mode visibility: paste-back by default, live when loopback is up */
|
|
654
|
+
.eg-live-only { display: none; }
|
|
655
|
+
.eg-comments-composer.eg-live-mode .eg-live-only { display: inline; }
|
|
656
|
+
.eg-comments-composer.eg-live-mode .eg-paste-only { display: none; }
|
|
657
|
+
|
|
658
|
+
.eg-comment-post-btn {
|
|
659
|
+
display: none;
|
|
660
|
+
background: var(--terracotta);
|
|
661
|
+
color: white;
|
|
662
|
+
border: none;
|
|
663
|
+
border-radius: 4px;
|
|
664
|
+
padding: 0.625rem 1rem;
|
|
665
|
+
margin-top: 0.75rem;
|
|
666
|
+
font-family: var(--font-sans);
|
|
667
|
+
font-size: 14px;
|
|
668
|
+
font-weight: 500;
|
|
669
|
+
cursor: pointer;
|
|
670
|
+
}
|
|
671
|
+
.eg-comment-post-btn:hover { filter: brightness(1.05); }
|
|
672
|
+
.eg-comment-post-btn:disabled { opacity: 0.5; cursor: progress; }
|
|
673
|
+
.eg-comments-composer.eg-live-mode .eg-comment-post-btn { display: inline-block; }
|
|
427
674
|
</style>
|
|
428
675
|
<script>
|
|
429
676
|
// Theme engine: light / auto / dark
|
|
@@ -487,6 +734,209 @@ export function htmlShell(bodyHtml, { title = 'Egregore Artifact', type = 'artif
|
|
|
487
734
|
<div class="eg-artifact" data-type="${escapeHtml(type)}">
|
|
488
735
|
${bodyHtml}
|
|
489
736
|
</div>
|
|
737
|
+
<script>
|
|
738
|
+
// Comment composer — assembles a structured payload and copies it to the
|
|
739
|
+
// clipboard. The user pastes into Claude; the /comment skill recognizes
|
|
740
|
+
// the marker and runs the write/index/notify/commit flow.
|
|
741
|
+
(function() {
|
|
742
|
+
var composer = document.querySelector('.eg-comments-composer');
|
|
743
|
+
if (!composer) return;
|
|
744
|
+
|
|
745
|
+
var parentKind = composer.dataset.parentKind;
|
|
746
|
+
var parentId = composer.dataset.parentId;
|
|
747
|
+
var textarea = composer.querySelector('.eg-comment-textarea');
|
|
748
|
+
var status = composer.querySelector('.eg-comment-status');
|
|
749
|
+
var copyBtn = composer.querySelector('.eg-comment-copy-btn');
|
|
750
|
+
var postBtn = composer.querySelector('.eg-comment-post-btn');
|
|
751
|
+
var previewCode = composer.querySelector('.eg-comment-preview-code');
|
|
752
|
+
var targetChip = composer.querySelector('.eg-comment-target');
|
|
753
|
+
var placeholderDefault = textarea.placeholder;
|
|
754
|
+
var activeReplyTo = null;
|
|
755
|
+
|
|
756
|
+
var SERVER = 'http://127.0.0.1:7321';
|
|
757
|
+
|
|
758
|
+
// Probe the loopback server. If reachable, switch the composer into
|
|
759
|
+
// live mode so the user gets a real "Post comment" button instead of
|
|
760
|
+
// the clipboard handoff.
|
|
761
|
+
fetch(SERVER + '/health', { method: 'GET' })
|
|
762
|
+
.then(function(r) { if (r.ok) composer.classList.add('eg-live-mode'); })
|
|
763
|
+
.catch(function() { /* paste-back stays as the default */ });
|
|
764
|
+
|
|
765
|
+
function buildPayload() {
|
|
766
|
+
var body = textarea.value.trim();
|
|
767
|
+
if (!body) return '';
|
|
768
|
+
var parentArg = activeReplyTo
|
|
769
|
+
? ('reply ' + activeReplyTo)
|
|
770
|
+
: (parentKind + '/' + parentId);
|
|
771
|
+
return '#egregore-comment:v1 ' + parentArg + '\\n\\n' + body;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function updatePreview() {
|
|
775
|
+
previewCode.textContent = buildPayload();
|
|
776
|
+
// Reset stale "Copied" state when the user edits
|
|
777
|
+
if (status.textContent && status.classList.contains('eg-comment-status-ok')) {
|
|
778
|
+
status.textContent = '';
|
|
779
|
+
status.className = 'eg-comment-status';
|
|
780
|
+
copyBtn.textContent = 'Copy';
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function updateTarget() {
|
|
785
|
+
if (activeReplyTo) {
|
|
786
|
+
var btn = document.querySelector('.eg-comment-reply[data-reply-to="' + activeReplyTo + '"]');
|
|
787
|
+
var author = btn ? btn.dataset.replyAuthor : '';
|
|
788
|
+
targetChip.textContent = author
|
|
789
|
+
? ('reply → ' + author + ' · ' + activeReplyTo)
|
|
790
|
+
: ('reply → ' + activeReplyTo);
|
|
791
|
+
} else {
|
|
792
|
+
targetChip.textContent = parentKind + '/' + parentId;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
textarea.addEventListener('input', updatePreview);
|
|
797
|
+
|
|
798
|
+
function clearReplyState() {
|
|
799
|
+
var buttons = document.querySelectorAll('.eg-comment-reply');
|
|
800
|
+
for (var i = 0; i < buttons.length; i++) {
|
|
801
|
+
buttons[i].classList.remove('eg-comment-reply-active');
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
var replyButtons = document.querySelectorAll('.eg-comment-reply');
|
|
806
|
+
for (var i = 0; i < replyButtons.length; i++) {
|
|
807
|
+
replyButtons[i].addEventListener('click', function(e) {
|
|
808
|
+
var btn = e.currentTarget;
|
|
809
|
+
var id = btn.dataset.replyTo;
|
|
810
|
+
var isActive = btn.classList.contains('eg-comment-reply-active');
|
|
811
|
+
clearReplyState();
|
|
812
|
+
if (isActive) {
|
|
813
|
+
activeReplyTo = null;
|
|
814
|
+
textarea.placeholder = placeholderDefault;
|
|
815
|
+
} else {
|
|
816
|
+
btn.classList.add('eg-comment-reply-active');
|
|
817
|
+
activeReplyTo = id;
|
|
818
|
+
textarea.placeholder = 'Replying to ' + id + ' — click ↩ again to cancel';
|
|
819
|
+
}
|
|
820
|
+
updateTarget();
|
|
821
|
+
updatePreview();
|
|
822
|
+
textarea.focus();
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function setStatus(ok, msg) {
|
|
827
|
+
status.textContent = msg;
|
|
828
|
+
status.className = 'eg-comment-status eg-comment-status-' + (ok ? 'ok' : 'err');
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Select the preview frame's text so the user has a visible affordance
|
|
832
|
+
// even if the copy mechanism fails.
|
|
833
|
+
function selectPreview() {
|
|
834
|
+
var range = document.createRange();
|
|
835
|
+
range.selectNodeContents(previewCode);
|
|
836
|
+
var sel = window.getSelection();
|
|
837
|
+
sel.removeAllRanges();
|
|
838
|
+
sel.addRange(range);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
copyBtn.addEventListener('click', function() {
|
|
842
|
+
var payload = buildPayload();
|
|
843
|
+
if (!payload) {
|
|
844
|
+
setStatus(false, 'Type a comment first — there’s nothing to copy.');
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Primary path: select the preview frame and execCommand('copy').
|
|
849
|
+
// Synchronous, works on file://, no permission prompt.
|
|
850
|
+
var copied = false;
|
|
851
|
+
try {
|
|
852
|
+
selectPreview();
|
|
853
|
+
copied = !!(document.execCommand && document.execCommand('copy'));
|
|
854
|
+
} catch (e) { copied = false; }
|
|
855
|
+
|
|
856
|
+
if (copied) {
|
|
857
|
+
setStatus(true, '✓ Snippet copied. Paste it into your Claude terminal.');
|
|
858
|
+
copyBtn.textContent = '✓ Copied';
|
|
859
|
+
setTimeout(function() { copyBtn.textContent = 'Copy'; }, 2000);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Fallback: modern Clipboard API.
|
|
864
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
865
|
+
try {
|
|
866
|
+
navigator.clipboard.writeText(payload).then(function() {
|
|
867
|
+
setStatus(true, '✓ Snippet copied. Paste it into your Claude terminal.');
|
|
868
|
+
copyBtn.textContent = '✓ Copied';
|
|
869
|
+
setTimeout(function() { copyBtn.textContent = 'Copy'; }, 2000);
|
|
870
|
+
}, function() {
|
|
871
|
+
selectPreview();
|
|
872
|
+
setStatus(false, 'Auto-copy failed — snippet selected above, press ⌘C / Ctrl+C.');
|
|
873
|
+
});
|
|
874
|
+
return;
|
|
875
|
+
} catch (e) { /* fall through */ }
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Last resort: leave the preview selected for manual copy.
|
|
879
|
+
selectPreview();
|
|
880
|
+
setStatus(false, 'Auto-copy unavailable — snippet selected above, press ⌘C / Ctrl+C.');
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
// Live-mode primary action: POST directly to the loopback server.
|
|
884
|
+
// If the connection is refused (server died since page-load), drop the
|
|
885
|
+
// composer back into paste-back mode rather than leaving the user stuck.
|
|
886
|
+
postBtn.addEventListener('click', function() {
|
|
887
|
+
var body = textarea.value.trim();
|
|
888
|
+
if (!body) {
|
|
889
|
+
setStatus(false, 'Type a comment first.');
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
var parentArg = activeReplyTo
|
|
893
|
+
? ('reply ' + activeReplyTo)
|
|
894
|
+
: (parentKind + '/' + parentId);
|
|
895
|
+
|
|
896
|
+
postBtn.disabled = true;
|
|
897
|
+
postBtn.textContent = 'Posting…';
|
|
898
|
+
setStatus(true, '');
|
|
899
|
+
|
|
900
|
+
// Tell the server where this rendered HTML lives so it can re-render
|
|
901
|
+
// in place. location.pathname for file:// URLs is the absolute path.
|
|
902
|
+
var refreshPath = (location.protocol === 'file:') ? decodeURIComponent(location.pathname) : '';
|
|
903
|
+
|
|
904
|
+
fetch(SERVER + '/api/comment', {
|
|
905
|
+
method: 'POST',
|
|
906
|
+
headers: { 'Content-Type': 'application/json' },
|
|
907
|
+
body: JSON.stringify({ parent: parentArg, body: body, refreshPath: refreshPath }),
|
|
908
|
+
})
|
|
909
|
+
.then(function(r) { return r.json().then(function(j) { return { ok: r.ok, json: j }; }); })
|
|
910
|
+
.then(function(res) {
|
|
911
|
+
if (res.ok && res.json && res.json.ok) {
|
|
912
|
+
var notified = (res.json.notified || []).filter(Boolean);
|
|
913
|
+
var msg = '✓ Posted on ' + res.json.parent;
|
|
914
|
+
if (notified.length) msg += ' · DMed: ' + notified.join(', ');
|
|
915
|
+
setStatus(true, msg + ' — refreshing…');
|
|
916
|
+
postBtn.textContent = '✓ Posted';
|
|
917
|
+
setTimeout(function() { window.location.reload(); }, 800);
|
|
918
|
+
} else {
|
|
919
|
+
postBtn.disabled = false;
|
|
920
|
+
postBtn.textContent = 'Post comment';
|
|
921
|
+
var err = (res.json && res.json.error) || 'unknown error';
|
|
922
|
+
setStatus(false, 'Post failed: ' + err);
|
|
923
|
+
}
|
|
924
|
+
})
|
|
925
|
+
.catch(function(err) {
|
|
926
|
+
// Network-level failure (server died, etc). Drop to paste-back so
|
|
927
|
+
// the user has a working path — and tell them why.
|
|
928
|
+
composer.classList.remove('eg-live-mode');
|
|
929
|
+
postBtn.disabled = false;
|
|
930
|
+
postBtn.textContent = 'Post comment';
|
|
931
|
+
setStatus(false,
|
|
932
|
+
'Local server unreachable (' + (err.message || 'network error') +
|
|
933
|
+
'). Switched to clipboard mode — use the Copy button.'
|
|
934
|
+
);
|
|
935
|
+
updatePreview();
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
})();
|
|
939
|
+
</script>
|
|
490
940
|
</body>
|
|
491
941
|
</html>`;
|
|
492
942
|
}
|