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/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 bodyHtml = renderToStaticMarkup(element);
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
  }