From 1063079b8d054048e2aa2ae05e2751d3447ca37a Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 21 Feb 2026 16:48:22 +0100 Subject: [PATCH] refactor Nostr-Signature: 62b813f817173c9e35eb05088240f7ec50ecab697c8c6d4a5c19d47664ef3837 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc ca9c70fc7bf8b1bb1726461bb843127d1bddc4de96652cfc7497698a3f5c4dc4a8c3f5a7a240710db77afabeee2a3b7d594f75f42a0a8b28aeeef50f66b506c9 --- nostr/commit-signatures.jsonl | 1 + src/lib/components/RepoHeaderEnhanced.svelte | 506 ++--- src/lib/components/RepoTabs.svelte | 177 +- src/lib/components/UserBadge.svelte | 34 +- src/lib/services/git/repo-manager.ts | 2 +- src/lib/styles/components.css | 737 +++++++ src/lib/styles/repo.css | 1879 +++++++++++++++++ src/routes/repos/+page.svelte | 3 +- src/routes/repos/[npub]/[repo]/+page.svelte | 1924 +----------------- src/routes/users/[npub]/+page.svelte | 621 +++++- static/icons/git-commit.svg | 5 + static/icons/more-vertical.svg | 5 + static/icons/tag.svg | 4 + 13 files changed, 3355 insertions(+), 2543 deletions(-) create mode 100644 src/lib/styles/components.css create mode 100644 src/lib/styles/repo.css create mode 100644 static/icons/git-commit.svg create mode 100644 static/icons/more-vertical.svg create mode 100644 static/icons/tag.svg diff --git a/nostr/commit-signatures.jsonl b/nostr/commit-signatures.jsonl index 66e1d7b..9c62fa2 100644 --- a/nostr/commit-signatures.jsonl +++ b/nostr/commit-signatures.jsonl @@ -41,3 +41,4 @@ {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771669826,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","user badge is a universal hyperlink to the profile page"]],"content":"Signed commit: user badge is a universal hyperlink to the profile page","id":"973a406714e586037d81cca323024ff5e2cc1fbaeda8846f6f2994c3829c4fe0","sig":"e7a58526a3786fc1b9ab1f957c87c13a42d3c2cc95effcf4ce4f4710e01ecc45fcff3ca542c5fa223961d7b99fe336a2851c133aebe3bfc1a591ffe1c34b221a"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771680916,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix profile feeds"]],"content":"Signed commit: fix profile feeds","id":"33f33d76f6c79e68fdab72c8fdfc7e1f0ecc53a879a7f5aef02481f17384a06f","sig":"8f9056eab081d66edb693eb35a2e400368aa897746b97ca40a216604dc14ee877eb7f4f16dd2eeac257025b3adfe82e23734c7c106b6cec5e8a1ca661c872cc5"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771681068,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","remove landing page search bar"]],"content":"Signed commit: remove landing page search bar","id":"71087b100ce14a1f2eb975be23450c62143ee11a8fd0429ec7440bfea1751741","sig":"695c704503ed1397f6871770ad55822a17a503bcfb71a0db7b3f2477cacb0e767b9f122075b0216f884e41e175c0ac9f9e3d743086a2aa34db4aa1207c900703"} +{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771682804,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","repo page refactor"]],"content":"Signed commit: repo page refactor","id":"9ad7610ff7aa61d62d3772d6ae7c0589cda8ff95cd7a60b81c84ba879e0f9d8a","sig":"8918f36d426d352a6787543daaa044cf51855632e2257f29cc18bb87db31d61c877b525113e21045d3bc135376e1c0574454e28bd409d3135bcb80079bc11947"} diff --git a/src/lib/components/RepoHeaderEnhanced.svelte b/src/lib/components/RepoHeaderEnhanced.svelte index a28526d..f975b4a 100644 --- a/src/lib/components/RepoHeaderEnhanced.svelte +++ b/src/lib/components/RepoHeaderEnhanced.svelte @@ -1,6 +1,7 @@
@@ -107,16 +111,91 @@
{#if userPubkey} - + {/if}
@@ -127,8 +206,57 @@
- Owner: - + + {#if showOwnerMenu && (allMaintainers.length > 0 || ownerPubkey)} +
showOwnerMenu = false} + onkeydown={(e) => { + if (e.key === 'Escape') { + showOwnerMenu = false; + } + }} + role="button" + tabindex="0" + aria-label="Close menu" + >
+
+
Owners & Maintainers
+
+ {#if allMaintainers.length > 0} + {#each allMaintainers as maintainer} +
+ + {#if maintainer.isOwner} + Owner + {:else} + Maintainer + {/if} +
+ {/each} + {:else} +
+ + Owner +
+ {/if} +
+
+ {/if}
{#if cloneUrls.length > 0} @@ -214,356 +342,4 @@ {/if}
- {#if showMoreMenu && userPubkey} -
showMoreMenu = false} - onkeydown={(e) => { - if (e.key === 'Escape') { - showMoreMenu = false; - } - }} - role="button" - tabindex="0" - aria-label="Close menu" - >
-
- {#if onFork} - - {/if} - {#if onCreateIssue} - - {/if} - {#if onCreatePR} - - {/if} - {#if onCreatePatch} - - {/if} - {#if hasUnlimitedAccess && (isRepoCloned === false || isRepoCloned === null) && onCloneToServer} - - {/if} - {#if isMaintainer && onSettings} - - {/if} - {#if onGenerateVerification} - - {/if} - {#if onDeleteAnnouncement} - - {/if} - {#if isMaintainer && onCreateBranch} - - {/if} -
- {/if}
- - diff --git a/src/lib/components/RepoTabs.svelte b/src/lib/components/RepoTabs.svelte index 87063c3..f6d5684 100644 --- a/src/lib/components/RepoTabs.svelte +++ b/src/lib/components/RepoTabs.svelte @@ -1,4 +1,5 @@ - - diff --git a/src/lib/components/UserBadge.svelte b/src/lib/components/UserBadge.svelte index 3a1c659..e35e7b2 100644 --- a/src/lib/components/UserBadge.svelte +++ b/src/lib/components/UserBadge.svelte @@ -9,9 +9,10 @@ interface Props { pubkey: string; disableLink?: boolean; + inline?: boolean; } - let { pubkey, disableLink = false }: Props = $props(); + let { pubkey, disableLink = false, inline = false }: Props = $props(); // Convert pubkey to npub for navigation (reactive) const profileUrl = $derived.by(() => { @@ -183,7 +184,21 @@ } -{#if disableLink} +{#if inline} + {#if disableLink} + {truncateHandle(userProfile?.name) || getShortNpub()} + {:else} + { + e.stopPropagation(); + }} + > + {truncateHandle(userProfile?.name) || getShortNpub()} + + {/if} +{:else if disableLink}
{#if userProfile?.picture} Profile @@ -249,6 +264,18 @@ white-space: nowrap; } + .user-badge-inline { + display: inline; + color: var(--accent); + text-decoration: none; + font-weight: 500; + font-size: inherit; + } + + .user-badge-inline:hover { + text-decoration: underline; + } + /* Hide name on narrow screens, show only picture */ @media (max-width: 768px) { .user-badge-name { @@ -257,6 +284,9 @@ .user-badge { padding: 0.25rem; + width: fit-content; + display: inline-flex; + flex-shrink: 0; } } diff --git a/src/lib/services/git/repo-manager.ts b/src/lib/services/git/repo-manager.ts index c4c79e8..0e89f85 100644 --- a/src/lib/services/git/repo-manager.ts +++ b/src/lib/services/git/repo-manager.ts @@ -3,7 +3,7 @@ * Handles repo provisioning, syncing, and NIP-34 integration */ -import { existsSync, mkdirSync, writeFileSync, statSync, readFileSync } from 'fs'; +import { existsSync, mkdirSync, statSync } from 'fs'; import { join } from 'path'; import { readdir, readFile } from 'fs/promises'; import { spawn } from 'child_process'; diff --git a/src/lib/styles/components.css b/src/lib/styles/components.css new file mode 100644 index 0000000..3f342f9 --- /dev/null +++ b/src/lib/styles/components.css @@ -0,0 +1,737 @@ +/* Component Styles - Shared across components */ + +/* RepoHeaderEnhanced Component */ +.repo-header { + padding: 0.75rem 1rem; + background: var(--card-bg, #ffffff); + border-bottom: 1px solid var(--border-color, #e0e0e0); + position: sticky; + top: 0; + z-index: 100; +} + +@media (max-width: 768px) { + .repo-header { + padding: 0.5rem 0.75rem; + } +} + +.repo-header-top { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 0.5rem; +} + +@media (max-width: 768px) { + .repo-header-top { + margin-bottom: 0.25rem; + gap: 0.5rem; + } +} + +.repo-title-section { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.repo-name { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: var(--text-primary, #1a1a1a); + word-break: break-word; +} + +.repo-badge { + display: inline-block; + padding: 0.125rem 0.5rem; + font-size: 0.75rem; + border-radius: 0.25rem; + font-weight: 500; +} + +.repo-badge.private { + background: var(--error-bg, #fee); + color: var(--error-text, #c00); +} + +.bookmark-button { + padding: 0.25rem; + background: transparent; + border: none; + cursor: pointer; + display: flex; + align-items: center; +} + +.bookmark-button.bookmarked img { + filter: brightness(0) saturate(100%) invert(67%) sepia(93%) saturate(1352%) hue-rotate(358deg) brightness(102%) contrast(106%); +} + +.repo-header-actions { + display: flex; + gap: 0.5rem; + flex-shrink: 0; +} + +.menu-button-wrapper { + position: relative; +} + +.menu-button, +.clone-button, +.branch-button, +.copy-clone-button { + position: relative; + padding: 0.5rem; + background: transparent; + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 0.375rem; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.875rem; + color: var(--text-primary, #1a1a1a); + transition: all 0.2s ease; +} + +.menu-button:hover, +.clone-button:hover, +.branch-button:hover, +.copy-clone-button:hover { + background: var(--bg-secondary, #f5f5f5); + border-color: var(--accent, #007bff); +} + +.icon { + width: 18px; + height: 18px; + flex-shrink: 0; + /* Theme-aware icon colors */ + filter: brightness(0) saturate(100%) invert(1); /* Default white for dark themes */ + opacity: 1; +} + +/* Light theme: black icon */ +:global([data-theme="light"]) .icon { + filter: brightness(0) saturate(100%); /* Black in light theme */ + opacity: 1; +} + +/* Dark themes: white icon */ +:global([data-theme="dark"]) .icon, +:global([data-theme="black"]) .icon { + filter: brightness(0) saturate(100%) invert(1); /* White in dark themes */ + opacity: 1; +} + +.repo-description { + margin: 0.5rem 0; + font-size: 0.875rem; + color: var(--text-secondary, #666); + line-height: 1.5; +} + +@media (max-width: 768px) { + .repo-description { + margin: 0.25rem 0; + } +} + +.repo-meta { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: center; + margin-top: 0.75rem; + font-size: 0.875rem; +} + +@media (max-width: 768px) { + .repo-meta { + margin-top: 0.5rem; + gap: 0.75rem; + } +} + +.repo-owner { + position: relative; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.owner-badge-button { + display: flex; + align-items: center; + gap: 0.5rem; + background: transparent; + border: none; + cursor: pointer; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + transition: background 0.2s ease; + color: inherit; + font-size: inherit; +} + +.owner-badge-button:hover { + background: var(--bg-secondary, #f5f5f5); +} + +.owner-badge-button .meta-label { + color: var(--text-secondary, #666); +} + +.owner-badge-count { + padding: 0.125rem 0.375rem; + background: var(--bg-secondary, #f5f5f5); + border-radius: 0.75rem; + font-size: 0.75rem; + font-weight: 500; + color: var(--text-secondary, #666); +} + +.owner-menu-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 99; +} + +.owner-menu { + position: absolute; + top: 100%; + left: 0; + margin-top: 0.25rem; + background: var(--card-bg, #ffffff); + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 0.375rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + z-index: 100; + min-width: 250px; + max-width: 400px; +} + +.owner-menu-header { + padding: 0.75rem 1rem; + font-weight: 600; + font-size: 0.875rem; + color: var(--text-primary, #1a1a1a); + border-bottom: 1px solid var(--border-color, #e0e0e0); +} + +.owner-menu-list { + max-height: 300px; + overflow-y: auto; +} + +.owner-menu-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border-color, #e0e0e0); + transition: background 0.2s ease; +} + +.owner-menu-item:last-child { + border-bottom: none; +} + +.owner-menu-item:hover { + background: var(--bg-secondary, #f5f5f5); +} + +.owner-menu-item :global(.user-badge) { + flex: 1; + min-width: 0; +} + +.owner-menu-item :global(a.user-badge) { + text-decoration: none; + color: inherit; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.owner-menu-item :global(a.user-badge:hover) { + text-decoration: none; + opacity: 0.8; +} + +.owner-menu-badge { + padding: 0.125rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 500; + flex-shrink: 0; +} + +.owner-menu-badge.owner { + background: var(--accent-bg, #e7f3ff); + color: var(--accent, #007bff); +} + +.owner-menu-badge.maintainer { + background: var(--bg-secondary, #f5f5f5); + color: var(--text-secondary, #666); +} + +.meta-label { + color: var(--text-secondary, #666); +} + +.repo-clone, +.repo-branch { + position: relative; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.clone-menu, +.branch-menu { + position: absolute; + top: 100%; + left: 0; + margin-top: 0.25rem; + background: var(--card-bg, #ffffff); + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 0.375rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + z-index: 10; + min-width: 200px; + max-width: 90vw; + max-height: 300px; + overflow-y: auto; + overflow-x: hidden; +} + +@media (max-width: 768px) { + .clone-menu { + max-width: calc(100vw - 1.5rem); + min-width: min(200px, calc(100vw - 1.5rem)); + } + + .clone-url-item { + font-size: 0.8125rem; + padding: 0.5rem; + } +} + +.clone-url-item, +.branch-item { + display: block; + width: 100%; + padding: 0.5rem 0.75rem; + text-align: left; + background: transparent; + border: none; + border-bottom: 1px solid var(--border-color, #e0e0e0); + cursor: pointer; + font-size: 0.875rem; + color: var(--text-primary, #1a1a1a); + word-break: break-all; + overflow-wrap: break-word; + white-space: normal; + line-height: 1.4; + box-sizing: border-box; + overflow: hidden; + word-wrap: break-word; +} + +.branch-item { + display: flex; + justify-content: space-between; + align-items: center; + word-break: normal; +} + +.branch-item.active { + background: var(--bg-secondary, #f5f5f5); + font-weight: 600; +} + +.branch-badge { + font-size: 0.75rem; + padding: 0.125rem 0.375rem; + background: var(--bg-secondary, #f5f5f5); + border-radius: 0.25rem; + color: var(--text-secondary, #666); +} + +.clone-url-item:last-child, +.branch-item:last-child { + border-bottom: none; +} + +.clone-url-item:hover, +.branch-item:hover { + background: var(--bg-secondary, #f5f5f5); +} + +.delete-branch-button { + padding: 0.25rem 0.5rem; + background: var(--error-text, #dc2626); + color: #ffffff; + border: none; + border-radius: 0.25rem; + cursor: pointer; + font-size: 0.875rem; +} + +.delete-branch-button:hover { + background: var(--error-hover, #c82333); +} + +.more-menu-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 99; +} + +.more-menu { + position: absolute; + top: calc(100% + 0.25rem); + right: 0; + background: var(--card-bg, #ffffff); + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 0.375rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + z-index: 100; + min-width: 240px; + max-width: min(90vw, 360px); +} + +/* On mobile, ensure menu doesn't overflow screen */ +@media (max-width: 768px) { + .more-menu { + right: 0; + max-width: calc(100vw - 2rem); + min-width: 220px; + } +} + +/* On very small screens, position menu to not overflow */ +@media (max-width: 480px) { + .more-menu { + right: 0; + max-width: calc(100vw - 1rem); + min-width: auto; + } +} + +.menu-item { + display: block; + width: 100%; + padding: 0.5rem 0.75rem; + text-align: left; + background: transparent; + border: none; + border-bottom: 1px solid var(--border-color, #e0e0e0); + cursor: pointer; + font-size: 0.875rem; + color: var(--text-primary, #1a1a1a); +} + +.menu-item:last-child { + border-bottom: none; +} + +.menu-item:hover:not(:disabled) { + background: var(--bg-secondary, #f5f5f5); +} + +.menu-item:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.menu-item-danger { + color: var(--error-text, #dc2626); +} + +.menu-item-danger:hover:not(:disabled) { + background: var(--error-bg, #fee); +} + +/* Icon button - icon-only button style */ +.icon-button { + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + min-width: auto; + width: auto; +} + +.icon-button .icon { + width: 18px; + height: 18px; + margin: 0; +} + +@media (min-width: 768px) { + .repo-header { + padding: 1rem 1.5rem; + } + + .repo-name { + font-size: 1.5rem; + } + + .repo-description { + font-size: 1rem; + } +} + +/* RepoTabs Component */ +.repo-tabs { + position: relative; + background: var(--card-bg, #ffffff); + border-bottom: 1px solid var(--border-color, #e0e0e0); + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.mobile-tabs-menu-button { + display: none; + align-items: center; + justify-content: flex-start; + gap: 0.5rem; + padding: 0.75rem 1rem; + width: 100%; + background: transparent; + border: none; + border-bottom: 1px solid var(--border-color, #e0e0e0); + cursor: pointer; + font-size: 0.875rem; + color: var(--text-primary, #1a1a1a); + font-weight: 500; +} + +.mobile-tabs-menu-button .icon { + width: 20px; + height: 20px; + flex-shrink: 0; +} + +.current-tab-label { + flex: 1; + text-align: left; +} + +.tabs-container { + display: none; + gap: 0; +} + +.tab-button { + padding: 0.75rem 1rem; + background: transparent; + border: none; + border-bottom: 2px solid transparent; + cursor: pointer; + font-size: 0.875rem; + color: var(--text-secondary, #666); + display: flex; + align-items: center; + gap: 0.5rem; + white-space: nowrap; + transition: all 0.2s ease; + position: relative; +} + +.tab-button:hover { + color: var(--text-primary, #1a1a1a); + background: var(--bg-secondary, #f5f5f5); +} + +.tab-button.active { + color: var(--accent, #007bff); + border-bottom-color: var(--accent, #007bff); + font-weight: 600; +} + +.tab-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + /* Theme-aware icon colors */ + filter: brightness(0) saturate(100%) invert(1); /* Default white for dark themes */ + opacity: 1; +} + +/* Light theme: black icon */ +:global([data-theme="light"]) .tab-icon { + filter: brightness(0) saturate(100%); /* Black in light theme */ + opacity: 1; +} + +/* Dark themes: white icon */ +:global([data-theme="dark"]) .tab-icon, +:global([data-theme="black"]) .tab-icon { + filter: brightness(0) saturate(100%) invert(1); /* White in dark themes */ + opacity: 1; +} + +/* Active tab: match text color (accent color) */ +.tab-button.active .tab-icon { + filter: brightness(0) saturate(100%) invert(48%) sepia(79%) saturate(2476%) hue-rotate(200deg) brightness(118%) contrast(119%); + opacity: 1; +} + +.tab-label { + display: none; +} + +.tab-count { + padding: 0.125rem 0.375rem; + background: var(--bg-secondary, #f5f5f5); + border-radius: 0.75rem; + font-size: 0.75rem; + font-weight: 500; + color: var(--text-secondary, #666); +} + +.tab-button.active .tab-count { + background: var(--accent, #007bff); + color: var(--accent-text, #ffffff); +} + +.mobile-menu-button { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + width: 100%; + background: transparent; + border: none; + border-bottom: 1px solid var(--border-color, #e0e0e0); + cursor: pointer; + font-size: 0.875rem; + color: var(--text-primary, #1a1a1a); + font-weight: 500; +} + +.mobile-menu-button .icon { + width: 20px; + height: 20px; +} + +@media (max-width: 480px) { + .mobile-menu-button .current-tab-label { + display: none; + } + + .mobile-menu-button { + justify-content: center; + } +} + +.mobile-menu-button .icon { + width: 18px; + height: 18px; +} + +.current-tab-label { + flex: 1; + text-align: left; +} + +.mobile-tabs-menu { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: var(--card-bg, #ffffff); + border-bottom: 1px solid var(--border-color, #e0e0e0); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + z-index: 50; + max-height: 70vh; + overflow-y: auto; + min-width: 200px; + max-width: 100vw; +} + +.mobile-tab-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.875rem 1rem; + width: 100%; + min-width: 0; + background: transparent; + border: none; + border-bottom: 1px solid var(--border-color, #e0e0e0); + cursor: pointer; + font-size: 0.875rem; + color: var(--text-primary, #1a1a1a); + text-align: left; + transition: background 0.2s ease; + box-sizing: border-box; + word-wrap: break-word; + overflow-wrap: break-word; +} + +.mobile-tab-item span { + flex: 1; + min-width: 0; + word-wrap: break-word; + overflow-wrap: break-word; +} + +.mobile-tab-item:hover { + background: var(--bg-secondary, #f5f5f5); +} + +.mobile-tab-item.active { + background: var(--bg-secondary, #f5f5f5); + color: var(--accent, #007bff); + font-weight: 600; +} + +.mobile-tab-item .tab-count { + margin-left: auto; +} + +@media (min-width: 768px) { + .tabs-container { + display: flex; + } + + .mobile-tabs-menu-button, + .mobile-tabs-menu { + display: none; + } + + .tab-label { + display: inline; + } +} + +@media (max-width: 767px) { + .tabs-container { + display: none; + } + + .mobile-tabs-menu-button { + display: flex; + } +} diff --git a/src/lib/styles/repo.css b/src/lib/styles/repo.css new file mode 100644 index 0000000..9031a36 --- /dev/null +++ b/src/lib/styles/repo.css @@ -0,0 +1,1879 @@ +/* Repository Page Styles */ + +.container { + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.repo-metadata-section { + padding: 0.75rem 1rem; + background: var(--card-bg, #ffffff); + border-bottom: 1px solid var(--border-color, #e0e0e0); + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: center; + font-size: 0.875rem; + width: 100%; + max-width: 100%; + overflow: hidden; + box-sizing: border-box; +} + +@media (min-width: 768px) { + .repo-metadata-section { + padding: 1rem 1.5rem; + } +} + +@media (max-width: 768px) { + .repo-clone-urls { + flex-direction: column; + align-items: flex-start; + } + + .clone-url-wrapper { + width: 100%; + max-width: 100%; + } + + .clone-url { + max-width: calc(100% - 2rem); + } +} + +.repo-banner { + width: 100%; + height: 200px; + overflow: hidden; + background: var(--bg-secondary); + margin-bottom: 0; + position: relative; + display: none; /* Hidden on mobile by default */ +} + +.desktop-only { + display: none; /* Hidden on mobile */ +} + +@media (min-width: 768px) { + .desktop-only { + display: block; /* Show on desktop */ + } + + .repo-banner { + display: block; /* Show on desktop */ + } +} + +.repo-banner::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient( + to bottom, + transparent 0%, + transparent 60%, + var(--card-bg) 100% + ); + z-index: 1; + pointer-events: none; +} + +.repo-banner::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient( + to right, + transparent 0%, + transparent 85%, + var(--card-bg) 100% + ); + z-index: 1; + pointer-events: none; +} + +.repo-banner img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.repo-banner img[src=""], +.repo-banner img:not([src]) { + display: none; +} + +/* Responsive design for smaller screens */ +.mobile-toggle-button { + display: none; /* Hidden by default on desktop */ + padding: 0.5rem; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 4px; + cursor: pointer; + transition: background 0.2s; + align-items: center; + justify-content: center; +} + +.mobile-toggle-button .icon-inline { + width: 16px; + height: 16px; +} + +.mobile-toggle-button:hover { + background: var(--bg-primary); +} + +.hide-on-mobile { + display: none; +} + +@media (max-width: 768px) { + .repo-banner { + height: 150px; + } + + /* Mobile toggle button visible on narrow screens */ + .mobile-toggle-button { + display: inline-flex; + } + + /* File tree and editor area full width and height on mobile */ + .file-tree { + width: 100%; + flex: 1 1 auto; + min-height: 0; + flex-basis: auto; + } + + .editor-area { + width: 100%; + flex: 1; + min-height: 0; + max-height: none; + } + + /* Hide the appropriate view based on toggle state */ + .file-tree.hide-on-mobile { + display: none !important; + } + + .editor-area.hide-on-mobile { + display: none !important; + } + + /* Stack layout on mobile */ + .repo-layout { + flex-direction: column; + flex: 1; + min-height: 0; + } + + /* Editor header wraps on mobile */ + .editor-header { + flex-wrap: wrap; + gap: 0.25rem; + padding: 0.5rem 0.75rem; + align-items: flex-start; + } + + .file-path { + flex: 1 1 100%; + min-width: 0; + word-break: break-all; + margin-bottom: 0; + padding-bottom: 0; + } + + .editor-actions { + flex: 1 1 auto; + justify-content: flex-end; + min-width: 0; + gap: 0.5rem; + } + + /* Modal responsive styles */ + .modal { + width: 95%; + max-width: 95%; + max-height: 90vh; + margin: 1rem; + padding: 1rem; + overflow-y: auto; + } + + .modal h3 { + font-size: 1.25rem; + margin-bottom: 1rem; + } + + .modal label { + display: block; + margin-bottom: 0.75rem; + font-size: 0.9rem; + } + + .modal input, + .modal textarea, + .modal select { + width: 100%; + font-size: 0.9rem; + padding: 0.5rem; + } + + .modal-actions { + flex-direction: column; + gap: 0.5rem; + } + + .modal-actions button { + width: 100%; + padding: 0.75rem; + } + + /* Issue and PR lists responsive */ + .issue-item, .pr-item { + padding: 0.5rem; + } + + .issue-header, .pr-header { + flex-wrap: wrap; + gap: 0.25rem; + } + + .issue-subject, .pr-subject { + font-size: 0.9rem; + word-break: break-word; + } + + .issue-meta, .pr-meta { + flex-wrap: wrap; + font-size: 0.7rem; + gap: 0.5rem; + } + + .issue-actions { + flex-direction: column; + width: 100%; + } + + .issue-action-btn { + width: 100%; + padding: 0.5rem; + } + + /* Sidebars responsive */ + .prs-sidebar, .issues-sidebar { + width: 100%; + max-width: 100%; + } + + .prs-header, .issues-header { + flex-wrap: wrap; + gap: 0.5rem; + } + + .create-pr-button, .create-issue-button { + width: 100%; + padding: 0.75rem; + } + + /* File tree responsive */ + .file-tree-header { + flex-wrap: wrap; + gap: 0.5rem; + } + + .file-tree-actions { + flex-wrap: wrap; + gap: 0.5rem; + } + + .create-file-button { + font-size: 0.85rem; + padding: 0.5rem 0.75rem; + } + + /* Back button responsive */ + .back-btn { + width: 100%; + margin-bottom: 1rem; + padding: 0.75rem; + } + + .non-maintainer-notice { + font-size: 0.7rem; + flex: 1 1 100%; + order: 2; + margin-top: 0; + padding-top: 0.25rem; + line-height: 1.3; + } +} + +/* Desktop: always show both file tree and editor */ +@media (min-width: 769px) { + .file-tree.hide-on-mobile { + display: flex; + } + + .editor-area.hide-on-mobile { + display: flex; + } + + .mobile-toggle-button { + display: none; + } +} + +.fork-badge { + padding: 0.25rem 0.5rem; + background: var(--accent); + color: var(--accent-text, #ffffff); + border-radius: 4px; + font-size: 0.85rem; + margin-left: 0.5rem; + font-weight: 600; + border: 1px solid var(--accent); +} + +.fork-badge a { + color: var(--accent-text, #ffffff); + text-decoration: none; + font-weight: 600; +} + +.fork-badge a:hover { + text-decoration: underline; + opacity: 1; +} + +.repo-language { + display: inline-flex; + align-items: center; + gap: 0.25rem; + font-size: 0.875rem; + color: var(--text-secondary); + font-weight: 500; +} + +.repo-language .icon-inline { + opacity: 0.9; +} + +.repo-topics { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; +} + +.topic-tag { + padding: 0.25rem 0.5rem; + background: var(--accent); + color: var(--accent-text, #ffffff); + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 600; + border: 1px solid var(--accent); +} + +.repo-website { + margin-top: 0.5rem; + font-size: 0.875rem; +} + +.repo-website a { + color: var(--link-color); + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 0.25rem; +} + +.repo-website a:hover { + text-decoration: underline; +} + +.repo-clone-urls { + margin-top: 0.5rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + font-size: 0.75rem; + width: 100%; + max-width: 100%; + overflow: hidden; +} + +.clone-label { + color: var(--text-muted); + font-weight: 500; +} + +.clone-url-wrapper { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; + max-width: 100%; + min-width: 0; +} + +.clone-url { + padding: 0.125rem 0.375rem; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 0.25rem; + font-family: 'IBM Plex Mono', monospace; + font-size: 0.75rem; + color: var(--text-primary); + word-break: break-all; + overflow-wrap: break-word; + white-space: normal; + max-width: 100%; + min-width: 0; + flex: 1 1 auto; + box-sizing: border-box; +} + +.clone-more { + color: var(--text-muted); + font-size: 0.75rem; +} + +.repo-contributors { + margin-top: 0.75rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75rem; +} + +.contributors-label { + font-size: 0.875rem; + color: var(--text-muted); + font-weight: 500; +} + +.contributors-list { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; +} + +.contributor-item { + display: inline-flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; + padding: 0.25rem 0.5rem; + border-radius: 0.5rem; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + transition: all 0.2s ease; +} + +.contributor-item:hover { + border-color: var(--accent); + background: var(--card-bg); +} + +.contributor-item.contributor-owner { + background: var(--accent-light); + border: 2px solid var(--accent); + font-weight: 600; + box-shadow: 0 0 0 1px var(--accent-light); +} + +.contributor-item.contributor-owner:hover { + background: var(--accent-light); + border-color: var(--accent-hover); + box-shadow: 0 0 0 2px var(--accent-light); +} + +.contributor-badge { + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + white-space: nowrap; + letter-spacing: 0.05em; + border: 1px solid transparent; + /* Ensure minimum size for touch targets */ + min-height: 1.5rem; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.contributor-badge.owner { + /* High contrast colors for all themes */ + background: var(--bg-tertiary, #4a5568); + color: var(--text-primary, #ffffff); + border-color: var(--border-color, #2d3748); +} + +.contributor-badge.maintainer { + /* High contrast colors for all themes */ + background: var(--success-bg, #22543d); + color: var(--success-text, #ffffff); + border-color: var(--border-color, #1a202c); +} + +.repo-view { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.repo-layout { + flex: 1; + display: flex; + overflow: hidden; +} + +.file-tree { + width: 300px; + min-width: 300px; + max-width: 300px; + border-right: 1px solid var(--border-color); + background: var(--bg-secondary); + display: flex; + flex-direction: column; + overflow: hidden; + flex: 0 0 300px; /* Fixed width, don't grow or shrink */ + min-height: 0; /* Allow flex child to shrink */ +} + +.file-tree-header { + padding: 1rem; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; +} + +.file-tree-header h2 { + margin: 0; + font-size: 1rem; + color: var(--text-primary); +} + +.back-button { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 0.25rem; + cursor: pointer; + color: var(--text-primary); + transition: background 0.2s ease; +} + +.back-button:hover { + background: var(--bg-secondary); +} + +.file-list { + list-style: none; + padding: 0.5rem 0; + margin: 0; + overflow-y: auto; + overflow-x: hidden; + flex: 1; + min-height: 0; /* Allows flex child to shrink below content size */ + width: 100%; /* Fill horizontal space */ +} + +.file-item { + margin: 0; +} + +.file-button { + width: 100%; + padding: 0.5rem 1rem; + text-align: left; + background: none; + border: none; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + color: var(--text-primary); + font-weight: 500; + transition: background 0.2s ease, color 0.2s ease; + box-sizing: border-box; +} + +.file-button:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.file-item.selected .file-button { + background: var(--accent); + color: var(--accent-text, #ffffff); + font-weight: 600; +} + +.file-item.selected .file-button:hover { + background: var(--accent-hover); + color: var(--accent-text, #ffffff); +} + +.file-size { + color: var(--text-secondary); + font-size: 0.75rem; + margin-left: auto; + opacity: 0.9; +} + +.file-item.selected .file-size { + color: var(--accent-text, #ffffff); + opacity: 0.9; +} + +.editor-area { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background: var(--card-bg); + max-height: calc(200vh - 400px); /* Twice the original height */ +} + +.editor-header { + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; +} + +.file-path { + font-family: 'IBM Plex Mono', monospace; + font-size: 0.875rem; + color: var(--text-primary); +} + +.editor-actions { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +.unsaved-indicator { + color: var(--warning-text); + font-size: 0.875rem; +} + +.non-maintainer-notice { + font-size: 0.75rem; + color: var(--text-muted); + white-space: normal; + line-height: 1.4; +} + +.save-button { + padding: 0.5rem 1rem; + background: var(--button-primary); + color: var(--accent-text, #ffffff); + border: none; + border-radius: 0.25rem; + cursor: pointer; + font-size: 0.875rem; + font-family: 'IBM Plex Serif', serif; + transition: background 0.2s ease; +} + +.save-button:hover:not(:disabled) { + background: var(--button-primary-hover); +} + +.save-button:disabled { + background: var(--text-muted); + cursor: not-allowed; + opacity: 0.6; +} + +.editor-container { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 0; /* Allows flex child to shrink below content size */ +} + +.empty-state { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); +} + +.loading { + padding: 2rem; + text-align: center; + color: var(--text-muted); +} + +.error { + background: var(--error-bg); + color: var(--error-text); + padding: 1rem; + margin: 1rem; + border-radius: 0.5rem; + border: 1px solid var(--error-text); +} + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal { + background: var(--card-bg); + padding: 2rem; + border-radius: 0.5rem; + min-width: 400px; + max-width: 600px; + border: 1px solid var(--border-color); +} + +.modal h3 { + margin: 0 0 1rem 0; + color: var(--text-primary); +} + +.verification-modal { + max-width: 800px; + min-width: 600px; + max-height: 90vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.modal-header { + flex-shrink: 0; + margin-bottom: 1rem; +} + +.modal-body { + flex: 1; + overflow-y: auto; + min-height: 0; +} + +.verification-instructions { + color: var(--text-secondary); + margin-bottom: 1rem; + line-height: 1.6; +} + +.verification-instructions code { + background: var(--bg-secondary); + padding: 0.125rem 0.375rem; + border-radius: 0.25rem; + font-family: monospace; + color: var(--accent); +} + +.verification-file-content { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 0.375rem; + overflow: hidden; + margin-bottom: 1rem; + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} + +.file-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + background: var(--bg-tertiary); + border-bottom: 1px solid var(--border-color); +} + +.filename { + font-family: monospace; + font-weight: 500; + color: var(--text-primary); +} + +.file-actions { + display: flex; + gap: 0.5rem; +} + +.copy-button, +.download-button { + padding: 0.375rem 0.75rem; + background: var(--bg-primary); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: 0.25rem; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.copy-button:hover, +.download-button:hover { + background: var(--bg-secondary); + border-color: var(--accent); +} + +.file-content { + margin: 0; + padding: 1rem; + overflow-x: auto; + overflow-y: auto; + background: var(--bg-primary); + max-height: 400px; +} + +.file-content code { + font-family: 'Courier New', monospace; + font-size: 0.875rem; + color: var(--text-primary); + white-space: pre; +} + +.modal label { + display: block; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.modal input, +.modal textarea, +.modal select { + width: 100%; + padding: 0.5rem; + border: 1px solid var(--input-border); + border-radius: 0.25rem; + font-family: 'IBM Plex Serif', serif; + background: var(--input-bg); + color: var(--text-primary); + margin-top: 0.5rem; +} + +.modal input:focus, +.modal textarea:focus, +.modal select:focus { + outline: none; + border-color: var(--input-focus); +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 1rem; +} + +.cancel-button { + padding: 0.5rem 1rem; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 0.25rem; + cursor: pointer; + color: var(--text-primary); + font-family: 'IBM Plex Serif', serif; + transition: background 0.2s ease; +} + +.cancel-button:hover { + background: var(--bg-secondary); +} + +/* File tree actions */ +.file-tree-actions { + display: flex; + gap: 0.5rem; +} + +.create-file-button, .create-tag-button { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + background: var(--button-primary); + color: white; + border: none; + border-radius: 0.25rem; + cursor: pointer; + font-family: 'IBM Plex Serif', serif; + transition: background 0.2s ease; +} + +.create-file-button:hover, .create-tag-button:hover { + background: var(--button-primary-hover); +} + +.delete-file-button { + padding: 0.25rem; + background: none; + border: none; + cursor: pointer; + font-size: 0.75rem; + opacity: 0.6; +} + +.delete-file-button:hover { + opacity: 1; +} + +.file-item { + display: flex; + align-items: center; +} + +.file-item .file-button { + flex: 1; +} + +/* History sidebar */ +.history-sidebar, .tags-sidebar { + width: 300px; + border-right: 1px solid var(--border-color); + background: var(--bg-secondary); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.history-header, .tags-header { + padding: 1rem; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; +} + +.history-header h2, .tags-header h2 { + margin: 0; + font-size: 1rem; + color: var(--text-primary); +} + +.refresh-button { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 0.25rem; + cursor: pointer; + color: var(--text-primary); + transition: background 0.2s ease; + display: inline-flex; + align-items: center; + gap: 0.375rem; +} + +.refresh-button:hover { + background: var(--bg-secondary); +} + +.refresh-button .icon-inline { + width: 0.875rem; + height: 0.875rem; +} + +.commit-list, .tag-list { + list-style: none; + padding: 0; + margin: 0; + overflow-y: auto; + flex: 1; +} + +.commit-item, .tag-item { + border-bottom: 1px solid var(--border-color); +} + +.commit-button { + width: 100%; + padding: 0.75rem 1rem; + text-align: left; + background: none; + border: none; + cursor: pointer; + display: block; +} + +.commit-button:hover { + background: var(--bg-tertiary); +} + +.commit-item.selected .commit-button { + background: var(--accent); + color: var(--accent-text, #ffffff); +} + +.commit-hash { + font-family: 'IBM Plex Mono', monospace; + font-size: 0.75rem; + color: var(--text-muted); + margin-bottom: 0.25rem; +} + +.commit-message { + font-weight: 500; + margin-bottom: 0.25rem; + color: var(--text-primary); +} + +.commit-meta { + font-size: 0.75rem; + color: var(--text-muted); + display: flex; + gap: 1rem; +} + +.tag-item { + padding: 0.75rem 1rem; +} + +.tag-name { + font-weight: 500; + color: var(--link-color); + margin-bottom: 0.25rem; +} + +.tag-hash { + font-family: 'IBM Plex Mono', monospace; + font-size: 0.75rem; + color: var(--text-muted); + margin-bottom: 0.25rem; +} + +.tag-message { + font-size: 0.875rem; + color: var(--text-muted); +} + +/* Diff view */ +.diff-view { + flex: 1; + overflow: auto; + padding: 1rem; +} + +.diff-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border-color); +} + +.diff-header h3 { + margin: 0; + color: var(--text-primary); +} + +.close-button { + padding: 0.25rem 0.5rem; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 0.25rem; + cursor: pointer; + font-size: 1.25rem; + line-height: 1; + color: var(--text-primary); + transition: background 0.2s ease; +} + +.close-button:hover { + background: var(--bg-secondary); +} + +.diff-file { + margin-bottom: 2rem; +} + +.diff-file-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem; + background: var(--bg-secondary); + border-radius: 0.25rem; + margin-bottom: 0.5rem; +} + +.diff-file-name { + font-family: 'IBM Plex Mono', monospace; + font-weight: 500; + color: var(--text-primary); +} + +.diff-stats { + display: flex; + gap: 0.5rem; +} + +.additions { + color: var(--success-text); +} + +.deletions { + color: var(--error-text); +} + +.diff-content { + background: var(--bg-tertiary); + color: var(--text-primary); + padding: 1rem; + border-radius: 0.25rem; + overflow-x: auto; + font-size: 0.875rem; + line-height: 1.5; + border: 1px solid var(--border-color); +} + +.diff-content code { + font-family: 'IBM Plex Mono', monospace; + white-space: pre; +} + +.read-only-editor { + height: 100%; + overflow-y: auto; + overflow-x: hidden; + padding: 1.5rem; + min-height: 0; /* Allows flex child to shrink below content size */ +} + +.read-only-editor :global(.hljs) { + padding: 1rem; + background: var(--bg-tertiary); + color: var(--text-primary); + border-radius: 4px; + overflow-x: auto; + margin: 0; + border: 1px solid var(--border-color); +} + +.read-only-editor :global(pre) { + margin: 0; + padding: 0; +} + +.read-only-editor :global(code) { + font-family: 'IBM Plex Mono', monospace; + font-size: 14px; + line-height: 1.5; + display: block; + white-space: pre; +} + +.readme-section { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.readme-header { + padding: 1rem; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; +} + +.readme-header h3 { + margin: 0; + font-size: 1.25rem; + color: var(--text-primary); +} + +.readme-actions { + display: flex; + gap: 1rem; + align-items: center; +} + +.discussions-content { + flex: 1; + display: flex; + flex-direction: column; + overflow-y: auto; + overflow-x: hidden; + background: var(--card-bg); + min-height: 0; /* Allows flex child to shrink below content size */ +} + +.discussions-header { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; +} + +@media (max-width: 767px) { + .discussions-header { + padding: 0.75rem 1rem; + } +} + +.discussions-actions { + display: flex; + gap: 0.75rem; + align-items: center; +} + +.btn-secondary { + padding: 0.5rem 1rem; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 0.25rem; + color: var(--text-primary); + cursor: pointer; + font-size: 0.875rem; + transition: background 0.2s; +} + +.btn-secondary:hover:not(:disabled) { + background: var(--bg-tertiary); +} + +.btn-secondary:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.discussions-header h2 { + margin: 0; + font-size: 1.25rem; + color: var(--text-primary); +} + +.discussion-item { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border-color); +} + +.discussion-header { + margin-bottom: 0.5rem; +} + +.discussion-header h3 { + margin: 0 0 0.25rem 0; + font-size: 1.1rem; + color: var(--text-primary); +} + +.discussion-meta { + display: flex; + gap: 1rem; + font-size: 0.875rem; + color: var(--text-muted); +} + +.discussion-type { + padding: 0.125rem 0.5rem; + border-radius: 0.25rem; + background: var(--bg-secondary); + font-weight: 500; +} + +.discussion-body { + margin-top: 0.5rem; + color: var(--text-primary); + white-space: pre-wrap; +} + +.discussion-title-row { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.expand-button { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + display: flex; + align-items: center; + justify-content: center; + min-width: 1.5rem; + transition: color 0.2s; +} + +.expand-button:hover { + color: var(--text-primary); +} + +.comment-count { + font-weight: 500; +} + +.btn-small { + padding: 0.25rem 0.75rem; + font-size: 0.875rem; +} + +.comments-section { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--border-color); +} + +.comments-section h4 { + margin: 0 0 0.75rem 0; + font-size: 0.9rem; + color: var(--text-muted); + font-weight: 500; +} + +.comment-item { + padding: 0.75rem 0; + border-bottom: 1px solid var(--border-light); +} + +.comment-item:last-child { + border-bottom: none; +} + +.comment-meta { + display: flex; + gap: 0.75rem; + align-items: center; + margin-bottom: 0.5rem; + font-size: 0.875rem; + color: var(--text-muted); +} + +.comment-content { + color: var(--text-primary); + white-space: pre-wrap; + line-height: 1.5; +} + +.referenced-event { + margin-bottom: 1rem; + padding: 0.75rem; + background: var(--bg-primary); + border-left: 3px solid var(--border-color); + border-radius: 0.5rem; + font-size: 0.875rem; +} + +:global([data-theme="light"]) .referenced-event { + background: #e8e8e8; +} + +:global([data-theme="dark"]) .referenced-event { + background: rgba(0, 0, 0, 0.2); +} + +:global([data-theme="black"]) .referenced-event { + background: #0a0a0a; +} + +.referenced-event-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.referenced-event-time { + font-size: 0.75rem; + color: var(--text-muted); +} + +.referenced-event-content { + color: var(--text-secondary); + white-space: pre-wrap; + word-wrap: break-word; + line-height: 1.5; + max-height: 10rem; + overflow: hidden; + text-overflow: ellipsis; +} + +.nostr-link-event { + margin: 0.75rem 0; + padding: 0.75rem; + background: var(--bg-primary); + border-left: 3px solid var(--border-color); + border-radius: 0.5rem; + font-size: 0.875rem; +} + +:global([data-theme="light"]) .nostr-link-event { + background: #e8e8e8; +} + +:global([data-theme="dark"]) .nostr-link-event { + background: rgba(0, 0, 0, 0.2); +} + +:global([data-theme="black"]) .nostr-link-event { + background: #0a0a0a; +} + +.nostr-link-event-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.nostr-link-event-time { + font-size: 0.75rem; + color: var(--text-muted); +} + +.nostr-link-event-content { + color: var(--text-secondary); + white-space: pre-wrap; + word-wrap: break-word; + line-height: 1.5; + max-height: 10rem; + overflow: hidden; + text-overflow: ellipsis; +} + +.nostr-link-placeholder { + color: var(--text-muted); + font-style: italic; +} + +.nested-replies { + margin-left: 2rem; + margin-top: 0.75rem; + padding-left: 1rem; + border-left: 2px solid var(--border-light); +} + +.nested-comment { + margin-top: 0.75rem; +} + +.readme-content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 1.5rem; + min-height: 0; /* Allows flex child to shrink below content size */ +} + +.readme-content.markdown { + padding: 1.5rem; +} + +.readme-content.markdown :global(h1), +.readme-content.markdown :global(h2), +.readme-content.markdown :global(h3), +.readme-content.markdown :global(h4), +.readme-content.markdown :global(h5), +.readme-content.markdown :global(h6) { + margin-top: 1.5rem; + margin-bottom: 0.75rem; + color: var(--text-primary); +} + +.readme-content.markdown :global(p) { + margin-bottom: 1rem; + line-height: 1.6; +} + +.readme-content.markdown :global(code) { + background: var(--bg-secondary); + padding: 0.2rem 0.4rem; + border-radius: 3px; + font-family: 'IBM Plex Mono', monospace; + font-size: 0.9em; +} + +.readme-content.markdown :global(pre) { + background: var(--bg-secondary); + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + border: 1px solid var(--border-light); + margin: 1rem 0; +} + +.readme-content.markdown :global(pre code) { + background: none; + padding: 0; +} + +.readme-content :global(.hljs) { + background: var(--bg-secondary); + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + border: 1px solid var(--border-light); +} + +.readme-content :global(pre.hljs) { + margin: 1rem 0; +} + +/* Documentation */ +.docs-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background: var(--card-bg); +} + +.documentation-body { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 1.5rem; + min-height: 0; +} + +.documentation-body :global(h1), +.documentation-body :global(h2), +.documentation-body :global(h3), +.documentation-body :global(h4), +.documentation-body :global(h5), +.documentation-body :global(h6) { + margin-top: 1.5rem; + margin-bottom: 0.75rem; + color: var(--text-primary); +} + +.documentation-body :global(p) { + margin-bottom: 1rem; + line-height: 1.6; +} + +.documentation-body :global(code) { + background: var(--bg-secondary); + padding: 0.2rem 0.4rem; + border-radius: 3px; + font-family: 'IBM Plex Mono', monospace; + font-size: 0.9em; +} + +.documentation-body :global(pre) { + background: var(--bg-secondary); + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + border: 1px solid var(--border-light); + margin: 1rem 0; +} + +.documentation-body :global(pre code) { + background: none; + padding: 0; +} + +.documentation-body :global(.hljs) { + background: var(--bg-secondary); + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + border: 1px solid var(--border-light); +} + +.documentation-body :global(pre.hljs) { + margin: 1rem 0; +} + +/* Issues and PRs */ +.issues-sidebar, .prs-sidebar { + width: 300px; + border-right: 1px solid var(--border-color); + background: var(--bg-secondary); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.issues-header, .prs-header { + padding: 1rem; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; +} + +.issues-header h2, .prs-header h2 { + margin: 0; + font-size: 1rem; + color: var(--text-primary); +} + +.create-issue-button, .create-pr-button { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + background: var(--button-primary); + color: white; + border: none; + border-radius: 0.25rem; + cursor: pointer; + font-family: 'IBM Plex Serif', serif; + transition: background 0.2s ease; +} + +.create-issue-button:hover, .create-pr-button:hover { + background: var(--button-primary-hover); +} + +.issue-list, .pr-list { + list-style: none; + padding: 0; + margin: 0; + overflow-y: auto; + flex: 1; +} + +.issue-actions { + display: flex; + gap: 0.5rem; + margin-top: 0.5rem; + flex-wrap: wrap; +} + +.issue-action-btn { + padding: 0.4rem 0.8rem; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.85rem; + font-family: 'IBM Plex Serif', serif; + transition: background 0.2s ease; +} + +.issue-action-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.issue-action-btn.close-btn { + background: var(--error-text, #dc3545); + color: white; +} + +.issue-action-btn.close-btn:hover:not(:disabled) { + background: var(--error-hover, #c82333); +} + +.issue-action-btn.resolve-btn { + background: var(--success-text, #28a745); + color: white; +} + +.issue-action-btn.resolve-btn:hover:not(:disabled) { + background: var(--success-hover, #218838); +} + +.issue-action-btn.reopen-btn { + background: var(--accent, #007bff); + color: white; +} + +.issue-action-btn.reopen-btn:hover:not(:disabled) { + background: var(--accent-hover, #0056b3); +} + +.issue-item, .pr-item { + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border-color); + cursor: pointer; + transition: background 0.2s ease; +} + +.issue-item:hover, .pr-item:hover { + background: var(--bg-tertiary); +} + +.issue-header, .pr-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.25rem; +} + +.issue-status, .pr-status { + padding: 0.125rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; +} + +.issue-status.open, .pr-status.open { + background: var(--accent); + color: var(--accent-text, #ffffff); + font-weight: 600; +} + +.issue-status.closed, .pr-status.closed { + background: var(--error-bg); + color: var(--error-text); +} + +.issue-status.resolved, .pr-status.merged { + background: var(--success-bg); + color: var(--success-text); +} + +.issue-subject, .pr-subject { + font-weight: 500; + flex: 1; + color: var(--text-primary); +} + +.issue-meta, .pr-meta { + font-size: 0.75rem; + color: var(--text-muted); + display: flex; + gap: 0.75rem; +} + +.pr-commit { + font-family: 'IBM Plex Mono', monospace; +} + +.issues-content, .prs-content { + flex: 1; + overflow-y: auto; + padding: 2rem; + background: var(--card-bg); +} + +.issue-detail, .pr-detail { + margin-bottom: 2rem; + padding-bottom: 2rem; + border-bottom: 1px solid var(--border-color); +} + +.issue-detail h3, .pr-detail h3 { + margin: 0 0 0.5rem 0; + font-size: 1.5rem; + color: var(--text-primary); +} + +.issue-meta-detail, .pr-meta-detail { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; + font-size: 0.875rem; + color: var(--text-muted); +} + +.issue-body, .pr-body { + line-height: 1.6; + color: var(--text-primary); +} + +.verification-badge { + display: inline-flex; + align-items: center; + padding: 0.125rem 0.25rem; + border-radius: 0.25rem; + font-size: 0.75rem; + flex-shrink: 0; +} + +.verification-badge.loading { + opacity: 0.6; +} + +.verification-badge.verified { + color: var(--success-text, #10b981); +} + +.verification-badge.unverified { + color: var(--error-text, #f59e0b); +} + +.verification-badge .icon-inline { + width: 1em; + height: 1em; + margin: 0; +} + +.icon-inline { + width: 1em; + height: 1em; + vertical-align: middle; + display: inline-block; + margin-right: 0.25rem; + /* Make icons visible on dark backgrounds by inverting to light */ + filter: brightness(0) saturate(100%) invert(1); +} + +.icon-small { + width: 16px; + height: 16px; + vertical-align: middle; + /* Make icons visible on dark backgrounds by inverting to light */ + filter: brightness(0) saturate(100%) invert(1); +} + +/* Theme-aware icon colors */ + +.verification-badge.verified .icon-inline { + /* Green checkmark for verified */ + filter: brightness(0) saturate(100%) invert(48%) sepia(79%) saturate(2476%) hue-rotate(86deg) brightness(118%) contrast(119%); +} + +.verification-badge.unverified .icon-inline { + /* Orange/yellow warning for unverified */ + filter: brightness(0) saturate(100%) invert(67%) sepia(93%) saturate(1352%) hue-rotate(358deg) brightness(102%) contrast(106%); +} + +.file-button .icon-inline { + filter: brightness(0) saturate(100%) invert(1); + opacity: 0.7; +} + +.delete-file-button .icon-small { + /* Red for delete button */ + filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2878%) hue-rotate(346deg) brightness(104%) contrast(97%); +} diff --git a/src/routes/repos/+page.svelte b/src/routes/repos/+page.svelte index 80185dd..92d4b29 100644 --- a/src/routes/repos/+page.svelte +++ b/src/routes/repos/+page.svelte @@ -37,7 +37,7 @@ let mostFavoritedRepos = $state>([]); let loadingMostFavorited = $state(false); let mostFavoritedPage = $state(0); - let mostFavoritedCache: { data: typeof mostFavoritedRepos; timestamp: number } | null = null; + let mostFavoritedCache = $state<{ data: typeof mostFavoritedRepos; timestamp: number } | null>(null); const CACHE_TTL = 5 * 60 * 1000; // 5 minutes import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; @@ -962,6 +962,7 @@ text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; + line-clamp: 2; -webkit-box-orient: vertical; } diff --git a/src/routes/repos/[npub]/[repo]/+page.svelte b/src/routes/repos/[npub]/[repo]/+page.svelte index 9e27f56..e957f5d 100644 --- a/src/routes/repos/[npub]/[repo]/+page.svelte +++ b/src/routes/repos/[npub]/[repo]/+page.svelte @@ -9,6 +9,7 @@ import RepoHeaderEnhanced from '$lib/components/RepoHeaderEnhanced.svelte'; import RepoTabs from '$lib/components/RepoTabs.svelte'; import NostrLinkRenderer from '$lib/components/NostrLinkRenderer.svelte'; + import '$lib/styles/repo.css'; import { getPublicKeyWithNIP07, isNIP07Available, signEventWithNIP07 } from '$lib/services/nostr/nip07-signer.js'; import { NostrClient } from '$lib/services/nostr/nostr-client.js'; import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS, combineRelays } from '$lib/config.js'; @@ -3663,49 +3664,11 @@ deletingAnnouncement={deletingAnnouncement} hasUnlimitedAccess={hasUnlimitedAccess($userStore.userLevel)} needsClone={needsClone} + allMaintainers={allMaintainers} /> {/if} - - {#if allMaintainers.length > 0 || pageData.repoOwnerPubkey} - - {/if} + {#if pageData.repoWebsite || (pageData.repoCloneUrls && pageData.repoCloneUrls.length > 0) || pageData.repoLanguage || (pageData.repoTopics && pageData.repoTopics.length > 0) || forkInfo?.isFork} {/if}
- - diff --git a/src/routes/users/[npub]/+page.svelte b/src/routes/users/[npub]/+page.svelte index 992c361..ecc799c 100644 --- a/src/routes/users/[npub]/+page.svelte +++ b/src/routes/users/[npub]/+page.svelte @@ -89,21 +89,22 @@ let nostrLinkProfiles = $state>(new Map()); // npub -> pubkey hex // Parse nostr: links from content and extract IDs/pubkeys - function parseNostrLinks(content: string): Array<{ type: 'nevent' | 'naddr' | 'note1' | 'npub' | 'profile'; value: string; start: number; end: number }> { - const links: Array<{ type: 'nevent' | 'naddr' | 'note1' | 'npub' | 'profile'; value: string; start: number; end: number }> = []; - const nostrLinkRegex = /nostr:(nevent1|naddr1|note1|npub1|profile1)[a-zA-Z0-9]+/g; + function parseNostrLinks(content: string): Array<{ type: 'nevent' | 'naddr' | 'note1' | 'npub' | 'profile' | 'nprofile'; value: string; start: number; end: number }> { + const links: Array<{ type: 'nevent' | 'naddr' | 'note1' | 'npub' | 'profile' | 'nprofile'; value: string; start: number; end: number }> = []; + const nostrLinkRegex = /nostr:(nevent1|naddr1|note1|npub1|profile1|nprofile1)[a-zA-Z0-9]+/g; let match; while ((match = nostrLinkRegex.exec(content)) !== null) { const fullMatch = match[0]; const prefix = match[1]; - let type: 'nevent' | 'naddr' | 'note1' | 'npub' | 'profile'; + let type: 'nevent' | 'naddr' | 'note1' | 'npub' | 'profile' | 'nprofile'; if (prefix === 'nevent1') type = 'nevent'; else if (prefix === 'naddr1') type = 'naddr'; else if (prefix === 'note1') type = 'note1'; else if (prefix === 'npub1') type = 'npub'; else if (prefix === 'profile1') type = 'profile'; + else if (prefix === 'nprofile1') type = 'nprofile'; else continue; links.push({ @@ -141,11 +142,18 @@ const aTag = `${decoded.data.kind}:${decoded.data.pubkey}:${decoded.data.identifier}`; aTags.push(aTag); } - } else if (link.type === 'npub' || link.type === 'profile') { + } else if (link.type === 'npub' || link.type === 'profile' || link.type === 'nprofile') { const decoded = nip19.decode(link.value.replace('nostr:', '')); if (decoded.type === 'npub') { npubs.push(link.value); nostrLinkProfiles.set(link.value, decoded.data as string); + } else if (decoded.type === 'nprofile') { + // nprofile contains { pubkey: string, relays?: string[] } + const pubkey = (decoded.data as { pubkey: string }).pubkey; + if (pubkey) { + npubs.push(link.value); + nostrLinkProfiles.set(link.value, pubkey); + } } } } catch { @@ -222,9 +230,29 @@ return undefined; } - // Get pubkey from nostr: npub/profile link + // Get pubkey from nostr: npub/profile/nprofile link function getPubkeyFromNostrLink(link: string): string | undefined { - return nostrLinkProfiles.get(link); + // Check cache first + const cached = nostrLinkProfiles.get(link); + if (cached) return cached; + + // If not in cache, try to decode nprofile on the fly + if (link.startsWith('nostr:nprofile1')) { + try { + const decoded = nip19.decode(link.replace('nostr:', '')); + if (decoded.type === 'nprofile') { + const pubkey = (decoded.data as { pubkey: string }).pubkey; + if (pubkey) { + nostrLinkProfiles.set(link, pubkey); + return pubkey; + } + } + } catch { + // Invalid link + } + } + + return undefined; } // Process content with nostr links into parts for rendering @@ -843,13 +871,22 @@ i * const userPubkey = profileOwnerPubkeyHex; // Store in local variable for type safety loadingActivity = true; try { - // Step 1: Fetch all repo announcements where user is owner or maintainer - const repoAnnouncements = await nostrClient.fetchEvents([ - { - kinds: [KIND.REPO_ANNOUNCEMENT], - authors: [userPubkey], - limit: 100 - } + // Step 1: Fetch repo announcements in parallel (reduced limit) + const [repoAnnouncements, allAnnouncements] = await Promise.all([ + nostrClient.fetchEvents([ + { + kinds: [KIND.REPO_ANNOUNCEMENT], + authors: [userPubkey], + limit: 50 // Reduced from 100 + } + ]), + nostrClient.fetchEvents([ + { + kinds: [KIND.REPO_ANNOUNCEMENT], + '#p': [userPubkey], + limit: 50 // Reduced from 100 + } + ]) ]); // Step 2: Extract a-tags from repo announcements @@ -862,22 +899,11 @@ i * } } - // Step 3: Also check for repos where user is a maintainer (not just owner) - // We'll fetch announcements and check maintainer tags - const allAnnouncements = await nostrClient.fetchEvents([ - { - kinds: [KIND.REPO_ANNOUNCEMENT], - '#p': [userPubkey], // Events that mention the user - limit: 100 - } - ]); - + // Step 3: Check for repos where user is a maintainer for (const announcement of allAnnouncements) { - // Check if user is in maintainers tag const maintainersTag = announcement.tags.find(t => t[0] === 'maintainers'); if (maintainersTag) { const isMaintainer = maintainersTag.slice(1).some(m => { - // Handle both hex and npub formats try { const decoded = nip19.decode(m); if (decoded.type === 'npub') { @@ -899,89 +925,88 @@ i * } } - // Step 4: Fetch events that reference the user or their repos + // Step 4: Fetch events that reference the user or their repos (reduced limits) const filters: any[] = []; // Events with user in p-tag filters.push({ '#p': [userPubkey], - limit: 200 + limit: 100 // Reduced from 200 }); // Events with user in q-tag filters.push({ '#q': [userPubkey], - limit: 200 + limit: 100 // Reduced from 200 }); // Events with repo a-tags if (aTags.size > 0) { filters.push({ '#a': Array.from(aTags), - limit: 200 + limit: 100 // Reduced from 200 }); } - const allActivityEvents = await nostrClient.fetchEvents(filters); + const allActivityEvents = await Promise.race([ + nostrClient.fetchEvents(filters), + new Promise((resolve) => setTimeout(() => resolve([]), 15000)) // 15s timeout + ]); // Step 5: Deduplicate, filter, and sort by created_at (newest first) const eventMap = new Map(); for (const event of allActivityEvents) { - // Use shared exclusion function to filter out: - // - User's own events - // - Ephemeral events (20000-29999) - // - Replaceable events (0, 3, 10000-19999) - metadata/configuration - // - Non-repo regular kinds (1, 2, 5, 6, 7, 8, 24) if (shouldExcludeEvent(event, userPubkey, true)) { continue; } - // Keep the newest version if duplicate const existing = eventMap.get(event.id); if (!existing || event.created_at > existing.created_at) { eventMap.set(event.id, event); } } - // Sort by created_at descending and limit to 200 + // Sort by created_at descending and limit to 50 (reduced from 200) activityEvents = Array.from(eventMap.values()) .sort((a, b) => b.created_at - a.created_at) - .slice(0, 200); - - // Fetch referenced events from a-tags and e-tags - await loadReferencedEvents(activityEvents); - - // Fetch nostr: links from event content - for (const event of activityEvents) { - if (event.content) { - await loadNostrLinks(event.content); - } - } + .slice(0, 50); + + // Step 6: Load referenced events and nostr links in parallel (limited) + await Promise.all([ + loadReferencedEvents(activityEvents.slice(0, 50)), // Only load for first 50 + // Batch load nostr links for all events at once + Promise.all(activityEvents.slice(0, 50).map(event => + event.content ? loadNostrLinks(event.content) : Promise.resolve() + )) + ]); } catch (err) { console.error('Failed to load activity:', err); error = 'Failed to load activity'; } finally { - activityLoaded = true; // Mark as loaded to prevent infinite loop (even on error) + activityLoaded = true; loadingActivity = false; } } async function loadReferencedEvents(events: NostrEvent[]) { + // Limit to first 50 events to avoid too many queries + const eventsToProcess = events.slice(0, 50); + // Collect all referenced event IDs and a-tags const eventIds = new Set(); const aTags = new Set(); - for (const event of events) { - // Collect e-tags (event references) - const eTags = event.tags.filter(t => t[0] === 'e' && t[1]); + for (const event of eventsToProcess) { + // Collect e-tags (event references) - limit to first 5 per event + const eTags = event.tags.filter(t => t[0] === 'e' && t[1]).slice(0, 5); for (const eTag of eTags) { if (eTag[1]) { eventIds.add(eTag[1]); } } - // Collect a-tags (addressable event references) - const aTagValues = event.tags.filter(t => t[0] === 'a' && t[1]); + // Collect a-tags (addressable event references) - limit to first 5 per event + const aTagValues = event.tags.filter(t => t[0] === 'a' && t[1]).slice(0, 5); for (const aTag of aTagValues) { if (aTag[1]) { aTags.add(aTag[1]); @@ -991,53 +1016,81 @@ i * if (eventIds.size === 0 && aTags.size === 0) return; - // Fetch events by ID + // Limit total references to prevent too many queries + const limitedEventIds = Array.from(eventIds).slice(0, 50); + const limitedATags = Array.from(aTags).slice(0, 50); + + // Fetch events by ID (single batch query) const eventsToFetch: Promise[] = []; - if (eventIds.size > 0) { + if (limitedEventIds.length > 0) { eventsToFetch.push( - nostrClient.fetchEvents([ - { - ids: Array.from(eventIds), - limit: eventIds.size - } + Promise.race([ + nostrClient.fetchEvents([ + { + ids: limitedEventIds, + limit: limitedEventIds.length + } + ]), + new Promise((resolve) => setTimeout(() => resolve([]), 8000)) ]).catch(() => []) ); } - // Fetch events by a-tags - if (aTags.size > 0) { - for (const aTag of aTags) { + // Batch a-tags by kind and author to reduce queries + if (limitedATags.length > 0) { + const aTagGroups = new Map>(); // key: "kind:pubkey", value: Set of d-tags + + for (const aTag of limitedATags) { const parts = aTag.split(':'); if (parts.length === 3) { - const kind = parseInt(parts[0]); + const kind = parts[0]; const pubkey = parts[1]; const dTag = parts[2]; + const key = `${kind}:${pubkey}`; - eventsToFetch.push( + if (!aTagGroups.has(key)) { + aTagGroups.set(key, new Set()); + } + aTagGroups.get(key)!.add(dTag); + } + } + + // Fetch events by grouped a-tags (one query per kind:pubkey combination) + for (const [key, dTags] of aTagGroups.entries()) { + const parts = key.split(':'); + const kind = parseInt(parts[0]); + const pubkey = parts[1]; + + // Limit d-tags per query to 10 + const dTagsArray = Array.from(dTags).slice(0, 10); + + eventsToFetch.push( + Promise.race([ nostrClient.fetchEvents([ { kinds: [kind], authors: [pubkey], - '#d': [dTag], - limit: 1 + '#d': dTagsArray, + limit: dTagsArray.length } - ]).catch(() => []) - ); - } + ]), + new Promise((resolve) => setTimeout(() => resolve([]), 8000)) + ]).catch(() => []) + ); } } - // Fetch all referenced events + // Fetch all referenced events with timeout try { - const fetchPromises = eventsToFetch.map(p => - Promise.race([ - p, - new Promise((resolve) => setTimeout(() => resolve([]), 10000)) - ]) + const fetchedEventsArrays = await Promise.all( + eventsToFetch.map(p => + Promise.race([ + p, + new Promise((resolve) => setTimeout(() => resolve([]), 8000)) + ]) + ) ); - - const fetchedEventsArrays = await Promise.all(fetchPromises); const fetchedEvents = fetchedEventsArrays.flat(); // Update cache reactively - add new events, avoid duplicates @@ -1051,6 +1104,97 @@ i * } } + // Extract repo info from a-tag + function getRepoInfoFromATag(aTag: string): { ownerPubkey: string; repoName: string } | null { + const parts = aTag.split(':'); + if (parts.length >= 3) { + const kind = parseInt(parts[0]); + if (kind === KIND.REPO_ANNOUNCEMENT) { + return { + ownerPubkey: parts[1], + repoName: parts[2] + }; + } + } + return null; + } + + // Get repo info from event (checks a-tags) + function getRepoInfo(event: NostrEvent): { ownerPubkey: string; repoName: string; ownerNpub: string } | null { + const aTag = event.tags.find(t => t[0] === 'a' && t[1])?.[1]; + if (aTag) { + const repoInfo = getRepoInfoFromATag(aTag); + if (repoInfo) { + try { + const ownerNpub = nip19.npubEncode(repoInfo.ownerPubkey); + return { + ...repoInfo, + ownerNpub + }; + } catch { + // If encoding fails, return null instead of incomplete object + return null; + } + } + } + return null; + } + + // Get git event type name + function getGitEventTypeName(kind: number): string { + switch (kind) { + case KIND.ISSUE: + return 'Issue'; + case KIND.PULL_REQUEST: + return 'Pull Request'; + case KIND.PULL_REQUEST_UPDATE: + return 'Pull Request Update'; + case KIND.PATCH: + return 'Patch'; + case KIND.STATUS_OPEN: + return 'Status: Open'; + case KIND.STATUS_APPLIED: + return 'Status: Applied'; + case KIND.STATUS_CLOSED: + return 'Status: Closed'; + case KIND.STATUS_DRAFT: + return 'Status: Draft'; + default: + return `Event (kind ${kind})`; + } + } + + // Get status from event tags + function getEventStatus(event: NostrEvent): string | null { + const statusTag = event.tags.find(t => t[0] === 'status' && t[1]); + if (statusTag?.[1]) { + return statusTag[1]; + } + + // Check if kind itself indicates status + if (event.kind === KIND.STATUS_OPEN) return 'open'; + if (event.kind === KIND.STATUS_APPLIED) return 'applied'; + if (event.kind === KIND.STATUS_CLOSED) return 'closed'; + if (event.kind === KIND.STATUS_DRAFT) return 'draft'; + + return null; + } + + // Check if event is git-related + function isGitEvent(kind: number): boolean { + const gitKinds = [ + KIND.ISSUE, + KIND.PULL_REQUEST, + KIND.PULL_REQUEST_UPDATE, + KIND.PATCH, + KIND.STATUS_OPEN, + KIND.STATUS_APPLIED, + KIND.STATUS_CLOSED, + KIND.STATUS_DRAFT + ]; + return gitKinds.includes(kind as any); + } + function getEventContext(event: NostrEvent): string { // Special handling for reaction events (kind 7) if (event.kind === 7) { @@ -1446,6 +1590,15 @@ i * } } + async function copyNpub() { + try { + await navigator.clipboard.writeText(npub); + alert('Npub copied to clipboard!'); + } catch (err) { + console.error('Failed to copy npub:', err); + } + } + const isOwnProfile = $derived(viewerPubkeyHex === profileOwnerPubkeyHex); // Sort payment targets with lightning first @@ -1496,6 +1649,14 @@ i * {/if}
{npub} +
@@ -1810,7 +1971,7 @@ i * {:else if part.type === 'profile' && part.pubkey} - + {:else} {part.value} {/if} @@ -1904,13 +2065,53 @@ i * {:else if part.type === 'profile' && part.pubkey} - + {:else} {part.value} {/if} {/each} {/if} + {:else if isGitEvent(event.kind)} + {@const repoInfo = getRepoInfo(event)} + {@const eventType = getGitEventTypeName(event.kind)} + {@const status = getEventStatus(event)} + {@const subjectTag = event.tags.find(t => t[0] === 'subject' && t[1])?.[1]} + {@const dTag = event.tags.find(t => t[0] === 'd' && t[1])?.[1]} + {@const rootETag = event.tags.find(t => t[0] === 'e' && t[3] === 'root')?.[1]} + {@const referencedGitEvent = rootETag ? getReferencedEvent(rootETag) : null} + {@const referencedSubject = referencedGitEvent ? referencedGitEvent.tags.find(t => t[0] === 'subject' && t[1])?.[1] : null} + {@const displaySubject = subjectTag || referencedSubject} +
+ {#if repoInfo} +
+ + {repoInfo.repoName} + + {eventType} + {#if status} + + {status} + + {/if} +
+ {:else} +
+ {eventType} + {#if status} + + {status} + + {/if} +
+ {/if} + {#if displaySubject} +
{displaySubject}
+ {/if} + {#if event.content && event.content.trim()} +
{event.content.trim().slice(0, 200)}{event.content.trim().length > 200 ? '...' : ''}
+ {/if} +
{:else} {@const eTag = event.tags.find(t => t[0] === 'e' && t[1])?.[1]} {@const aTag = event.tags.find(t => t[0] === 'a' && t[1])?.[1]} @@ -1939,7 +2140,7 @@ i * {:else if part.type === 'profile' && part.pubkey} - + {:else} {part.value} {/if} @@ -2142,6 +2343,62 @@ i * margin-bottom: 2rem; } + @media (max-width: 768px) { + .profile-header { + grid-template-columns: 1fr; + gap: 1.5rem; + padding: 1.5rem; + } + + .profile-avatar-section { + justify-self: center; + } + + .profile-info { + text-align: center; + } + + .profile-actions { + justify-content: center; + } + } + + @media (max-width: 480px) { + .profile-header { + padding: 1rem; + gap: 1rem; + } + + .profile-avatar, + .profile-avatar-placeholder { + width: 80px; + height: 80px; + } + + .profile-avatar-placeholder { + font-size: 2rem; + } + + .profile-name { + font-size: 1.5rem; + } + + .profile-bio { + font-size: 1rem; + } + + .profile-npub { + font-size: 0.75rem; + padding: 0.375rem 0.5rem; + word-break: break-all; + } + + .action-button { + padding: 0.5rem 1rem; + font-size: 0.875rem; + } + } + .profile-avatar-section { position: relative; } @@ -2188,6 +2445,17 @@ i * .profile-meta { margin-top: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: nowrap; + justify-content: center; + } + + @media (min-width: 769px) { + .profile-meta { + justify-content: flex-start; + } } .profile-npub { @@ -2198,6 +2466,14 @@ i * padding: 0.5rem 0.75rem; border-radius: 0.375rem; display: inline-block; + word-break: break-all; + flex: 0 1 auto; + min-width: 0; + } + + .copy-npub-button { + flex-shrink: 0; + flex-grow: 0; } .profile-actions { @@ -2626,6 +2902,18 @@ i * gap: 0.5rem; flex-wrap: wrap; flex: 1; + min-width: 0; + } + + .message-participants :global(.user-badge) { + flex-shrink: 0; + } + + @media (max-width: 768px) { + .message-participants :global(.user-badge) { + width: auto; + display: inline-flex; + } } .participants-label { @@ -2691,21 +2979,25 @@ i * margin-bottom: 1rem; padding: 0.75rem; background: var(--bg-secondary); - border-left: 3px solid var(--border-color); - border-radius: 0.5rem; + border: 1px solid var(--border-color); + border-left: 3px solid var(--accent, #007bff); + border-radius: 0.375rem; font-size: 0.875rem; } :global([data-theme="light"]) .quoted-event { - background: #e8e8e8; + background: #f5f5f5; + border-color: #e0e0e0; } :global([data-theme="dark"]) .quoted-event { - background: rgba(0, 0, 0, 0.2); + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.1); } :global([data-theme="black"]) .quoted-event { - background: #0a0a0a; + background: rgba(255, 255, 255, 0.03); + border-color: rgba(255, 255, 255, 0.1); } .quoted-event-header { @@ -2713,26 +3005,136 @@ i * align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; + flex-wrap: wrap; } .quoted-event-time { font-size: 0.75rem; color: var(--text-muted); + margin-left: auto; } .quoted-event-content { color: var(--text-secondary); white-space: pre-wrap; word-wrap: break-word; + overflow-wrap: break-word; line-height: 1.5; - max-height: 10rem; + max-height: 8rem; overflow: hidden; - text-overflow: ellipsis; + position: relative; + } + + .quoted-event-content::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2rem; + background: linear-gradient(to bottom, transparent, var(--bg-secondary)); + pointer-events: none; + } + + :global([data-theme="light"]) .quoted-event-content::after { + background: linear-gradient(to bottom, transparent, #f5f5f5); + } + + :global([data-theme="dark"]) .quoted-event-content::after { + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.05)); + } + + :global([data-theme="black"]) .quoted-event-content::after { + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.03)); } .quoted-event-loading { opacity: 0.6; font-style: italic; + color: var(--text-muted); + } + + /* Git Event Display */ + .git-event-display { + padding: 1rem; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-left: 3px solid var(--accent, #007bff); + border-radius: 0.5rem; + } + + .git-event-header { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; + margin-bottom: 0.75rem; + } + + .git-event-repo-link { + text-decoration: none; + color: inherit; + font-weight: 600; + transition: color 0.2s ease; + } + + .git-event-repo-link:hover { + color: var(--accent, #007bff); + } + + .git-event-repo-name { + font-size: 1rem; + color: var(--text-primary); + } + + .git-event-type { + padding: 0.25rem 0.75rem; + background: var(--accent-bg, #e7f3ff); + color: var(--accent, #007bff); + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + } + + .git-event-status { + padding: 0.25rem 0.75rem; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + } + + .git-event-status.status-open { + background: #d4edda; + color: #155724; + } + + .git-event-status.status-closed { + background: #f8d7da; + color: #721c24; + } + + .git-event-status.status-applied { + background: #d1ecf1; + color: #0c5460; + } + + .git-event-status.status-draft { + background: #fff3cd; + color: #856404; + } + + .git-event-subject { + font-size: 1rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.5rem; + } + + .git-event-content { + color: var(--text-secondary); + line-height: 1.6; + white-space: pre-wrap; + word-wrap: break-word; } /* Activity */ @@ -2921,6 +3323,9 @@ i * border: 1px solid var(--border-color); border-radius: 0.5rem; border-left: 3px solid var(--accent); + max-height: 250px; + overflow: hidden; + position: relative; } .repost-author { @@ -2939,7 +3344,34 @@ i * color: var(--text-primary); white-space: pre-wrap; word-wrap: break-word; + overflow-wrap: break-word; line-height: 1.6; + max-height: calc(250px - 3rem); + overflow: hidden; + position: relative; + } + + .repost-text::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2rem; + background: linear-gradient(to bottom, transparent, var(--bg-primary)); + pointer-events: none; + } + + :global([data-theme="light"]) .repost-text::after { + background: linear-gradient(to bottom, transparent, var(--bg-primary, #ffffff)); + } + + :global([data-theme="dark"]) .repost-text::after { + background: linear-gradient(to bottom, transparent, var(--bg-primary, #1a1a1a)); + } + + :global([data-theme="black"]) .repost-text::after { + background: linear-gradient(to bottom, transparent, var(--bg-primary, #000000)); } .referenced-event { @@ -3199,15 +3631,6 @@ i * padding: 1rem; } - .profile-header { - grid-template-columns: 1fr; - text-align: center; - } - - .profile-avatar-section { - justify-self: center; - } - .repo-grid { grid-template-columns: 1fr; } diff --git a/static/icons/git-commit.svg b/static/icons/git-commit.svg new file mode 100644 index 0000000..c7effb7 --- /dev/null +++ b/static/icons/git-commit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/icons/more-vertical.svg b/static/icons/more-vertical.svg new file mode 100644 index 0000000..373aab7 --- /dev/null +++ b/static/icons/more-vertical.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/icons/tag.svg b/static/icons/tag.svg new file mode 100644 index 0000000..acee0a7 --- /dev/null +++ b/static/icons/tag.svg @@ -0,0 +1,4 @@ + + + +