diff --git a/src/app.css b/src/app.css
index 1b0a9d5..d64dddc 100644
--- a/src/app.css
+++ b/src/app.css
@@ -1143,6 +1143,632 @@ body::before {
text-transform: uppercase !important;
}
+/* Terminal Theme - Toolbar and Editor */
+[data-design-theme="terminal"] .editor-toolbar,
+[data-design-theme="terminal"] .toolbar-group {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+}
+
+/* Terminal Theme - Textarea Buttons Toolbar (GIF/Emoji) - Transparent */
+[data-design-theme="terminal"] .textarea-buttons {
+ background: transparent !important;
+ border: none !important;
+}
+
+/* Terminal Theme - Advanced Search Container */
+[data-design-theme="terminal"] .advanced-filters-grid {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+}
+
+/* Terminal Theme - Event Relay Badge (in search results) */
+[data-design-theme="terminal"] .event-relay-badge {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+}
+
+/* Terminal Theme - Reply Context and Quoted Context */
+[data-design-theme="terminal"] .reply-context,
+[data-design-theme="terminal"] .quoted-context {
+ background: #000000 !important;
+ background-color: #000000 !important;
+ border-left-color: #00ff00 !important;
+ color: #00ff00 !important;
+ border: 1px solid #00ff00 !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.3) !important;
+}
+
+/* Override Tailwind background classes - need to target both light and dark variants */
+[data-design-theme="terminal"] .reply-context.bg-fog-highlight,
+[data-design-theme="terminal"] .dark .reply-context.bg-fog-dark-highlight,
+[data-design-theme="terminal"] .reply-context.dark\:bg-fog-dark-highlight,
+[data-design-theme="terminal"] .quoted-context.bg-fog-highlight,
+[data-design-theme="terminal"] .dark .quoted-context.bg-fog-dark-highlight,
+[data-design-theme="terminal"] .quoted-context.dark\:bg-fog-dark-highlight {
+ background: #000000 !important;
+ background-color: #000000 !important;
+}
+
+[data-design-theme="terminal"] .reply-context-content,
+[data-design-theme="terminal"] .quoted-context-content,
+[data-design-theme="terminal"] .reply-preview,
+[data-design-theme="terminal"] .quoted-preview,
+[data-design-theme="terminal"] .reply-kind-info,
+[data-design-theme="terminal"] .quoted-kind-info {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+/* Override Tailwind text color classes - need to target both light and dark variants */
+[data-design-theme="terminal"] .reply-context.text-fog-text-light,
+[data-design-theme="terminal"] .dark .reply-context.text-fog-dark-text-light,
+[data-design-theme="terminal"] .reply-context.dark\:text-fog-dark-text-light,
+[data-design-theme="terminal"] .quoted-context.text-fog-text-light,
+[data-design-theme="terminal"] .dark .quoted-context.text-fog-dark-text-light,
+[data-design-theme="terminal"] .quoted-context.dark\:text-fog-dark-text-light {
+ color: #00ff00 !important;
+}
+
+/* Terminal Theme - Referenced Event Preview */
+[data-design-theme="terminal"] .referenced-event-preview {
+ background: #000000 !important;
+ background-color: #000000 !important;
+ border-color: #00ff00 !important;
+ border-left-color: #00ff00 !important;
+ color: #00ff00 !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.3) !important;
+}
+
+[data-design-theme="terminal"] .referenced-event-label,
+[data-design-theme="terminal"] .referenced-event-title,
+[data-design-theme="terminal"] .referenced-event-preview-text,
+[data-design-theme="terminal"] .loading-text,
+[data-design-theme="terminal"] .error-text {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .view-event-button,
+[data-design-theme="terminal"] .view-website-button {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .view-event-button:hover,
+[data-design-theme="terminal"] .view-website-button:hover {
+ background: rgba(0, 255, 0, 0.1) !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important;
+}
+
+[data-design-theme="terminal"] .website-link {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+/* Terminal Theme - JSON Preview - Sharp Text Rendering */
+[data-design-theme="terminal"] .json-preview code,
+[data-design-theme="terminal"] .json-preview code.hljs,
+[data-design-theme="terminal"] .json-content code,
+[data-design-theme="terminal"] .json-content code.hljs,
+[data-design-theme="terminal"] .example-modal .json-preview code,
+[data-design-theme="terminal"] .example-modal .json-preview code.hljs,
+[data-design-theme="terminal"] .event-json .json-preview code,
+[data-design-theme="terminal"] .event-json .json-preview code.hljs {
+ text-shadow: none !important;
+ -webkit-font-smoothing: antialiased !important;
+ -moz-osx-font-smoothing: grayscale !important;
+ font-smoothing: antialiased !important;
+ text-rendering: optimizeLegibility !important;
+}
+
+/* Remove text-shadow from highlight.js spans inside JSON */
+[data-design-theme="terminal"] .json-preview code.hljs span,
+[data-design-theme="terminal"] .json-content code.hljs span,
+[data-design-theme="terminal"] .example-modal .json-preview code.hljs span,
+[data-design-theme="terminal"] .event-json .json-preview code.hljs span,
+[data-design-theme="terminal"] .json-preview code.hljs *,
+[data-design-theme="terminal"] .json-content code.hljs *,
+[data-design-theme="terminal"] .example-modal .json-preview code.hljs *,
+[data-design-theme="terminal"] .event-json .json-preview code.hljs * {
+ text-shadow: none !important;
+}
+
+/* Terminal Theme - RSS Page */
+[data-design-theme="terminal"] .rss-setup,
+[data-design-theme="terminal"] .rss-info,
+[data-design-theme="terminal"] .rss-items-section,
+[data-design-theme="terminal"] .rss-comment-form {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .rss-feed-item,
+[data-design-theme="terminal"] .rss-item {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .rss-feed-link,
+[data-design-theme="terminal"] .rss-item-link {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .rss-feed-link:hover,
+[data-design-theme="terminal"] .rss-item-link:hover {
+ color: #00ff00 !important;
+ text-shadow: 0 0 5px rgba(0, 255, 0, 0.8) !important;
+}
+
+[data-design-theme="terminal"] .rss-feed-badge {
+ background: #000000 !important;
+ border: 1px solid #00ff00 !important;
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .rss-item-time,
+[data-design-theme="terminal"] .rss-item-description,
+[data-design-theme="terminal"] .feed-error {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .create-rss-button,
+[data-design-theme="terminal"] .edit-rss-button,
+[data-design-theme="terminal"] .pagination-button {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .create-rss-button:hover:not(:disabled),
+[data-design-theme="terminal"] .edit-rss-button:hover:not(:disabled),
+[data-design-theme="terminal"] .pagination-button:hover:not(:disabled) {
+ background: rgba(0, 255, 0, 0.1) !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important;
+}
+
+[data-design-theme="terminal"] .pagination-button:disabled {
+ opacity: 0.5 !important;
+ border-color: rgba(0, 255, 0, 0.3) !important;
+}
+
+[data-design-theme="terminal"] .btn-action,
+[data-design-theme="terminal"] .btn-primary,
+[data-design-theme="terminal"] .btn-secondary {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .btn-action:hover:not(:disabled),
+[data-design-theme="terminal"] .btn-primary:hover:not(:disabled),
+[data-design-theme="terminal"] .btn-secondary:hover:not(:disabled) {
+ background: rgba(0, 255, 0, 0.1) !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important;
+}
+
+[data-design-theme="terminal"] .btn-action:disabled,
+[data-design-theme="terminal"] .btn-primary:disabled,
+[data-design-theme="terminal"] .btn-secondary:disabled {
+ opacity: 0.5 !important;
+ border-color: rgba(0, 255, 0, 0.3) !important;
+}
+
+/* Terminal Theme - Relay Page */
+[data-design-theme="terminal"] .relay-category,
+[data-design-theme="terminal"] .custom-relay-section {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .category-title {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .relay-item {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .relay-item:hover {
+ background: rgba(0, 255, 0, 0.1) !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .relay-url {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .relay-item:hover .relay-url {
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .relay-status.connected {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .relay-status.disconnected {
+ color: #ff0000 !important;
+ text-shadow: 0 0 3px rgba(255, 0, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .relay-item:hover .relay-status {
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .relay-category-badge {
+ background: #000000 !important;
+ border: 1px solid #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .relay-item:hover .relay-category-badge {
+ background: rgba(0, 255, 0, 0.1) !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .relay-arrow {
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .custom-relay-text-input {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .custom-relay-text-input:focus {
+ box-shadow: 0 0 10px rgba(0, 255, 0, 0.6) !important;
+ outline: none !important;
+}
+
+[data-design-theme="terminal"] .custom-relay-button {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .custom-relay-button:hover:not(:disabled) {
+ background: rgba(0, 255, 0, 0.1) !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important;
+}
+
+[data-design-theme="terminal"] .custom-relay-button:disabled {
+ opacity: 0.5 !important;
+ border-color: rgba(0, 255, 0, 0.3) !important;
+}
+
+/* Terminal Theme - Relay Feed Info Bar */
+[data-design-theme="terminal"] .relay-info {
+ background: #000000 !important;
+ border: 1px solid #00ff00 !important;
+ border-radius: 0.25rem !important;
+ padding: 0.75rem 1rem !important;
+ margin-bottom: 1rem !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.3) !important;
+}
+
+[data-design-theme="terminal"] .relay-info-text {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+ margin: 0 !important;
+}
+
+[data-design-theme="terminal"] .relay-info-text code.relay-url {
+ color: #00ff00 !important;
+ background: transparent !important;
+ border: none !important;
+ padding: 0 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .relay-info-text .relay-port {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+/* Terminal Theme - Topics Page */
+[data-design-theme="terminal"] .topic-item {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .topic-item:hover {
+ background: rgba(0, 255, 0, 0.1) !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .topic-item.interest {
+ border-left-color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .topic-name,
+[data-design-theme="terminal"] .topic-count {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .topic-item:hover .topic-name,
+[data-design-theme="terminal"] .topic-item:hover .topic-count {
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .pagination-button {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .pagination-button:hover:not(:disabled) {
+ background: rgba(0, 255, 0, 0.1) !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important;
+}
+
+[data-design-theme="terminal"] .pagination-button:disabled {
+ opacity: 0.5 !important;
+ border-color: rgba(0, 255, 0, 0.3) !important;
+}
+
+[data-design-theme="terminal"] .pagination-info {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+/* Terminal Theme - Repos Page */
+[data-design-theme="terminal"] .repo-item,
+[data-design-theme="terminal"] .search-results-section,
+[data-design-theme="terminal"] .profile-result-card,
+[data-design-theme="terminal"] .event-result-card {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .repo-item:hover,
+[data-design-theme="terminal"] .profile-result-card:hover,
+[data-design-theme="terminal"] .event-result-card:hover {
+ background: rgba(0, 255, 0, 0.1) !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important;
+ border-color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .repo-name,
+[data-design-theme="terminal"] .repo-description,
+[data-design-theme="terminal"] .repo-meta,
+[data-design-theme="terminal"] .repo-kind,
+[data-design-theme="terminal"] .results-title,
+[data-design-theme="terminal"] .results-group h3 {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .repo-kind {
+ background: #000000 !important;
+ border: 1px solid #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+/* Terminal Theme - Repo Detail Page */
+[data-design-theme="terminal"] .repo-meta-section,
+[data-design-theme="terminal"] .readme-container,
+[data-design-theme="terminal"] .commit-card,
+[data-design-theme="terminal"] .branch-item,
+[data-design-theme="terminal"] .issue-item,
+[data-design-theme="terminal"] .documentation-item {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .metadata-label,
+[data-design-theme="terminal"] .metadata-value,
+[data-design-theme="terminal"] .clone-url,
+[data-design-theme="terminal"] .event-id,
+[data-design-theme="terminal"] .naddr-code {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .relay-link {
+ background: #000000 !important;
+ border: 1px solid #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .relay-link:hover {
+ background: rgba(0, 255, 0, 0.1) !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .metadata-link {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .primary-badge {
+ background: #000000 !important;
+ border: 1px solid #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .maintainer-item.is-owner {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .branch-item.default {
+ border-color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .branch-name,
+[data-design-theme="terminal"] .branch-commit,
+[data-design-theme="terminal"] .branch-message,
+[data-design-theme="terminal"] .commit-sha,
+[data-design-theme="terminal"] .commit-message,
+[data-design-theme="terminal"] .commit-meta {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .branch-badge {
+ background: #000000 !important;
+ border: 1px solid #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .issues-filter {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .filter-label,
+[data-design-theme="terminal"] .filter-count {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .status-filter-select {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .status-filter-select:hover,
+[data-design-theme="terminal"] .status-filter-select:focus {
+ box-shadow: 0 0 10px rgba(0, 255, 0, 0.6) !important;
+ outline: none !important;
+}
+
+[data-design-theme="terminal"] .issue-header {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .status-label,
+[data-design-theme="terminal"] .status-changing,
+[data-design-theme="terminal"] .comments-header {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .status-select {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .status-select:hover:not(:disabled) {
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important;
+}
+
+[data-design-theme="terminal"] .status-select.open,
+[data-design-theme="terminal"] .status-select.closed,
+[data-design-theme="terminal"] .status-select.resolved,
+[data-design-theme="terminal"] .status-select.draft {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .issue-comments {
+ border-top-color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .comment-item {
+ border-left-color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .doc-header {
+ border-bottom-color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .doc-kind {
+ background: #000000 !important;
+ border: 1px solid #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .doc-event-link {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .toolbar-button {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .toolbar-button:hover:not(:disabled) {
+ background: rgba(0, 255, 0, 0.1) !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important;
+}
+
+[data-design-theme="terminal"] .toolbar-separator {
+ background: #00ff00 !important;
+}
+
+/* Terminal Theme - Modals */
+[data-design-theme="terminal"] .modal-overlay {
+ background: rgba(0, 0, 0, 0.8) !important;
+}
+
+[data-design-theme="terminal"] .modal-content,
+[data-design-theme="terminal"] .example-modal {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+ box-shadow: 0 0 10px rgba(0, 255, 0, 0.5) !important;
+}
+
+[data-design-theme="terminal"] .modal-header,
+[data-design-theme="terminal"] .modal-body,
+[data-design-theme="terminal"] .modal-footer {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .modal-header h2,
+[data-design-theme="terminal"] .modal-header h3 {
+ color: #00ff00 !important;
+ text-shadow: 0 0 3px rgba(0, 255, 0, 0.8) !important;
+}
+
+[data-design-theme="terminal"] .close-button {
+ background: transparent !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .close-button:hover {
+ background: rgba(0, 255, 0, 0.1) !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important;
+}
+
+/* Terminal Theme - General Buttons */
+[data-design-theme="terminal"] button:not(.toolbar-button):not(.close-button):not(.view-button):not(.toggle-button):not(.option-button):not(.action-button):not(.back-button):not(.write-button):not(.see-new-events-btn-header):not(.see-more-events-btn-header):not(.find-button):not(.create-rss-button):not(.edit-rss-button):not(.bulk-action-button):not(.load-more-button):not(.clear-kind-button) {
+ background: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] button:not(.toolbar-button):not(.close-button):not(.view-button):not(.toggle-button):not(.option-button):not(.action-button):not(.back-button):not(.write-button):not(.see-new-events-btn-header):not(.see-more-events-btn-header):not(.find-button):not(.create-rss-button):not(.edit-rss-button):not(.bulk-action-button):not(.load-more-button):not(.clear-kind-button):hover:not(:disabled) {
+ background: rgba(0, 255, 0, 0.1) !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important;
+}
+
[data-design-theme="terminal"] body::before {
background:
repeating-linear-gradient(
@@ -1945,3 +2571,169 @@ audio {
background-color: #1a8cd8 !important; /* Darker blue on hover */
opacity: 1 !important;
}
+
+/* Terminal Theme - Cache Page */
+[data-design-theme="terminal"] .cache-page,
+[data-design-theme="terminal"] .stats-section,
+[data-design-theme="terminal"] .filters-section,
+[data-design-theme="terminal"] .archive-section,
+[data-design-theme="terminal"] .bulk-actions-section,
+[data-design-theme="terminal"] .events-section {
+ background-color: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .back-button,
+[data-design-theme="terminal"] .bulk-action-button,
+[data-design-theme="terminal"] .action-button,
+[data-design-theme="terminal"] .load-more-button,
+[data-design-theme="terminal"] .clear-kind-button {
+ background-color: rgba(0, 255, 0, 0.1) !important;
+ border: 1px solid #00ff00 !important;
+ color: #00ff00 !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.3) !important;
+}
+
+[data-design-theme="terminal"] .back-button:hover:not(:disabled),
+[data-design-theme="terminal"] .bulk-action-button:hover:not(:disabled),
+[data-design-theme="terminal"] .action-button:hover:not(:disabled),
+[data-design-theme="terminal"] .load-more-button:hover:not(:disabled),
+[data-design-theme="terminal"] .clear-kind-button:hover:not(:disabled) {
+ background-color: rgba(0, 255, 0, 0.2) !important;
+ box-shadow: 0 0 10px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .stat-card,
+[data-design-theme="terminal"] .kind-item,
+[data-design-theme="terminal"] .event-card {
+ background-color: rgba(0, 255, 0, 0.05) !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .kind-item:hover {
+ background-color: rgba(0, 255, 0, 0.15) !important;
+ box-shadow: 0 0 8px rgba(0, 255, 0, 0.4) !important;
+}
+
+[data-design-theme="terminal"] .kind-item:hover .kind-name,
+[data-design-theme="terminal"] .kind-item:hover .kind-count {
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .stat-label,
+[data-design-theme="terminal"] .stat-value,
+[data-design-theme="terminal"] .kind-name,
+[data-design-theme="terminal"] .kind-count,
+[data-design-theme="terminal"] .event-id,
+[data-design-theme="terminal"] .event-meta,
+[data-design-theme="terminal"] .event-content-preview {
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .filter-label,
+[data-design-theme="terminal"] .filter-input,
+[data-design-theme="terminal"] .filter-select {
+ background-color: rgba(0, 255, 0, 0.05) !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .filter-input:focus,
+[data-design-theme="terminal"] .filter-select:focus {
+ outline: 2px solid #00ff00 !important;
+ box-shadow: 0 0 8px rgba(0, 255, 0, 0.4) !important;
+}
+
+[data-design-theme="terminal"] .event-id-code,
+[data-design-theme="terminal"] .event-json {
+ background-color: rgba(0, 255, 0, 0.05) !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .event-json code,
+[data-design-theme="terminal"] .event-meta code {
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .action-button.delete-action {
+ border-color: #ff0000 !important;
+ color: #ff0000 !important;
+}
+
+[data-design-theme="terminal"] .action-button.delete-action:hover {
+ background-color: rgba(255, 0, 0, 0.2) !important;
+ box-shadow: 0 0 10px rgba(255, 0, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .action-button.delete-request-action {
+ border-color: #ffaa00 !important;
+ color: #ffaa00 !important;
+}
+
+[data-design-theme="terminal"] .action-button.delete-request-action:hover {
+ background-color: rgba(255, 170, 0, 0.2) !important;
+ box-shadow: 0 0 10px rgba(255, 170, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .archive-stats p,
+[data-design-theme="terminal"] .archive-note,
+[data-design-theme="terminal"] .recover-event-section h3 {
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .loading-state,
+[data-design-theme="terminal"] .empty-state {
+ color: #00ff00 !important;
+}
+
+/* Terminal Theme - About Page */
+[data-design-theme="terminal"] .about-page,
+[data-design-theme="terminal"] .about-section {
+ background-color: #000000 !important;
+ border-color: #00ff00 !important;
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .back-button {
+ background-color: rgba(0, 255, 0, 0.1) !important;
+ border: 1px solid #00ff00 !important;
+ color: #00ff00 !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.3) !important;
+}
+
+[data-design-theme="terminal"] .back-button:hover {
+ background-color: rgba(0, 255, 0, 0.2) !important;
+ box-shadow: 0 0 10px rgba(0, 255, 0, 0.6) !important;
+}
+
+[data-design-theme="terminal"] .section-title,
+[data-design-theme="terminal"] .section-content,
+[data-design-theme="terminal"] .section-content p {
+ color: #00ff00 !important;
+}
+
+[data-design-theme="terminal"] .link {
+ color: #00ff00 !important;
+ text-decoration: underline;
+}
+
+[data-design-theme="terminal"] .link:hover {
+ color: #00ff00 !important;
+ text-shadow: 0 0 5px rgba(0, 255, 0, 0.8) !important;
+}
+
+[data-design-theme="terminal"] .version-badge {
+ background-color: rgba(0, 255, 0, 0.2) !important;
+ border: 1px solid #00ff00 !important;
+ color: #00ff00 !important;
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.3) !important;
+}
+
+[data-design-theme="terminal"] .features-list li,
+[data-design-theme="terminal"] .changelog-list li,
+[data-design-theme="terminal"] .links-list li {
+ color: #00ff00 !important;
+}
diff --git a/src/lib/components/content/FileExplorer.svelte b/src/lib/components/content/FileExplorer.svelte
index 9a9298d..dcaa3c4 100644
--- a/src/lib/components/content/FileExplorer.svelte
+++ b/src/lib/components/content/FileExplorer.svelte
@@ -1,5 +1,8 @@
@@ -346,13 +457,37 @@
- {:else if selectedFile && fileContent !== null}
+ {:else if selectedFile && (fileContent !== null || fileUrl)}
-
{fileContent}
+ {#if isImageFile(selectedFile) && fileUrl}
+
+

+
+ {:else if isVideoFile(selectedFile) && fileUrl}
+
+
+
+ {:else if isAudioFile(selectedFile) && fileUrl}
+
+ {:else if fileContent !== null}
+ {#if isCodeFile(selectedFile)}
+
{fileContent}
+ {:else}
+
{fileContent}
+ {/if}
+ {/if}
{:else}
@@ -599,16 +734,128 @@
.file-content-code {
margin: 0;
- font-family: 'Courier New', Courier, monospace;
- font-size: 0.875rem;
- line-height: 1.5;
- color: var(--fog-text, #1f2937);
- white-space: pre-wrap;
+ background: #1e1e1e !important; /* VS Code dark background, same as JSON preview */
+ border: 1px solid #3e3e3e;
+ border-radius: 4px;
+ padding: 1rem;
+ overflow-x: auto;
+ white-space: pre;
word-wrap: break-word;
}
:global(.dark) .file-content-code {
- color: var(--fog-dark-text, #f9fafb);
+ background: #1e1e1e !important;
+ border-color: #3e3e3e;
+ }
+
+ .file-content-code code {
+ display: block;
+ overflow-x: auto;
+ padding: 0;
+ background: transparent !important;
+ color: #d4d4d4; /* VS Code text color */
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Droid Sans Mono', 'Source Code Pro', monospace;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ }
+
+ .file-content-code :global(code.hljs),
+ .file-content-code :global(code.hljs *),
+ .file-content-code :global(code.hljs span),
+ .file-content-code :global(code.hljs .hljs-keyword),
+ .file-content-code :global(code.hljs .hljs-string),
+ .file-content-code :global(code.hljs .hljs-comment),
+ .file-content-code :global(code.hljs .hljs-number),
+ .file-content-code :global(code.hljs .hljs-function),
+ .file-content-code :global(code.hljs .hljs-variable),
+ .file-content-code :global(code.hljs .hljs-class),
+ .file-content-code :global(code.hljs .hljs-title),
+ .file-content-code :global(code.hljs .hljs-attr),
+ .file-content-code :global(code.hljs .hljs-tag),
+ .file-content-code :global(code.hljs .hljs-name),
+ .file-content-code :global(code.hljs .hljs-selector-id),
+ .file-content-code :global(code.hljs .hljs-selector-class),
+ .file-content-code :global(code.hljs .hljs-attribute),
+ .file-content-code :global(code.hljs .hljs-built_in),
+ .file-content-code :global(code.hljs .hljs-literal),
+ .file-content-code :global(code.hljs .hljs-type),
+ .file-content-code :global(code.hljs .hljs-property),
+ .file-content-code :global(code.hljs .hljs-operator),
+ .file-content-code :global(code.hljs .hljs-punctuation),
+ .file-content-code :global(code.hljs .hljs-meta),
+ .file-content-code :global(code.hljs .hljs-doctag),
+ .file-content-code :global(code.hljs .hljs-section),
+ .file-content-code :global(code.hljs .hljs-addition),
+ .file-content-code :global(code.hljs .hljs-deletion),
+ .file-content-code :global(code.hljs .hljs-emphasis),
+ .file-content-code :global(code.hljs .hljs-strong) {
+ text-shadow: none !important;
+ -webkit-font-smoothing: antialiased !important;
+ -moz-osx-font-smoothing: grayscale !important;
+ font-smoothing: antialiased !important;
+ text-rendering: geometricPrecision !important;
+ transform: translateZ(0);
+ backface-visibility: hidden;
+ filter: none !important;
+ will-change: auto;
+ }
+
+ .file-image-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 1rem;
+ background: var(--fog-highlight, #f3f4f6);
+ border-radius: 0.375rem;
+ border: 1px solid var(--fog-border, #e5e7eb);
+ }
+
+ :global(.dark) .file-image-container {
+ background: var(--fog-dark-highlight, #374151);
+ border-color: var(--fog-dark-border, #475569);
+ }
+
+ .file-image {
+ max-width: 100%;
+ max-height: 70vh;
+ height: auto;
+ border-radius: 0.25rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ }
+
+ :global(.dark) .file-image {
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+ }
+
+ .file-media-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 1rem;
+ background: var(--fog-highlight, #f3f4f6);
+ border-radius: 0.375rem;
+ border: 1px solid var(--fog-border, #e5e7eb);
+ }
+
+ :global(.dark) .file-media-container {
+ background: var(--fog-dark-highlight, #374151);
+ border-color: var(--fog-dark-border, #475569);
+ }
+
+ .file-video {
+ max-width: 100%;
+ max-height: 70vh;
+ border-radius: 0.25rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ }
+
+ :global(.dark) .file-video {
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+ }
+
+ .file-audio {
+ width: 100%;
+ max-width: 600px;
}
.file-content-loading,
diff --git a/src/lib/components/layout/UnifiedSearch.svelte b/src/lib/components/layout/UnifiedSearch.svelte
index 1ea56d1..620b7d3 100644
--- a/src/lib/components/layout/UnifiedSearch.svelte
+++ b/src/lib/components/layout/UnifiedSearch.svelte
@@ -43,7 +43,7 @@
let cacheProfiles: string[] = [];
// Map to track which relay each event came from
const eventRelayMap = new Map();
- const CACHE_SEARCH_DEBOUNCE = 500; // 500ms debounce for cache search
+ const CACHE_SEARCH_DEBOUNCE = 1000; // 1000ms debounce for cache search
// Clear results at start of search
function clearResults() {
@@ -1073,7 +1073,7 @@
searching = true;
searchTimeout = setTimeout(() => {
performSearch();
- }, 300);
+ }, 800);
} else {
searchResults = [];
showResults = false;
diff --git a/src/lib/components/write/CreateEventForm.svelte b/src/lib/components/write/CreateEventForm.svelte
index 9f62a68..43e215c 100644
--- a/src/lib/components/write/CreateEventForm.svelte
+++ b/src/lib/components/write/CreateEventForm.svelte
@@ -98,6 +98,7 @@
let publicationResults = $state<{ success: string[]; failed: Array<{ relay: string; error: string }> } | null>(null);
let showJsonModal = $state(false);
let showPreviewModal = $state(false);
+ let exampleJsonPreviewRef: HTMLElement | null = $state(null);
let previewContent = $state('');
let previewEvent = $state(null);
let showExampleModal = $state(false);
@@ -201,6 +202,21 @@
const exampleJSON = $derived(getExampleJSON());
+ // Highlight example JSON when it changes
+ $effect(() => {
+ if (exampleJsonPreviewRef && exampleJSON && exampleJsonPreviewRef instanceof HTMLElement) {
+ try {
+ const highlighted = hljs.highlight(exampleJSON, { language: 'json' }).value;
+ exampleJsonPreviewRef.innerHTML = highlighted;
+ exampleJsonPreviewRef.className = 'hljs language-json';
+ } catch (err) {
+ // Fallback to plain text if highlighting fails
+ exampleJsonPreviewRef.textContent = exampleJSON;
+ exampleJsonPreviewRef.className = 'language-json';
+ }
+ }
+ });
+
const isKind30040 = $derived(selectedKind === KIND.PUBLICATION_INDEX);
const isKind10895 = $derived(selectedKind === KIND.RSS_FEED);
@@ -888,7 +904,7 @@
-
{exampleJSON}
+
{exampleJSON}
-
{JSON.stringify(foundEvent, null, 2)}
+
{JSON.stringify(foundEvent, null, 2)}
+ {#if isLoggedIn && showReplyForm}
+
+ {
+ showReplyForm = false;
+ }}
+ onCancel={() => {
+ showReplyForm = false;
+ }}
+ />
+
+ {/if}
+
{getKindInfo(highlight.kind).number}
{getKindInfo(highlight.kind).description}
diff --git a/src/lib/modules/reactions/FeedReactionButtons.svelte b/src/lib/modules/reactions/FeedReactionButtons.svelte
index 19bf911..99459c6 100644
--- a/src/lib/modules/reactions/FeedReactionButtons.svelte
+++ b/src/lib/modules/reactions/FeedReactionButtons.svelte
@@ -411,7 +411,16 @@
content: ''
};
- const relays = relayManager.getReactionPublishRelays();
+ // Extract relay hints from the event being reacted to (r tags)
+ const reactionRelayHints = event.tags
+ .filter(tag => tag[0] === 'r' && tag[1])
+ .map(tag => tag[1])
+ .filter((url): url is string => {
+ // Validate it's a websocket URL
+ return typeof url === 'string' && (url.startsWith('ws://') || url.startsWith('wss://'));
+ });
+
+ const relays = relayManager.getReactionPublishRelays(reactionRelayHints);
const results = await signAndPublish(deletionEvent, relays);
publicationResults = results;
publicationModalOpen = true;
@@ -475,8 +484,17 @@
// Sign the event first to get the ID
const signedEvent = await sessionManager.signEvent(reactionEvent);
- // Publish the signed event using reaction publish relays (filters read-only relays)
- const relays = relayManager.getReactionPublishRelays();
+ // Extract relay hints from the event being reacted to (r tags)
+ const reactionRelayHints = event.tags
+ .filter(tag => tag[0] === 'r' && tag[1])
+ .map(tag => tag[1])
+ .filter((url): url is string => {
+ // Validate it's a websocket URL
+ return typeof url === 'string' && (url.startsWith('ws://') || url.startsWith('wss://'));
+ });
+
+ // Publish the signed event using reaction publish relays (includes relay hints)
+ const relays = relayManager.getReactionPublishRelays(reactionRelayHints);
const results = await nostrClient.publish(signedEvent, { relays });
publicationResults = results;
publicationModalOpen = true;
diff --git a/src/lib/services/content/git-repo-fetcher.ts b/src/lib/services/content/git-repo-fetcher.ts
index 94371ce..19dfe02 100644
--- a/src/lib/services/content/git-repo-fetcher.ts
+++ b/src/lib/services/content/git-repo-fetcher.ts
@@ -199,11 +199,12 @@ async function fetchFromGitHub(owner: string, repo: string): Promise {
+ const readmeData = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${readmeFile}?ref=${defaultBranch}`).then(r => {
if (!r.ok) throw new Error('Not found');
return r.json();
});
@@ -220,6 +221,45 @@ async function fetchFromGitHub(owner: string, repo: string): Promise 0) {
+ const readmePatterns = [/^readme\.adoc$/i, /^readme\.md$/i, /^readme\.rst$/i, /^readme\.txt$/i, /^readme$/i];
+ let readmePath: string | null = null;
+ for (const file of files) {
+ if (file.type === 'file') {
+ const fileName = file.name;
+ for (const pattern of readmePatterns) {
+ if (pattern.test(fileName)) {
+ readmePath = file.path;
+ break;
+ }
+ }
+ if (readmePath) break;
+ }
+ }
+
+ // If found in tree, fetch it
+ if (readmePath) {
+ try {
+ const readmeData = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${readmePath}?ref=${defaultBranch}`).then(r => {
+ if (!r.ok) throw new Error('Not found');
+ return r.json();
+ });
+ if (readmeData.content) {
+ const content = atob(readmeData.content.replace(/\s/g, ''));
+ const format = readmePath.toLowerCase().endsWith('.adoc') ? 'asciidoc' : 'markdown';
+ readme = {
+ path: readmePath,
+ content,
+ format
+ };
+ }
+ } catch {
+ // Failed to fetch from tree path
+ }
+ }
+ }
return {
name: repoData.name,
@@ -283,6 +323,7 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr
}
// Try to fetch README (prioritize .adoc over .md)
+ // First try root directory (most common case)
let readme: { path: string; content: string; format: 'markdown' | 'asciidoc' } | undefined;
const readmeFiles = ['README.adoc', 'README.md', 'README.rst', 'README.txt'];
for (const readmeFile of readmeFiles) {
@@ -301,6 +342,42 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr
continue; // Try next file
}
}
+
+ // If not found in root, search the file tree (case-insensitive)
+ if (!readme && files.length > 0) {
+ const readmePatterns = [/^readme\.adoc$/i, /^readme\.md$/i, /^readme\.rst$/i, /^readme\.txt$/i, /^readme$/i];
+ let readmePath: string | null = null;
+ for (const file of files) {
+ if (file.type === 'file') {
+ const fileName = file.name;
+ for (const pattern of readmePatterns) {
+ if (pattern.test(fileName)) {
+ readmePath = file.path;
+ break;
+ }
+ }
+ if (readmePath) break;
+ }
+ }
+
+ // If found in tree, fetch it
+ if (readmePath) {
+ try {
+ const fileData = await fetch(`${baseUrl}/projects/${encodedPath}/repository/files/${encodeURIComponent(readmePath)}/raw?ref=${repoData.default_branch}`).then(r => {
+ if (!r.ok) throw new Error('Not found');
+ return r.text();
+ });
+ const format = readmePath.toLowerCase().endsWith('.adoc') ? 'asciidoc' : 'markdown';
+ readme = {
+ path: readmePath,
+ content: fileData,
+ format
+ };
+ } catch {
+ // Failed to fetch from tree path
+ }
+ }
+ }
return {
name: repoData.name,
@@ -361,6 +438,7 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
}
// Try to fetch README (prioritize .adoc over .md)
+ // First try root directory (most common case)
let readme: { path: string; content: string; format: 'markdown' | 'asciidoc' } | undefined;
const readmeFiles = ['README.adoc', 'README.md', 'README.rst', 'README.txt'];
for (const readmeFile of readmeFiles) {
@@ -382,6 +460,45 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
continue; // Try next file
}
}
+
+ // If not found in root, search the file tree (case-insensitive)
+ if (!readme && files.length > 0) {
+ const readmePatterns = [/^readme\.adoc$/i, /^readme\.md$/i, /^readme\.rst$/i, /^readme\.txt$/i, /^readme$/i];
+ let readmePath: string | null = null;
+ for (const file of files) {
+ if (file.type === 'file') {
+ const fileName = file.name;
+ for (const pattern of readmePatterns) {
+ if (pattern.test(fileName)) {
+ readmePath = file.path;
+ break;
+ }
+ }
+ if (readmePath) break;
+ }
+ }
+
+ // If found in tree, fetch it
+ if (readmePath) {
+ try {
+ const fileData = await fetch(`${baseUrl}/repos/${owner}/${repo}/contents/${readmePath}?ref=${repoData.default_branch}`).then(r => {
+ if (!r.ok) throw new Error('Not found');
+ return r.json();
+ });
+ if (fileData.content) {
+ const content = atob(fileData.content.replace(/\s/g, ''));
+ const format = readmePath.toLowerCase().endsWith('.adoc') ? 'asciidoc' : 'markdown';
+ readme = {
+ path: readmePath,
+ content,
+ format
+ };
+ }
+ } catch {
+ // Failed to fetch from tree path
+ }
+ }
+ }
return {
name: repoData.name,
diff --git a/src/lib/services/nostr/relay-manager.ts b/src/lib/services/nostr/relay-manager.ts
index 7f3d4ad..705d8c2 100644
--- a/src/lib/services/nostr/relay-manager.ts
+++ b/src/lib/services/nostr/relay-manager.ts
@@ -326,9 +326,48 @@ class RelayManager {
/**
* Get relays for publishing reactions (kind 7)
+ * If reacting to an event, include relay hints from that event
+ * Includes user outbox and local relays, excludes read-only relays (aggregators)
*/
- getReactionPublishRelays(): string[] {
- return this.getPublishRelays(config.defaultRelays);
+ getReactionPublishRelays(reactionRelayHints?: string[]): string[] {
+ // Start with default relays, excluding read-only relays
+ const baseRelays = config.defaultRelays.filter(
+ relay => !config.readOnlyRelays.includes(relay)
+ );
+
+ // Get publish relays (includes user outbox and local relays)
+ let relays = this.getPublishRelays(baseRelays, true);
+
+ // If reacting to an event with relay hints, add them (but filter out read-only)
+ if (reactionRelayHints && reactionRelayHints.length > 0) {
+ const filteredHints = reactionRelayHints.filter(
+ relay => !config.readOnlyRelays.includes(relay)
+ );
+ relays = [...relays, ...filteredHints];
+ }
+
+ // Normalize and filter after combining all relays
+ relays = this.normalizeRelays(relays);
+ relays = this.filterBlocked(relays);
+
+ // Ensure we always have at least the default relays (minus aggregators) to publish to
+ if (relays.length === 0) {
+ // Fallback to default relays if we somehow ended up with an empty list
+ relays = baseRelays.length > 0 ? baseRelays : config.defaultRelays.filter(
+ relay => !config.readOnlyRelays.includes(relay)
+ );
+ }
+
+ // Log for debugging
+ console.log('[RelayManager] Reaction publish relays:', {
+ baseRelays,
+ userOutbox: this.userOutbox,
+ userLocalRelaysWrite: this.userLocalRelaysWrite,
+ reactionRelayHints,
+ finalRelays: relays
+ });
+
+ return relays;
}
/**
@@ -355,6 +394,16 @@ class RelayManager {
updateBlockedRelays(blocked: Set): void {
this.blockedRelays = blocked;
}
+
+ /**
+ * Get local relays (kind 10432) - both read and write
+ */
+ getLocalRelays(): string[] {
+ const allLocalRelays = new Set();
+ this.userLocalRelaysRead.forEach(r => allLocalRelays.add(r));
+ this.userLocalRelaysWrite.forEach(r => allLocalRelays.add(r));
+ return Array.from(allLocalRelays);
+ }
}
export const relayManager = new RelayManager();
diff --git a/src/routes/feed/relay/[relay]/+page.svelte b/src/routes/feed/relay/[relay]/+page.svelte
index 5891642..acde373 100644
--- a/src/routes/feed/relay/[relay]/+page.svelte
+++ b/src/routes/feed/relay/[relay]/+page.svelte
@@ -12,19 +12,36 @@
function decodeRelayUrl(encoded: string): string | null {
try {
- // The relay parameter is just the domain (e.g., "nostr.wine" or "theforest.nostr1.com")
- // Construct the full wss:// URL
- const domain = encoded.trim();
+ // The relay parameter might be just the domain or might include protocol and port
+ let relayUrl = encoded.trim();
- // Validate it looks like a domain
- if (!domain || domain.includes('/') || domain.includes(':')) {
+ // If it already has a protocol, use it as-is
+ if (relayUrl.startsWith('ws://') || relayUrl.startsWith('wss://')) {
+ return relayUrl;
+ }
+
+ // Check if it's prefixed with ws- to indicate ws:// protocol
+ if (relayUrl.startsWith('ws-')) {
+ relayUrl = relayUrl.substring(3); // Remove ws- prefix
+ return `ws://${relayUrl}`;
+ }
+
+ // Validate it looks like a domain (may include port)
+ if (!relayUrl || relayUrl.includes('/')) {
return null;
}
- // Construct wss:// URL (assume wss:// unless it's localhost)
- const relayUrl = domain.startsWith('localhost') || domain.startsWith('127.0.0.1')
- ? `ws://${domain}`
- : `wss://${domain}`;
+ // Check if port is included (format: hostname:port)
+ const hasPort = relayUrl.includes(':') && !relayUrl.startsWith('localhost') && !relayUrl.startsWith('127.0.0.1') && !relayUrl.startsWith('192.168.') && !relayUrl.startsWith('10.') && !relayUrl.startsWith('172.');
+ const portMatch = relayUrl.match(/^([^:]+):(\d+)$/);
+
+ // Construct URL (preserve ws:// for localhost/127.0.0.1/local IPs, use wss:// for others)
+ // Preserve port if it was in the encoded string
+ if (relayUrl.startsWith('localhost') || relayUrl.startsWith('127.0.0.1') || relayUrl.startsWith('192.168.') || relayUrl.startsWith('10.') || relayUrl.startsWith('172.')) {
+ relayUrl = `ws://${relayUrl}`;
+ } else {
+ relayUrl = `wss://${relayUrl}`;
+ }
return relayUrl;
} catch (e) {
diff --git a/src/routes/find/+page.svelte b/src/routes/find/+page.svelte
index c7cecdb..cb73a1a 100644
--- a/src/routes/find/+page.svelte
+++ b/src/routes/find/+page.svelte
@@ -147,27 +147,8 @@
{/if}
-
-
-
-
-
-
-
- {#if cacheResults.events.length > 0 || cacheResults.profiles.length > 0 || searchResults.events.length > 0 || searchResults.profiles.length > 0}
-
+ {#if cacheResults.events.length > 0 || cacheResults.profiles.length > 0 || searchResults.events.length > 0 || searchResults.profiles.length > 0}
+
Search Results
{#if cacheResults.events.length > 0 || cacheResults.profiles.length > 0}
@@ -249,7 +230,26 @@
{/if}
- {/if}
+ {/if}
+
+
@@ -325,6 +325,10 @@
gap: 1.5rem;
}
+ .find-sections.collapsed {
+ display: none;
+ }
+
@media (min-width: 640px) {
.find-sections {
gap: 2rem;
@@ -364,7 +368,8 @@
.results-section {
- margin-top: 1.5rem;
+ margin-top: 0;
+ margin-bottom: 1.5rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.5rem;
padding: 1rem;
@@ -374,7 +379,7 @@
@media (min-width: 640px) {
.results-section {
padding: 1.5rem;
- margin-top: 2rem;
+ margin-bottom: 2rem;
}
}
diff --git a/src/routes/highlights/+page.svelte b/src/routes/highlights/+page.svelte
index f2bc5b4..bc6da42 100644
--- a/src/routes/highlights/+page.svelte
+++ b/src/routes/highlights/+page.svelte
@@ -237,7 +237,7 @@
No highlights found.
{:else}
-
+
allRelays.add(r));
+ // Add local relays if user is logged in
+ const currentPubkey = sessionManager.getCurrentPubkey();
+ if (currentPubkey) {
+ // Ensure relay manager has loaded user preferences
+ await relayManager.loadUserPreferences(currentPubkey);
+ // Get local relays (read and write combined)
+ const localRelays = relayManager.getLocalRelays();
+ localRelays.forEach(r => allRelays.add(r));
+ }
+
// Get connection status from nostrClient
const connectedRelays = nostrClient.getConnectedRelays();
- const relayList: RelayInfo[] = Array.from(allRelays).map(url => ({
- url,
- categories: categorizeRelay(url),
- connected: connectedRelays.includes(url)
- }));
+ const relayList: RelayInfo[] = Array.from(allRelays).map(url => {
+ const categories = categorizeRelay(url);
+ // Check if it's a local relay
+ const isLocalRelay = currentPubkey && !config.defaultRelays.includes(url) &&
+ !config.profileRelays.includes(url) &&
+ !config.threadPublishRelays.includes(url) &&
+ !config.gifRelays.includes(url);
+ if (isLocalRelay && !categories.includes('Local')) {
+ categories.push('Local');
+ }
+ return {
+ url,
+ categories,
+ connected: connectedRelays.includes(url)
+ };
+ });
// Sort by first category, then by URL
relayList.sort((a, b) => {
@@ -100,9 +121,13 @@
let url = tag[1].trim();
// Remove trailing slash
url = url.replace(/\/$/, '');
- // If no protocol, assume wss://
+ // If no protocol, use ws:// for local addresses, wss:// for others
if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
- url = `wss://${url}`;
+ if (url.startsWith('localhost') || url.startsWith('127.0.0.1') || url.startsWith('192.168.') || url.startsWith('10.') || url.startsWith('172.')) {
+ url = `ws://${url}`;
+ } else {
+ url = `wss://${url}`;
+ }
}
favoriteRelayUrls.add(url);
}
@@ -187,18 +212,26 @@
}
function handleRelayClick(url: string) {
- // Extract hostname from relay URL (e.g., wss://thecitadel.nostr1.com -> thecitadel.nostr1.com)
- // The route expects just the domain without protocol or port
+ // Extract hostname and port from relay URL for navigation
+ // The route expects domain with optional port, preserving protocol info for ws://
let relayPath = url;
try {
const urlObj = new URL(url);
- relayPath = urlObj.hostname;
+ // Include port if present
+ relayPath = urlObj.port ? `${urlObj.hostname}:${urlObj.port}` : urlObj.hostname;
+ // If it's ws:// and not localhost, prefix with ws- to indicate ws:// protocol
+ if (urlObj.protocol === 'ws:' && !relayPath.startsWith('localhost') && !relayPath.startsWith('127.0.0.1')) {
+ relayPath = `ws-${relayPath}`;
+ }
} catch {
- // If URL parsing fails, try to extract hostname manually
- // Remove protocol (wss:// or ws://) and trailing slash
+ // If URL parsing fails, try to extract manually
+ const protocol = url.startsWith('ws://') ? 'ws://' : 'wss://';
relayPath = url.replace(/^wss?:\/\//, '').replace(/\/$/, '');
- // Remove port if present (route doesn't support ports in the parameter)
- relayPath = relayPath.split(':')[0];
+ // Keep port if present (don't split on colon if it's part of hostname:port)
+ // If it's ws:// and not localhost, prefix with ws-
+ if (protocol === 'ws://' && !relayPath.startsWith('localhost') && !relayPath.startsWith('127.0.0.1')) {
+ relayPath = `ws-${relayPath}`;
+ }
}
// Navigate to feed page with relay filter
goto(`/feed/relay/${relayPath}`);
@@ -210,12 +243,17 @@
// Validate URL format
try {
- // Try to parse as URL, add protocol if missing
- let fullUrl = url;
- if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
- fullUrl = `wss://${url}`;
- }
- new URL(fullUrl); // Validate URL format
+ // Try to parse as URL, add protocol if missing
+ let fullUrl = url;
+ if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
+ // Use ws:// for local addresses, wss:// for others
+ if (url.startsWith('localhost') || url.startsWith('127.0.0.1') || url.startsWith('192.168.') || url.startsWith('10.') || url.startsWith('172.')) {
+ fullUrl = `ws://${url}`;
+ } else {
+ fullUrl = `wss://${url}`;
+ }
+ }
+ new URL(fullUrl); // Validate URL format
// Navigate to the relay
handleRelayClick(fullUrl);
@@ -308,7 +346,7 @@
{/if}
- {#each ['Default', 'Profile', 'Thread Publish', 'GIF', 'Other'] as category}
+ {#each ['Local', 'Default', 'Profile', 'Thread Publish', 'GIF', 'Other'] as category}
{@const categoryRelays = relays.filter(r => r.categories.includes(category))}
{#if categoryRelays.length > 0}
diff --git a/src/routes/repos/+page.svelte b/src/routes/repos/+page.svelte
index 7b6b400..a345b4a 100644
--- a/src/routes/repos/+page.svelte
+++ b/src/routes/repos/+page.svelte
@@ -11,12 +11,17 @@
import { goto } from '$app/navigation';
import { getRecentCachedEvents } from '../../lib/services/cache/event-cache.js';
import { KIND } from '../../lib/types/kind-lookup.js';
+ import { sessionManager } from '../../lib/services/auth/session-manager.js';
let repos = $state([]);
let loading = $state(true);
let filterResult = $state<{ type: 'event' | 'pubkey' | 'text' | null; value: string | null }>({ type: null, value: null });
let searchResults = $state<{ events: NostrEvent[]; profiles: string[] }>({ events: [], profiles: [] });
let unifiedSearchComponent: { triggerSearch: () => void } | null = $state(null);
+ let searchQuery = $state('');
+ let showMyRepos = $state(false);
+ const isLoggedIn = $derived(sessionManager.isLoggedIn());
+ const currentPubkey = $derived(sessionManager.getCurrentPubkey());
function handleFilterChange(result: { type: 'event' | 'pubkey' | 'text' | null; value: string | null }) {
filterResult = result;
@@ -68,36 +73,38 @@
}
async function loadRepos() {
- // Only show loading spinner if we don't have cached repos
+ // Check cache first before making any network requests
+ // If we already have cached repos, we'll enhance with fresh data in the background
const hasCachedRepos = repos.length > 0;
+
+ // Only show loading spinner if we don't have cached repos
if (!hasCachedRepos) {
loading = true;
}
+
try {
const relays = relayManager.getProfileReadRelays();
- // Fetch repo announcement events
- const allRepos: NostrEvent[] = [];
-
- // Stream fresh data from relays (progressive enhancement)
+ // Fetch repo announcement events with cache-first strategy
+ // This will check cache before making network requests
const events = await nostrClient.fetchEvents(
[{ kinds: [KIND.REPO_ANNOUNCEMENT], limit: 100 }],
relays,
{
- useCache: 'cache-first', // Already shown cache above, now stream updates
- cacheResults: true,
+ useCache: 'cache-first', // Check cache first, then fetch from relays if needed
+ cacheResults: true, // Cache any new results
onUpdate: (newRepos) => {
- // Merge with existing repos as they stream in
+ // Merge with existing repos as they stream in (progressive enhancement)
const reposByKey = new Map();
- // Add existing repos first
+ // Add existing repos first (from cache)
for (const repo of repos) {
const dTag = repo.tags.find(t => t[0] === 'd')?.[1] || '';
const key = `${repo.pubkey}:${dTag}`;
reposByKey.set(key, repo);
}
- // Add/update with new repos
+ // Add/update with new repos from network
for (const event of newRepos) {
const dTag = event.tags.find(t => t[0] === 'd')?.[1] || '';
const key = `${event.pubkey}:${dTag}`;
@@ -112,7 +119,7 @@
}
);
- // Final merge of any remaining events
+ // Final merge of any remaining events (in case onUpdate didn't catch them all)
const reposByKey = new Map();
// Add existing cached repos first
@@ -122,7 +129,7 @@
reposByKey.set(key, repo);
}
- // Add/update with new repos
+ // Add/update with new repos from network
for (const event of events) {
const dTag = event.tags.find(t => t[0] === 'd')?.[1] || '';
const key = `${event.pubkey}:${dTag}`;
@@ -135,8 +142,11 @@
// Sort by created_at descending
repos = Array.from(reposByKey.values()).sort((a, b) => b.created_at - a.created_at);
} catch (error) {
- // Failed to load repos
- repos = [];
+ // Failed to load repos - but we might still have cached data
+ // Only clear repos if we don't have any cached data
+ if (!hasCachedRepos) {
+ repos = [];
+ }
} finally {
loading = false;
}
@@ -213,29 +223,193 @@
}
}
- // Filter repos based on search query and pubkey filter
- let filteredRepos = $derived.by(() => {
- let filtered = repos;
+ // Helper functions to extract repo data
+ function getCloneUrls(event: NostrEvent): string[] {
+ if (!Array.isArray(event.tags)) return [];
+ return event.tags
+ .filter(t => Array.isArray(t) && t[0] === 'clone' && t[1])
+ .map(t => String(t[1]));
+ }
+
+ function getWebUrls(event: NostrEvent): string[] {
+ if (!Array.isArray(event.tags)) return [];
+ return event.tags
+ .filter(t => Array.isArray(t) && t[0] === 'web' && t[1])
+ .map(t => String(t[1]));
+ }
+
+ function getMaintainers(event: NostrEvent): string[] {
+ if (!Array.isArray(event.tags)) return [];
+ const maintainerTag = event.tags.find(t => Array.isArray(t) && t[0] === 'maintainers');
+ if (maintainerTag && maintainerTag.length > 1) {
+ return maintainerTag.slice(1).filter(m => m && typeof m === 'string') as string[];
+ }
+ return [];
+ }
+
+ function getDTagFromEvent(event: NostrEvent): string {
+ if (!Array.isArray(event.tags)) return '';
+ const dTag = event.tags.find(t => Array.isArray(t) && t[0] === 'd');
+ return dTag?.[1] || '';
+ }
+
+
+ // Decode bech32 pubkey (npub or nprofile) to hex
+ function decodePubkeyToHex(input: string): string | null {
+ if (!input || input.trim() === '') return null;
+
+ const trimmed = input.trim();
- // Filter by pubkey if provided
- if (filterResult.value && filterResult.type === 'pubkey') {
- const normalizedPubkey = filterResult.value.toLowerCase();
- if (/^[a-f0-9]{64}$/i.test(normalizedPubkey)) {
- filtered = filtered.filter(repo => repo.pubkey.toLowerCase() === normalizedPubkey);
+ // If it's already hex (64 chars), return as-is
+ if (/^[a-f0-9]{64}$/i.test(trimmed)) {
+ return trimmed.toLowerCase();
+ }
+
+ // Try to decode bech32
+ try {
+ if (trimmed.startsWith('npub')) {
+ const decoded = nip19.decode(trimmed);
+ if (decoded.type === 'npub') {
+ return decoded.data;
+ }
+ } else if (trimmed.startsWith('nprofile')) {
+ const decoded = nip19.decode(trimmed);
+ if (decoded.type === 'nprofile') {
+ return decoded.data.pubkey;
+ }
+ }
+ } catch (e) {
+ // Not a valid bech32
+ }
+
+ return null;
+ }
+
+ // Decode bech32 event ID (nevent, naddr, or note) to hex
+ function decodeEventIdToHex(input: string): string | null {
+ if (!input || input.trim() === '') return null;
+
+ const trimmed = input.trim();
+
+ // If it's already hex (64 chars), return as-is
+ if (/^[a-f0-9]{64}$/i.test(trimmed)) {
+ return trimmed.toLowerCase();
+ }
+
+ // Try to decode bech32
+ try {
+ if (trimmed.startsWith('nevent')) {
+ const decoded = nip19.decode(trimmed);
+ if (decoded.type === 'nevent') {
+ return decoded.data.id;
+ }
+ } else if (trimmed.startsWith('naddr')) {
+ // naddr doesn't have an event ID directly, but we can check if it matches
+ const decoded = nip19.decode(trimmed);
+ if (decoded.type === 'naddr') {
+ // Return the naddr as-is for matching
+ return trimmed;
+ }
+ } else if (trimmed.startsWith('note')) {
+ const decoded = nip19.decode(trimmed);
+ if (decoded.type === 'note') {
+ return decoded.data;
+ }
}
+ } catch (e) {
+ // Not a valid bech32
}
- // Filter by text search if provided
- if (filterResult.value && filterResult.type === 'text') {
- const query = filterResult.value.toLowerCase();
+ return null;
+ }
+
+ // Filter repos based on search query and filters
+ let filteredRepos = $derived.by(() => {
+ let filtered = repos;
+
+ // Filter by "See my repos" checkbox
+ if (showMyRepos && currentPubkey) {
filtered = filtered.filter(repo => {
- const name = getRepoName(repo).toLowerCase();
- const desc = getRepoDescription(repo).toLowerCase();
- return name.includes(query) || desc.includes(query);
+ // Check if repo owner matches
+ if (repo.pubkey.toLowerCase() === currentPubkey.toLowerCase()) {
+ return true;
+ }
+ // Check if user is a maintainer
+ const maintainers = getMaintainers(repo);
+ return maintainers.some(m => m.toLowerCase() === currentPubkey.toLowerCase());
+ });
+ }
+
+ // If no search query, return filtered list
+ if (!searchQuery.trim()) {
+ return filtered;
+ }
+
+ const query = searchQuery.trim().toLowerCase();
+
+ // Try to decode as pubkey (hex, npub, or nprofile)
+ const decodedPubkey = decodePubkeyToHex(query);
+ if (decodedPubkey) {
+ return filtered.filter(repo => {
+ // Match by owner pubkey
+ if (repo.pubkey.toLowerCase() === decodedPubkey) {
+ return true;
+ }
+ // Match by maintainer pubkey
+ const maintainers = getMaintainers(repo);
+ return maintainers.some(m => m.toLowerCase() === decodedPubkey);
+ });
+ }
+
+ // Try to decode as event ID (hex, note, nevent, or naddr)
+ const decodedEventId = decodeEventIdToHex(query);
+ if (decodedEventId) {
+ return filtered.filter(repo => {
+ // Match by event ID
+ if (repo.id.toLowerCase() === decodedEventId.toLowerCase()) {
+ return true;
+ }
+ // Match by naddr
+ const naddr = getNaddr(repo);
+ if (naddr && naddr.toLowerCase() === decodedEventId.toLowerCase()) {
+ return true;
+ }
+ return false;
});
}
- return filtered;
+ // Text search across all fields
+ return filtered.filter(repo => {
+ // Search in name
+ const name = getRepoName(repo).toLowerCase();
+ if (name.includes(query)) return true;
+
+ // Search in description
+ const desc = getRepoDescription(repo).toLowerCase();
+ if (desc.includes(query)) return true;
+
+ // Search in clone URLs
+ const cloneUrls = getCloneUrls(repo);
+ if (cloneUrls.some(url => url.toLowerCase().includes(query))) return true;
+
+ // Search in web URLs
+ const webUrls = getWebUrls(repo);
+ if (webUrls.some(url => url.toLowerCase().includes(query))) return true;
+
+ // Search in d-tag
+ const dTag = getDTagFromEvent(repo).toLowerCase();
+ if (dTag.includes(query)) return true;
+
+ // Search in naddr
+ const naddr = getNaddr(repo);
+ if (naddr && naddr.toLowerCase().includes(query)) return true;
+
+ // Search in maintainer pubkeys (as hex)
+ const maintainers = getMaintainers(repo);
+ if (maintainers.some(m => m.toLowerCase().includes(query))) return true;
+
+ return false;
+ });
});
@@ -244,20 +418,31 @@
@@ -334,6 +519,20 @@
{getRepoName(repo)}
Kind {repo.kind}
+
+ {#if getMaintainers(repo).length > 0}
+
+
Maintainers:
+
+ {#each getMaintainers(repo) as maintainerPubkey}
+
+ {/each}
+
+
+ {/if}
{#if getRepoDescription(repo)}
{getRepoDescription(repo)}
{/if}
@@ -365,8 +564,79 @@
border-bottom-color: var(--fog-dark-border, #374151);
}
+ .repos-header-top {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 1rem;
+ margin-bottom: 1rem;
+ }
+
+ .my-repos-checkbox {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ cursor: pointer;
+ padding: 0.5rem 1rem;
+ border: 1px solid var(--fog-border, #e5e7eb);
+ border-radius: 0.25rem;
+ background: var(--fog-post, #ffffff);
+ color: var(--fog-text, #1e293b);
+ white-space: nowrap;
+ user-select: none;
+ transition: all 0.2s;
+ }
+
+ .my-repos-checkbox:hover {
+ background: var(--fog-highlight, #f1f5f9);
+ border-color: var(--fog-accent, #94a3b8);
+ }
+
+ :global(.dark) .my-repos-checkbox {
+ background: var(--fog-dark-post, #334155);
+ border-color: var(--fog-dark-border, #475569);
+ color: var(--fog-dark-text, #f1f5f9);
+ }
+
+ :global(.dark) .my-repos-checkbox:hover {
+ background: var(--fog-dark-highlight, #475569);
+ border-color: var(--fog-dark-accent, #64748b);
+ }
+
+ .checkbox-input {
+ cursor: pointer;
+ }
+
.search-container {
- max-width: 500px;
+ max-width: 100%;
+ }
+
+ .repo-search-input {
+ width: 100%;
+ padding: 0.75rem;
+ border: 1px solid var(--fog-border, #e5e7eb);
+ border-radius: 0.375rem;
+ background: var(--fog-post, #ffffff);
+ color: var(--fog-text, #1e293b);
+ font-size: 1rem;
+ transition: all 0.2s;
+ }
+
+ .repo-search-input:focus {
+ outline: none;
+ border-color: var(--fog-accent, #64748b);
+ box-shadow: 0 0 0 3px rgba(100, 116, 139, 0.1);
+ }
+
+ :global(.dark) .repo-search-input {
+ background: var(--fog-dark-post, #334155);
+ border-color: var(--fog-dark-border, #475569);
+ color: var(--fog-dark-text, #f9fafb);
+ }
+
+ :global(.dark) .repo-search-input:focus {
+ border-color: var(--fog-dark-accent, #94a3b8);
+ box-shadow: 0 0 0 3px rgba(148, 163, 184, 0.1);
}
.loading-state,
@@ -449,6 +719,48 @@
background: var(--fog-dark-highlight, #374151);
}
+ .repo-owner {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin: 0.75rem 0;
+ font-size: 0.875rem;
+ }
+
+ .repo-owner-label {
+ color: var(--fog-text-light, #52667a);
+ font-weight: 500;
+ }
+
+ :global(.dark) .repo-owner-label {
+ color: var(--fog-dark-text-light, #a8b8d0);
+ }
+
+ .repo-maintainers {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5rem;
+ margin: 0.75rem 0;
+ font-size: 0.875rem;
+ }
+
+ .repo-maintainers-label {
+ color: var(--fog-text-light, #52667a);
+ font-weight: 500;
+ flex-shrink: 0;
+ }
+
+ :global(.dark) .repo-maintainers-label {
+ color: var(--fog-dark-text-light, #a8b8d0);
+ }
+
+ .repo-maintainers-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ align-items: center;
+ }
+
.repo-description {
color: var(--fog-text-light, #52667a);
margin: 0.5rem 0;
diff --git a/src/routes/repos/[naddr]/+page.svelte b/src/routes/repos/[naddr]/+page.svelte
index 1a73af1..5852bd7 100644
--- a/src/routes/repos/[naddr]/+page.svelte
+++ b/src/routes/repos/[naddr]/+page.svelte
@@ -60,9 +60,9 @@
}
});
- // Load git repo when repository tab is clicked
+ // Load git repo when repository or about tab is clicked (about tab needs README)
$effect(() => {
- if (activeTab === 'repository' && repoEvent && !gitRepo && !loadingGitRepo && !gitRepoFetchAttempted) {
+ if ((activeTab === 'repository' || activeTab === 'about') && repoEvent && !gitRepo && !loadingGitRepo && !gitRepoFetchAttempted) {
loadGitRepo();
}
});
diff --git a/src/routes/rss/+page.svelte b/src/routes/rss/+page.svelte
index 8fa1b4b..85699cc 100644
--- a/src/routes/rss/+page.svelte
+++ b/src/routes/rss/+page.svelte
@@ -415,8 +415,19 @@
{#if rssEvent}