Monorepo for Tangled tangled.org

Compare changes

Choose any two refs to compare.

+196 -448
-23
appview/db/profile.go
··· 98 }) 99 } 100 101 - punchcard, err := MakePunchcard( 102 - e, 103 - orm.FilterEq("did", forDid), 104 - orm.FilterGte("date", time.Now().AddDate(0, -TimeframeMonths, 0)), 105 - ) 106 - if err != nil { 107 - return nil, fmt.Errorf("error getting commits by did: %w", err) 108 - } 109 - for _, punch := range punchcard.Punches { 110 - if punch.Date.After(now) { 111 - continue 112 - } 113 - 114 - monthsAgo := monthsBetween(punch.Date, now) 115 - if monthsAgo >= TimeframeMonths { 116 - // shouldn't happen; but times are weird 117 - continue 118 - } 119 - 120 - idx := monthsAgo 121 - timeline.ByMonth[idx].Commits += punch.Count 122 - } 123 - 124 return &timeline, nil 125 } 126
··· 98 }) 99 } 100 101 return &timeline, nil 102 } 103
-18
appview/pages/funcmap.go
··· 332 } 333 return dict, nil 334 }, 335 - "queryParams": func(params ...any) (url.Values, error) { 336 - if len(params)%2 != 0 { 337 - return nil, errors.New("invalid queryParams call") 338 - } 339 - vals := make(url.Values, len(params)/2) 340 - for i := 0; i < len(params); i += 2 { 341 - key, ok := params[i].(string) 342 - if !ok { 343 - return nil, errors.New("queryParams keys must be strings") 344 - } 345 - v, ok := params[i+1].(string) 346 - if !ok { 347 - return nil, errors.New("queryParams values must be strings") 348 - } 349 - vals.Add(key, v) 350 - } 351 - return vals, nil 352 - }, 353 "deref": func(v any) any { 354 val := reflect.ValueOf(v) 355 if val.Kind() == reflect.Pointer && !val.IsNil() {
··· 332 } 333 return dict, nil 334 }, 335 "deref": func(v any) any { 336 val := reflect.ValueOf(v) 337 if val.Kind() == reflect.Pointer && !val.IsNil() {
+1 -1
appview/pages/templates/banner.html
··· 30 <div class="mx-6"> 31 These services may not be fully accessible until upgraded. 32 <a class="underline text-red-800 dark:text-red-200" 33 - href="https://docs.tangled.org/migrating-knots-and-spindles.html"> 34 Click to read the upgrade guide</a>. 35 </div> 36 </details>
··· 30 <div class="mx-6"> 31 These services may not be fully accessible until upgraded. 32 <a class="underline text-red-800 dark:text-red-200" 33 + href="https://docs.tangled.org/migrating-knots-spindles.html#migrating-knots-spindles"> 34 Click to read the upgrade guide</a>. 35 </div> 36 </details>
+2 -2
appview/pages/templates/fragments/pagination.html
··· 1 {{ define "fragments/pagination" }} 2 - {{/* Params: Page (pagination.Page), TotalCount (int), BasePath (string), QueryParams (url.Values) */}} 3 {{ $page := .Page }} 4 {{ $totalCount := .TotalCount }} 5 {{ $basePath := .BasePath }} 6 - {{ $queryParams := safeUrl .QueryParams.Encode }} 7 8 {{ $prev := $page.Previous.Offset }} 9 {{ $next := $page.Next.Offset }}
··· 1 {{ define "fragments/pagination" }} 2 + {{/* Params: Page (pagination.Page), TotalCount (int), BasePath (string), QueryParams (string) */}} 3 {{ $page := .Page }} 4 {{ $totalCount := .TotalCount }} 5 {{ $basePath := .BasePath }} 6 + {{ $queryParams := .QueryParams }} 7 8 {{ $prev := $page.Previous.Offset }} 9 {{ $next := $page.Next.Offset }}
+1 -1
appview/pages/templates/labels/fragments/label.html
··· 24 {{ $rhs = printf "%s" $v }} 25 {{ end }} 26 27 - {{ $chipClasses := "w-fit flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm text-inherit" }} 28 29 {{ if $isDid }} 30 <a href="/{{ $resolvedVal }}" class="{{ $chipClasses }} no-underline hover:underline">
··· 24 {{ $rhs = printf "%s" $v }} 25 {{ end }} 26 27 + {{ $chipClasses := "w-fit flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm" }} 28 29 {{ if $isDid }} 30 <a href="/{{ $resolvedVal }}" class="{{ $chipClasses }} no-underline hover:underline">
+28 -35
appview/pages/templates/layouts/fragments/topbar.html
··· 53 /> 54 <span class="hidden md:inline">{{ $user | resolve | truncateAt30 }}</span> 55 </summary> 56 - <div class="absolute right-0 mt-4 rounded bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700 shadow-lg z-50 text-sm" style="width: 14rem;"> 57 {{ $active := .Active.Did }} 58 - {{ $linkStyle := "flex items-center gap-3 px-4 py-2 hover:no-underline hover:bg-gray-50 hover:dark:bg-gray-700/50" }} 59 60 {{ $others := .Accounts | otherAccounts $active }} 61 {{ if $others }} 62 - <div class="text-sm text-gray-500 dark:text-gray-400 px-3 py-1 pt-2">switch account</div> 63 - {{ range $others }} 64 <button 65 type="button" 66 hx-post="/account/switch" 67 hx-vals='{"did": "{{ .Did }}"}' 68 hx-swap="none" 69 - class="{{$linkStyle}} w-full text-left pl-3" 70 > 71 - <img src="{{ tinyAvatar .Did }}" alt="" class="rounded-full size-6 flex-shrink-0 border border-gray-300 dark:border-gray-700" /> 72 - <span class="truncate flex-1">{{ .Did | resolve }}</span> 73 </button> 74 - {{ end }} 75 {{ end }} 76 77 - <a href="/login?mode=add_account" class="{{$linkStyle}} pl-3"> 78 - <div class="size-6 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center"> 79 - {{ i "plus" "size-3" }} 80 - </div> 81 - 82 - <div class="text-left flex-1 min-w-0 block truncate"> 83 - add account 84 - </div> 85 </a> 86 87 - <div class="border-t border-gray-200 dark:border-gray-700"> 88 - <a href="/{{ $active }}" class="{{$linkStyle}}"> 89 - {{ i "user" "size-4" }} 90 - profile 91 - </a> 92 - <a href="/{{ $active }}?tab=repos" class="{{$linkStyle}}"> 93 - {{ i "book-marked" "size-4" }} 94 - repositories 95 - </a> 96 - <a href="/{{ $active }}?tab=strings" class="{{$linkStyle}}"> 97 - {{ i "line-squiggle" "size-4" }} 98 - strings 99 - </a> 100 - <a href="/settings" class="{{$linkStyle}}"> 101 - {{ i "cog" "size-4" }} 102 - settings 103 - </a> 104 <a href="#" 105 hx-post="/logout" 106 hx-swap="none" 107 - class="{{$linkStyle}} text-red-400 hover:text-red-400 hover:bg-red-100 dark:hover:bg-red-700/20 pb-2"> 108 - {{ i "log-out" "size-4" }} 109 logout 110 </a> 111 </div>
··· 53 /> 54 <span class="hidden md:inline">{{ $user | resolve | truncateAt30 }}</span> 55 </summary> 56 + <div class="absolute right-0 mt-4 p-4 rounded bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700 shadow-lg z-50" style="width: 14rem;"> 57 {{ $active := .Active.Did }} 58 + 59 + <div class="pb-2 mb-2 border-b border-gray-200 dark:border-gray-700"> 60 + <div class="flex items-center gap-2"> 61 + <img src="{{ tinyAvatar $active }}" alt="" class="rounded-full h-8 w-8 flex-shrink-0 border border-gray-300 dark:border-gray-700" /> 62 + <div class="flex-1 overflow-hidden"> 63 + <p class="font-medium text-sm truncate">{{ $active | resolve }}</p> 64 + <p class="text-xs text-green-600 dark:text-green-400">active</p> 65 + </div> 66 + </div> 67 + </div> 68 69 {{ $others := .Accounts | otherAccounts $active }} 70 {{ if $others }} 71 + <div class="pb-2 mb-2 border-b border-gray-200 dark:border-gray-700"> 72 + <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">Switch Account</p> 73 + {{ range $others }} 74 <button 75 type="button" 76 hx-post="/account/switch" 77 hx-vals='{"did": "{{ .Did }}"}' 78 hx-swap="none" 79 + class="flex items-center gap-2 w-full py-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-left" 80 > 81 + <img src="{{ tinyAvatar .Did }}" alt="" class="rounded-full h-6 w-6 flex-shrink-0 border border-gray-300 dark:border-gray-700" /> 82 + <span class="text-sm truncate flex-1">{{ .Did | resolve }}</span> 83 </button> 84 + {{ end }} 85 + </div> 86 {{ end }} 87 88 + <a href="/login?mode=add_account" class="flex items-center gap-2 py-1 text-sm"> 89 + {{ i "plus" "w-4 h-4 flex-shrink-0" }} 90 + <span>Add another account</span> 91 </a> 92 93 + <div class="pt-2 mt-2 border-t border-gray-200 dark:border-gray-700 space-y-1"> 94 + <a href="/{{ $active }}" class="block py-1 text-sm">profile</a> 95 + <a href="/{{ $active }}?tab=repos" class="block py-1 text-sm">repositories</a> 96 + <a href="/{{ $active }}?tab=strings" class="block py-1 text-sm">strings</a> 97 + <a href="/settings" class="block py-1 text-sm">settings</a> 98 <a href="#" 99 hx-post="/logout" 100 hx-swap="none" 101 + class="block py-1 text-sm text-red-400 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 102 logout 103 </a> 104 </div>
+4 -38
appview/pages/templates/repo/fragments/diff.html
··· 4 #filesToggle:checked ~ div label[for="filesToggle"] .hide-text { display: inline; } 5 #filesToggle:not(:checked) ~ div label[for="filesToggle"] .hide-text { display: none; } 6 #filesToggle:checked ~ div div#files { width: fit-content; max-width: 15vw; margin-right: 1rem; } 7 - #filesToggle:not(:checked) ~ div div#files { width: 0; display: none; margin-right: 0; } 8 </style> 9 10 {{ template "diffTopbar" . }} ··· 14 {{ define "diffTopbar" }} 15 {{ $diff := index . 0 }} 16 {{ $opts := index . 1 }} 17 - {{ $root := "" }} 18 - {{ if gt (len .) 2 }} 19 - {{ $root = index . 2 }} 20 - {{ end }} 21 22 {{ block "filesCheckbox" $ }} {{ end }} 23 {{ block "subsCheckbox" $ }} {{ end }} 24 25 <!-- top bar --> 26 - <div class="sticky top-0 z-30 bg-slate-100 dark:bg-gray-900 flex items-center gap-2 col-span-full h-12 p-2 {{ if $root }}mt-4{{ end }}"> 27 <!-- left panel toggle --> 28 {{ template "filesToggle" . }} 29 ··· 31 {{ $stat := $diff.Stats }} 32 {{ $count := len $diff.ChangedFiles }} 33 {{ template "repo/fragments/diffStatPill" $stat }} 34 - <span class="text-xs text-gray-600 dark:text-gray-400 hidden md:inline-flex">{{ $count }} changed file{{ if ne $count 1 }}s{{ end }}</span> 35 - 36 - {{ if $root }} 37 - {{ if $root.IsInterdiff }} 38 - <!-- interdiff indicator --> 39 - <div class="flex items-center gap-2 before:content-['|'] before:text-gray-300 dark:before:text-gray-600 before:mr-2"> 40 - <span class="text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide">Interdiff</span> 41 - <a 42 - href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ sub $root.ActiveRound 1 }}" 43 - class="px-2 py-0.5 bg-white dark:bg-gray-700 rounded font-mono text-xs hover:bg-gray-50 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600" 44 - > 45 - #{{ sub $root.ActiveRound 1 }} 46 - </a> 47 - <span class="text-gray-400 text-xs">โ†’</span> 48 - <a 49 - href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ $root.ActiveRound }}" 50 - class="px-2 py-0.5 bg-white dark:bg-gray-700 rounded font-mono text-xs hover:bg-gray-50 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600" 51 - > 52 - #{{ $root.ActiveRound }} 53 - </a> 54 - </div> 55 - {{ else if ne $root.ActiveRound nil }} 56 - <!-- diff round indicator --> 57 - <div class="flex items-center gap-2 before:content-['|'] before:text-gray-300 dark:before:text-gray-600 before:mr-2"> 58 - <span class="text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide">Diff</span> 59 - <span class="px-2 py-0.5 bg-white dark:bg-gray-700 rounded font-mono text-xs border border-gray-300 dark:border-gray-600"> 60 - <span class="hidden md:inline">round </span>#{{ $root.ActiveRound }} 61 - </span> 62 - </div> 63 - {{ end }} 64 - {{ end }} 65 66 <!-- spacer --> 67 <div class="flex-grow"></div> ··· 171 {{ end }} 172 173 {{ define "collapseToggle" }} 174 - <label 175 title="Expand/Collapse diffs" 176 for="collapseToggle" 177 class="btn font-normal normal-case p-2"
··· 4 #filesToggle:checked ~ div label[for="filesToggle"] .hide-text { display: inline; } 5 #filesToggle:not(:checked) ~ div label[for="filesToggle"] .hide-text { display: none; } 6 #filesToggle:checked ~ div div#files { width: fit-content; max-width: 15vw; margin-right: 1rem; } 7 + #filesToggle:not(:checked) ~ div div#files { width: 0; display: hidden; margin-right: 0; } 8 </style> 9 10 {{ template "diffTopbar" . }} ··· 14 {{ define "diffTopbar" }} 15 {{ $diff := index . 0 }} 16 {{ $opts := index . 1 }} 17 18 {{ block "filesCheckbox" $ }} {{ end }} 19 {{ block "subsCheckbox" $ }} {{ end }} 20 21 <!-- top bar --> 22 + <div class="sticky top-0 z-30 bg-slate-100 dark:bg-gray-900 flex items-center gap-2 col-span-full h-12 p-2"> 23 <!-- left panel toggle --> 24 {{ template "filesToggle" . }} 25 ··· 27 {{ $stat := $diff.Stats }} 28 {{ $count := len $diff.ChangedFiles }} 29 {{ template "repo/fragments/diffStatPill" $stat }} 30 + {{ $count }} changed file{{ if ne $count 1 }}s{{ end }} 31 32 <!-- spacer --> 33 <div class="flex-grow"></div> ··· 137 {{ end }} 138 139 {{ define "collapseToggle" }} 140 + <label 141 title="Expand/Collapse diffs" 142 for="collapseToggle" 143 class="btn font-normal normal-case p-2"
+3 -3
appview/pages/templates/repo/fragments/splitDiff.html
··· 1 {{ define "repo/fragments/splitDiff" }} 2 {{ $name := .Id }} 3 - {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 group-target/line:bg-yellow-200/30 group-target/line:dark:bg-yellow-600/30" -}} 4 - {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline group-target/line:text-black group-target/line:dark:text-white" -}} 5 {{- $lineNrSepStyle := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 6 - {{- $containerStyle := "inline-flex w-full items-center target:bg-yellow-200/50 target:dark:bg-yellow-700/50 scroll-mt-48 group/line" -}} 7 {{- $emptyStyle := "bg-gray-200/30 dark:bg-gray-700/30" -}} 8 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400" -}} 9 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}}
··· 1 {{ define "repo/fragments/splitDiff" }} 2 {{ $name := .Id }} 3 + {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800" -}} 4 + {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 5 {{- $lineNrSepStyle := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 6 + {{- $containerStyle := "inline-flex w-full items-center target:bg-yellow-200 target:dark:bg-yellow-700 scroll-mt-48" -}} 7 {{- $emptyStyle := "bg-gray-200/30 dark:bg-gray-700/30" -}} 8 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400" -}} 9 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}}
+3 -3
appview/pages/templates/repo/fragments/unifiedDiff.html
··· 3 <div class="overflow-x-auto font-mono leading-normal"><div class="overflow-x-auto"><div class="inline-flex flex-col min-w-full">{{- range .TextFragments -}}<span class="block bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</span> 4 {{- $oldStart := .OldPosition -}} 5 {{- $newStart := .NewPosition -}} 6 - {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 group-target/line:bg-yellow-200/30 group-target/line:dark:bg-yellow-600/30" -}} 7 - {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline group-target/line:text-black group-target/line:dark:text-white" -}} 8 {{- $lineNrSepStyle1 := "" -}} 9 {{- $lineNrSepStyle2 := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 10 - {{- $containerStyle := "inline-flex w-full items-center target:bg-yellow-200/30 target:dark:bg-yellow-700/30 scroll-mt-48 group/line" -}} 11 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 " -}} 12 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} 13 {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}}
··· 3 <div class="overflow-x-auto font-mono leading-normal"><div class="overflow-x-auto"><div class="inline-flex flex-col min-w-full">{{- range .TextFragments -}}<span class="block bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</span> 4 {{- $oldStart := .OldPosition -}} 5 {{- $newStart := .NewPosition -}} 6 + {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 target:bg-yellow-200 target:dark:bg-yellow-600" -}} 7 + {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 8 {{- $lineNrSepStyle1 := "" -}} 9 {{- $lineNrSepStyle2 := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 10 + {{- $containerStyle := "inline-flex w-full items-center target:bg-yellow-200 target:dark:bg-yellow-700 scroll-mt-48" -}} 11 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 " -}} 12 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} 13 {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}}
+1 -1
appview/pages/templates/repo/issues/issues.html
··· 80 "Page" .Page 81 "TotalCount" .IssueCount 82 "BasePath" (printf "/%s/issues" .RepoInfo.FullName) 83 - "QueryParams" (queryParams "state" $state "q" .FilterQuery) 84 ) }} 85 {{ end }} 86 {{ end }}
··· 80 "Page" .Page 81 "TotalCount" .IssueCount 82 "BasePath" (printf "/%s/issues" .RepoInfo.FullName) 83 + "QueryParams" (printf "state=%s&q=%s" $state .FilterQuery) 84 ) }} 85 {{ end }} 86 {{ end }}
+1 -1
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
··· 12 > 13 <textarea 14 name="body" 15 - class="w-full p-2 rounded border" 16 rows=8 17 placeholder="Add to the discussion..."></textarea 18 >
··· 12 > 13 <textarea 14 name="body" 15 + class="w-full p-2 rounded border border-gray-200" 16 rows=8 17 placeholder="Add to the discussion..."></textarea 18 >
+70 -106
appview/pages/templates/repo/pulls/pull.html
··· 8 9 {{ define "mainLayout" }} 10 <div class="px-1 flex-grow flex flex-col gap-4"> 11 - <div class="max-w-full md:max-w-screen-lg mx-auto"> 12 {{ block "contentLayout" . }} 13 {{ block "content" . }}{{ end }} 14 {{ end }} ··· 22 <script> 23 (function() { 24 const details = document.getElementById('bottomSheet'); 25 - const backdrop = document.getElementById('bottomSheetBackdrop'); 26 const isDesktop = () => window.matchMedia('(min-width: 768px)').matches; 27 28 - // function to update backdrop 29 - const updateBackdrop = () => { 30 - if (backdrop) { 31 - if (details.open && !isDesktop()) { 32 - backdrop.classList.remove('opacity-0', 'pointer-events-none'); 33 - backdrop.classList.add('opacity-100', 'pointer-events-auto'); 34 - } else { 35 - backdrop.classList.remove('opacity-100', 'pointer-events-auto'); 36 - backdrop.classList.add('opacity-0', 'pointer-events-none'); 37 - } 38 - } 39 - }; 40 - 41 // close on mobile initially 42 if (!isDesktop()) { 43 details.open = false; 44 } 45 - updateBackdrop(); // initialize backdrop 46 47 // prevent closing on desktop 48 details.addEventListener('toggle', function(e) { 49 if (isDesktop() && !this.open) { 50 this.open = true; 51 } 52 - updateBackdrop(); 53 }); 54 55 const mediaQuery = window.matchMedia('(min-width: 768px)'); ··· 61 // switched to mobile - close 62 details.open = false; 63 } 64 - updateBackdrop(); 65 }); 66 - 67 - // close when clicking backdrop 68 - if (backdrop) { 69 - backdrop.addEventListener('click', () => { 70 - if (!isDesktop()) { 71 - details.open = false; 72 - } 73 - }); 74 - } 75 })(); 76 </script> 77 {{ end }} 78 79 {{ define "repoContentLayout" }} 80 - <div class="grid grid-cols-1 md:grid-cols-10 gap-4 w-full"> 81 <section class="bg-white col-span-1 md:col-span-8 dark:bg-gray-800 p-6 rounded relative w-full mx-auto dark:text-white h-full flex-shrink"> 82 {{ block "repoContent" . }}{{ end }} 83 </section> ··· 117 <div class="flex col-span-full"> 118 <!-- left panel --> 119 <div id="files" class="w-0 hidden md:block overflow-hidden sticky top-12 max-h-screen overflow-y-auto pb-12"> 120 - <section class="overflow-x-auto text-sm px-6 py-2 border-b border-x border-gray-200 dark:border-gray-700 w-full mx-auto min-h-full rounded-b rounded-t-none bg-white dark:bg-gray-800 drop-shadow-sm"> 121 {{ template "repo/fragments/fileTree" $diff.FileTree }} 122 </section> 123 </div> ··· 135 {{ define "subsPanel" }} 136 {{ $root := index . 2 }} 137 {{ $pull := $root.Pull }} 138 <!-- backdrop overlay - only visible on mobile when open --> 139 - <div id="bottomSheetBackdrop" class="fixed inset-0 bg-black/50 md:hidden opacity-0 pointer-events-none transition-opacity duration-300 z-40"></div> 140 <!-- right panel - bottom sheet on mobile, side panel on desktop --> 141 <div id="subs" class="fixed bottom-0 left-0 right-0 z-50 w-full md:static md:z-auto md:max-h-screen md:sticky md:top-12 overflow-hidden"> 142 - <details open id="bottomSheet" class="rounded-t-2xl md:rounded-t drop-shadow-lg md:drop-shadow-none group/panel"> 143 <summary class=" 144 flex gap-4 items-center justify-between 145 - rounded-t-2xl md:rounded-t cursor-pointer list-none p-4 md:h-12 146 text-white md:text-black md:dark:text-white 147 - bg-green-600 dark:bg-green-700 148 md:bg-white md:dark:bg-gray-800 149 drop-shadow-sm 150 - border-t md:border-x md:border-t-0 border-gray-200 dark:border-gray-700"> 151 - <h2 class="">Comments</h2> 152 {{ template "subsPanelSummary" $ }} 153 </summary> 154 <div class="max-h-[85vh] md:max-h-[calc(100vh-3rem-3rem)] w-full flex flex-col-reverse gap-4 overflow-y-auto bg-slate-100 dark:bg-gray-900 md:bg-transparent"> ··· 163 {{ $pull := $root.Pull }} 164 {{ $latest := $pull.LastRoundNumber }} 165 <div class="flex items-center gap-2 text-sm"> 166 <span class="md:hidden inline"> 167 - <span class="inline group-open/panel:hidden">{{ i "chevron-up" "size-4" }}</span> 168 - <span class="hidden group-open/panel:inline">{{ i "chevron-down" "size-4" }}</span> 169 </span> 170 </div> 171 {{ end }} ··· 177 {{ define "subsToggle" }} 178 <style> 179 /* Mobile: full width */ 180 - #subsToggle:checked ~ div div#subs { 181 width: 100%; 182 margin-left: 0; 183 } ··· 187 188 /* Desktop: 25vw with left margin */ 189 @media (min-width: 768px) { 190 - #subsToggle:checked ~ div div#subs { 191 width: 25vw; 192 margin-left: 1rem; 193 } ··· 208 209 {{ define "submissions" }} 210 {{ $lastIdx := sub (len .Pull.Submissions) 1 }} 211 - {{ if not .LoggedInUser }} 212 - {{ template "loginPrompt" $ }} 213 - {{ end }} 214 {{ range $ridx, $item := reverse .Pull.Submissions }} 215 {{ $idx := sub $lastIdx $ridx }} 216 {{ template "submission" (list $item $idx $lastIdx $) }} ··· 222 {{ $idx := index . 1 }} 223 {{ $lastIdx := index . 2 }} 224 {{ $root := index . 3 }} 225 - <div class="{{ if eq $item.RoundNumber 0 }}rounded-b border-t-0{{ else }}rounded{{ end }} border border-gray-200 dark:border-gray-700 w-full shadow-sm bg-gray-50 dark:bg-gray-900"> 226 {{ template "submissionHeader" $ }} 227 {{ template "submissionComments" $ }} 228 </div> 229 {{ end }} 230 ··· 233 {{ $lastIdx := index . 2 }} 234 {{ $root := index . 3 }} 235 {{ $round := $item.RoundNumber }} 236 - <div class=" 237 - {{ if eq $round 0 }}rounded-b{{ else }}rounded{{ end }} 238 - px-6 py-4 pr-2 pt-2 239 - {{ if eq $root.ActiveRound $round }} 240 - bg-blue-100 dark:bg-blue-900 border-b border-blue-200 dark:border-blue-700 241 - {{ else }} 242 - bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 243 - {{ end }} 244 - flex gap-2 sticky top-0 z-20"> 245 <!-- left column: just profile picture --> 246 <div class="flex-shrink-0 pt-2"> 247 <img ··· 269 {{ $round := $item.RoundNumber }} 270 <div class="flex gap-2 items-center justify-between mb-1"> 271 <span class="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 pt-2"> 272 - {{ resolve $root.Pull.OwnerDid }} submitted 273 - <span class="px-2 py-0.5 {{ if eq $root.ActiveRound $round }}text-white bg-blue-600 dark:bg-blue-500 border-blue-700 dark:border-blue-600{{ else }}text-black dark:text-white bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600{{ end }} rounded font-mono text-xs border"> 274 - #{{ $round }} 275 - </span> 276 <span class="select-none before:content-['\00B7']"></span> 277 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500" href="#round-#{{ $round }}"> 278 - {{ template "repo/fragments/shortTime" $item.Created }} 279 </a> 280 </span> 281 <div class="flex gap-2 items-center"> 282 {{ if ne $root.ActiveRound $round }} 283 <a class="btn-flat flex items-center gap-2 no-underline hover:no-underline text-sm" 284 - href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ $round }}?{{ safeUrl $root.DiffOpts.Encode }}#round-#{{ $round }}"> 285 {{ i "diff" "w-4 h-4" }} 286 diff 287 </a> ··· 492 493 {{ define "submissionComments" }} 494 {{ $item := index . 0 }} 495 - {{ $idx := index . 1 }} 496 - {{ $lastIdx := index . 2 }} 497 - {{ $root := index . 3 }} 498 - {{ $c := len $item.Comments }} 499 - <details class="relative ml-10 group/comments" open> 500 - <summary class="cursor-pointer list-none"> 501 - <div class="hidden group-open/comments:block absolute -left-8 top-0 bottom-0 w-16 transition-colors flex items-center justify-center group/border z-4"> 502 - <div class="absolute left-1/2 -translate-x-1/2 top-0 bottom-0 w-0.5 group-open/comments:bg-gray-200 dark:group-open/comments:bg-gray-700 group-hover/border:bg-gray-400 dark:group-hover/border:bg-gray-500 transition-colors"> </div> 503 - </div> 504 - <div class="group-open/comments:hidden block relative group/summary py-4"> 505 - <div class="absolute -left-8 top-0 bottom-0 w-16 transition-colors flex items-center justify-center z-4"> 506 - <div class="absolute left-1/2 -translate-x-1/2 h-1/3 top-0 bottom-0 w-0.5 bg-gray-200 dark:bg-gray-700 group-hover/summary:bg-gray-400 dark:group-hover/summary:bg-gray-500 transition-colors"></div> 507 - </div> 508 - <span class="text-gray-500 dark:text-gray-400 text-sm group-hover/summary:text-gray-600 dark:group-hover/summary:text-gray-300 transition-colors flex items-center gap-2 -ml-2 relative"> 509 - {{ i "circle-plus" "size-4 z-5" }} 510 - expand {{ $c }} comment{{ if ne $c 1 }}s{{ end }} 511 - </span> 512 - </div> 513 - </summary> 514 - <div> 515 - {{ range $item.Comments }} 516 - {{ template "submissionComment" . }} 517 - {{ end }} 518 - </div> 519 - 520 - <div class="relative -ml-10"> 521 - {{ if eq $lastIdx $item.RoundNumber }} 522 - {{ block "mergeStatus" $root }} {{ end }} 523 - {{ block "resubmitStatus" $root }} {{ end }} 524 - {{ end }} 525 - </div> 526 - <div class="relative -ml-10 bg-gray-50 dark:bg-gray-900"> 527 - {{ if $root.LoggedInUser }} 528 - {{ template "repo/pulls/fragments/pullActions" 529 - (dict 530 - "LoggedInUser" $root.LoggedInUser 531 - "Pull" $root.Pull 532 - "RepoInfo" $root.RepoInfo 533 - "RoundNumber" $item.RoundNumber 534 - "MergeCheck" $root.MergeCheck 535 - "ResubmitCheck" $root.ResubmitCheck 536 - "BranchDeleteStatus" $root.BranchDeleteStatus 537 - "Stack" $root.Stack) }} 538 - {{ end }} 539 - </div> 540 - </details> 541 {{ end }} 542 543 {{ define "submissionComment" }} 544 <div id="comment-{{.ID}}" class="flex gap-2 -ml-4 py-4 w-full mx-auto"> 545 <!-- left column: profile picture --> 546 - <div class="flex-shrink-0 h-fit relative"> 547 <img 548 src="{{ tinyAvatar .OwnerDid }}" 549 alt="" 550 - class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-5" 551 /> 552 </div> 553 <!-- right column: name and body in two rows -->
··· 8 9 {{ define "mainLayout" }} 10 <div class="px-1 flex-grow flex flex-col gap-4"> 11 + <div class="max-w-screen-lg mx-auto"> 12 {{ block "contentLayout" . }} 13 {{ block "content" . }}{{ end }} 14 {{ end }} ··· 22 <script> 23 (function() { 24 const details = document.getElementById('bottomSheet'); 25 const isDesktop = () => window.matchMedia('(min-width: 768px)').matches; 26 27 // close on mobile initially 28 if (!isDesktop()) { 29 details.open = false; 30 } 31 32 // prevent closing on desktop 33 details.addEventListener('toggle', function(e) { 34 if (isDesktop() && !this.open) { 35 this.open = true; 36 } 37 }); 38 39 const mediaQuery = window.matchMedia('(min-width: 768px)'); ··· 45 // switched to mobile - close 46 details.open = false; 47 } 48 }); 49 })(); 50 </script> 51 {{ end }} 52 53 {{ define "repoContentLayout" }} 54 + <div class="grid grid-cols-1 md:grid-cols-10 gap-4"> 55 <section class="bg-white col-span-1 md:col-span-8 dark:bg-gray-800 p-6 rounded relative w-full mx-auto dark:text-white h-full flex-shrink"> 56 {{ block "repoContent" . }}{{ end }} 57 </section> ··· 91 <div class="flex col-span-full"> 92 <!-- left panel --> 93 <div id="files" class="w-0 hidden md:block overflow-hidden sticky top-12 max-h-screen overflow-y-auto pb-12"> 94 + <section class="overflow-x-auto text-sm px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto min-h-full rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 95 {{ template "repo/fragments/fileTree" $diff.FileTree }} 96 </section> 97 </div> ··· 109 {{ define "subsPanel" }} 110 {{ $root := index . 2 }} 111 {{ $pull := $root.Pull }} 112 + 113 <!-- backdrop overlay - only visible on mobile when open --> 114 + <div class=" 115 + fixed inset-0 bg-black/50 z-50 md:hidden opacity-0 116 + pointer-events-none transition-opacity duration-300 117 + has-[~#subs_details[open]]:opacity-100 has-[~#subs_details[open]]:pointer-events-auto"> 118 + </div> 119 <!-- right panel - bottom sheet on mobile, side panel on desktop --> 120 <div id="subs" class="fixed bottom-0 left-0 right-0 z-50 w-full md:static md:z-auto md:max-h-screen md:sticky md:top-12 overflow-hidden"> 121 + <details open id="bottomSheet" class="group rounded-t-2xl md:rounded-t-sm drop-shadow-lg md:drop-shadow-none"> 122 <summary class=" 123 flex gap-4 items-center justify-between 124 + rounded-t-2xl md:rounded-t-sm cursor-pointer list-none p-4 md:h-12 125 text-white md:text-black md:dark:text-white 126 + bg-green-600 dark:bg-green-600 127 md:bg-white md:dark:bg-gray-800 128 drop-shadow-sm 129 + md:border-b md:border-x border-gray-200 dark:border-gray-700"> 130 + <h2 class="">Review Panel </h2> 131 {{ template "subsPanelSummary" $ }} 132 </summary> 133 <div class="max-h-[85vh] md:max-h-[calc(100vh-3rem-3rem)] w-full flex flex-col-reverse gap-4 overflow-y-auto bg-slate-100 dark:bg-gray-900 md:bg-transparent"> ··· 142 {{ $pull := $root.Pull }} 143 {{ $latest := $pull.LastRoundNumber }} 144 <div class="flex items-center gap-2 text-sm"> 145 + {{ if $root.IsInterdiff }} 146 + <span> 147 + viewing interdiff of 148 + <span class="font-mono">#{{ $root.ActiveRound }}</span> 149 + and 150 + <span class="font-mono">#{{ sub $root.ActiveRound 1 }}</span> 151 + </span> 152 + {{ else }} 153 + <span> 154 + viewing round 155 + <span class="font-mono">#{{ $root.ActiveRound }}</span> 156 + </span> 157 + {{ if ne $root.ActiveRound $latest }} 158 + <span>(outdated)</span> 159 + <span class="before:content-['ยท']"></span> 160 + <a class="underline" href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ $latest }}?{{ safeUrl $root.DiffOpts.Encode }}"> 161 + view latest 162 + </a> 163 + {{ end }} 164 + {{ end }} 165 <span class="md:hidden inline"> 166 + <span class="inline group-open:hidden">{{ i "chevron-up" "size-4" }}</span> 167 + <span class="hidden group-open:inline">{{ i "chevron-down" "size-4" }}</span> 168 </span> 169 </div> 170 {{ end }} ··· 176 {{ define "subsToggle" }} 177 <style> 178 /* Mobile: full width */ 179 + #subsToggle:checked ~ div div#subs { 180 width: 100%; 181 margin-left: 0; 182 } ··· 186 187 /* Desktop: 25vw with left margin */ 188 @media (min-width: 768px) { 189 + #subsToggle:checked ~ div div#subs { 190 width: 25vw; 191 margin-left: 1rem; 192 } ··· 207 208 {{ define "submissions" }} 209 {{ $lastIdx := sub (len .Pull.Submissions) 1 }} 210 {{ range $ridx, $item := reverse .Pull.Submissions }} 211 {{ $idx := sub $lastIdx $ridx }} 212 {{ template "submission" (list $item $idx $lastIdx $) }} ··· 218 {{ $idx := index . 1 }} 219 {{ $lastIdx := index . 2 }} 220 {{ $root := index . 3 }} 221 + <div class="rounded border border-gray-200 dark:border-gray-700 w-full shadow-sm bg-gray-50 dark:bg-gray-800/50"> 222 {{ template "submissionHeader" $ }} 223 {{ template "submissionComments" $ }} 224 + 225 + {{ if eq $lastIdx $item.RoundNumber }} 226 + {{ block "mergeStatus" $root }} {{ end }} 227 + {{ block "resubmitStatus" $root }} {{ end }} 228 + {{ end }} 229 + 230 + {{ if $root.LoggedInUser }} 231 + {{ template "repo/pulls/fragments/pullActions" 232 + (dict 233 + "LoggedInUser" $root.LoggedInUser 234 + "Pull" $root.Pull 235 + "RepoInfo" $root.RepoInfo 236 + "RoundNumber" $item.RoundNumber 237 + "MergeCheck" $root.MergeCheck 238 + "ResubmitCheck" $root.ResubmitCheck 239 + "BranchDeleteStatus" $root.BranchDeleteStatus 240 + "Stack" $root.Stack) }} 241 + {{ else }} 242 + {{ template "loginPrompt" $ }} 243 + {{ end }} 244 </div> 245 {{ end }} 246 ··· 249 {{ $lastIdx := index . 2 }} 250 {{ $root := index . 3 }} 251 {{ $round := $item.RoundNumber }} 252 + <div class="rounded px-6 py-4 pr-2 pt-2 bg-white dark:bg-gray-800 flex gap-2 sticky top-0 z-20 border-b border-gray-200 dark:border-gray-700"> 253 <!-- left column: just profile picture --> 254 <div class="flex-shrink-0 pt-2"> 255 <img ··· 277 {{ $round := $item.RoundNumber }} 278 <div class="flex gap-2 items-center justify-between mb-1"> 279 <span class="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 pt-2"> 280 + {{ resolve $root.Pull.OwnerDid }} submitted v{{ $round }} 281 <span class="select-none before:content-['\00B7']"></span> 282 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500" href="#round-#{{ $round }}"> 283 + {{ template "repo/fragments/shortTimeAgo" $item.Created }} 284 </a> 285 </span> 286 <div class="flex gap-2 items-center"> 287 {{ if ne $root.ActiveRound $round }} 288 <a class="btn-flat flex items-center gap-2 no-underline hover:no-underline text-sm" 289 + href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ $round }}?{{ safeUrl $root.DiffOpts.Encode }}"> 290 {{ i "diff" "w-4 h-4" }} 291 diff 292 </a> ··· 497 498 {{ define "submissionComments" }} 499 {{ $item := index . 0 }} 500 + <div class="relative ml-10 border-l-2 border-gray-200 dark:border-gray-700"> 501 + {{ range $item.Comments }} 502 + {{ template "submissionComment" . }} 503 + {{ end }} 504 + </div> 505 {{ end }} 506 507 {{ define "submissionComment" }} 508 <div id="comment-{{.ID}}" class="flex gap-2 -ml-4 py-4 w-full mx-auto"> 509 <!-- left column: profile picture --> 510 + <div class="flex-shrink-0"> 511 <img 512 src="{{ tinyAvatar .OwnerDid }}" 513 alt="" 514 + class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900" 515 /> 516 </div> 517 <!-- right column: name and body in two rows -->
+1 -1
appview/pages/templates/repo/pulls/pulls.html
··· 166 "Page" .Page 167 "TotalCount" .PullCount 168 "BasePath" (printf "/%s/pulls" .RepoInfo.FullName) 169 - "QueryParams" (queryParams "state" .FilteringBy.String "q" .FilterQuery) 170 ) }} 171 {{ end }} 172 {{ end }}
··· 166 "Page" .Page 167 "TotalCount" .PullCount 168 "BasePath" (printf "/%s/pulls" .RepoInfo.FullName) 169 + "QueryParams" (printf "state=%s&q=%s" .FilteringBy.String .FilterQuery) 170 ) }} 171 {{ end }} 172 {{ end }}
+16 -24
appview/pages/templates/strings/string.html
··· 10 11 {{ define "content" }} 12 {{ $ownerId := resolve .Owner.DID.String }} 13 - <section id="string-header" class="mb-2 py-2 px-4 dark:text-white"> 14 - <div class="text-lg flex flex-col sm:flex-row items-start gap-4 justify-between"> 15 - <!-- left items --> 16 - <div class="flex flex-col gap-2"> 17 - <!-- string owner / string name --> 18 - <div class="flex items-center gap-2 flex-wrap"> 19 - {{ template "user/fragments/picHandleLink" .Owner.DID.String }} 20 - <span class="select-none">/</span> 21 - <a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a> 22 - </div> 23 - 24 - <span class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-600 dark:text-gray-300"> 25 - {{ if .String.Description }} 26 - {{ .String.Description }} 27 - {{ else }} 28 - <span class="italic">this string has no description</span> 29 - {{ end }} 30 - </span> 31 </div> 32 - 33 - <div class="w-full sm:w-fit grid grid-cols-3 gap-2 z-auto"> 34 {{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }} 35 - <a class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group" 36 hx-boost="true" 37 href="/strings/{{ .String.Did }}/{{ .String.Rkey }}/edit"> 38 - {{ i "pencil" "w-4 h-4" }} 39 <span class="hidden md:inline">edit</span> 40 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 41 </a> 42 <button 43 - class="btn text-sm text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 flex items-center gap-2 group" 44 title="Delete string" 45 hx-delete="/strings/{{ .String.Did }}/{{ .String.Rkey }}/" 46 hx-swap="none" 47 hx-confirm="Are you sure you want to delete the string `{{ .String.Filename }}`?" 48 > 49 - {{ i "trash-2" "w-4 h-4" }} 50 <span class="hidden md:inline">delete</span> 51 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 52 </button> ··· 57 "StarCount" .StarCount) }} 58 </div> 59 </div> 60 </section> 61 <section class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full dark:text-white"> 62 <div class="flex flex-col md:flex-row md:justify-between md:items-center text-gray-500 dark:text-gray-400 text-sm md:text-base pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-700">
··· 10 11 {{ define "content" }} 12 {{ $ownerId := resolve .Owner.DID.String }} 13 + <section id="string-header" class="mb-4 py-2 px-6 dark:text-white"> 14 + <div class="text-lg flex items-center justify-between"> 15 + <div> 16 + <a href="/strings/{{ $ownerId }}">{{ $ownerId }}</a> 17 + <span class="select-none">/</span> 18 + <a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a> 19 </div> 20 + <div class="flex gap-2 items-stretch text-base"> 21 {{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }} 22 + <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 23 hx-boost="true" 24 href="/strings/{{ .String.Did }}/{{ .String.Rkey }}/edit"> 25 + {{ i "pencil" "size-4" }} 26 <span class="hidden md:inline">edit</span> 27 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 28 </a> 29 <button 30 + class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group p-2" 31 title="Delete string" 32 hx-delete="/strings/{{ .String.Did }}/{{ .String.Rkey }}/" 33 hx-swap="none" 34 hx-confirm="Are you sure you want to delete the string `{{ .String.Filename }}`?" 35 > 36 + {{ i "trash-2" "size-4" }} 37 <span class="hidden md:inline">delete</span> 38 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 39 </button> ··· 44 "StarCount" .StarCount) }} 45 </div> 46 </div> 47 + <span> 48 + {{ with .String.Description }} 49 + {{ . }} 50 + {{ end }} 51 + </span> 52 </section> 53 <section class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full dark:text-white"> 54 <div class="flex flex-col md:flex-row md:justify-between md:items-center text-gray-500 dark:text-gray-400 text-sm md:text-base pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-700">
+1 -1
appview/pulls/opengraph.go
··· 199 currentX += commentTextWidth + 40 200 201 // Draw files changed 202 - err = statusStatsArea.DrawLucideIcon("file-diff", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 203 if err != nil { 204 log.Printf("failed to draw file diff icon: %v", err) 205 }
··· 199 currentX += commentTextWidth + 40 200 201 // Draw files changed 202 + err = statusStatsArea.DrawLucideIcon("static/icons/file-diff", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 203 if err != nil { 204 log.Printf("failed to draw file diff icon: %v", err) 205 }
+1 -1
appview/pulls/pulls.go
··· 228 reactionMap, err := db.GetReactionMap(s.db, 20, pull.AtUri()) 229 if err != nil { 230 log.Println("failed to get pull reactions") 231 } 232 233 userReactions := map[models.ReactionKind]bool{} ··· 1874 record := pull.AsRecord() 1875 record.PatchBlob = blob.Blob 1876 record.CreatedAt = time.Now().Format(time.RFC3339) 1877 - record.Source.Sha = newSourceRev 1878 1879 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1880 Collection: tangled.RepoPullNSID,
··· 228 reactionMap, err := db.GetReactionMap(s.db, 20, pull.AtUri()) 229 if err != nil { 230 log.Println("failed to get pull reactions") 231 + s.pages.Notice(w, "pulls", "Failed to load pull. Try again later.") 232 } 233 234 userReactions := map[models.ReactionKind]bool{} ··· 1875 record := pull.AsRecord() 1876 record.PatchBlob = blob.Blob 1877 record.CreatedAt = time.Now().Format(time.RFC3339) 1878 1879 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1880 Collection: tangled.RepoPullNSID,
+19 -64
appview/repo/archive.go
··· 2 3 import ( 4 "fmt" 5 - "io" 6 "net/http" 7 "net/url" 8 "strings" 9 10 "github.com/go-chi/chi/v5" 11 ) 12 13 func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { ··· 25 scheme = "https" 26 } 27 host := fmt.Sprintf("%s://%s", scheme, f.Knot) 28 didSlashRepo := f.DidSlashRepo() 29 - 30 - // build the xrpc url 31 - u, err := url.Parse(host) 32 - if err != nil { 33 - l.Error("failed to parse host URL", "err", err) 34 rp.pages.Error503(w) 35 return 36 } 37 - 38 - u.Path = "/xrpc/sh.tangled.repo.archive" 39 - query := url.Values{} 40 - query.Set("format", "tar.gz") 41 - query.Set("prefix", r.URL.Query().Get("prefix")) 42 - query.Set("ref", ref) 43 - query.Set("repo", didSlashRepo) 44 - u.RawQuery = query.Encode() 45 - 46 - xrpcURL := u.String() 47 - 48 - // make the get request 49 - resp, err := http.Get(xrpcURL) 50 - if err != nil { 51 - l.Error("failed to call XRPC repo.archive", "err", err) 52 - rp.pages.Error503(w) 53 - return 54 - } 55 - 56 - // pass through headers from upstream response 57 - if contentDisposition := resp.Header.Get("Content-Disposition"); contentDisposition != "" { 58 - w.Header().Set("Content-Disposition", contentDisposition) 59 - } 60 - if contentType := resp.Header.Get("Content-Type"); contentType != "" { 61 - w.Header().Set("Content-Type", contentType) 62 - } 63 - if contentLength := resp.Header.Get("Content-Length"); contentLength != "" { 64 - w.Header().Set("Content-Length", contentLength) 65 - } 66 - if link := resp.Header.Get("Link"); link != "" { 67 - if resolvedRef, err := extractImmutableLink(link); err == nil { 68 - newLink := fmt.Sprintf("<%s/%s/archive/%s.tar.gz>; rel=\"immutable\"", 69 - rp.config.Core.AppviewHost, f.DidSlashRepo(), resolvedRef) 70 - w.Header().Set("Link", newLink) 71 - } 72 - } 73 - 74 - // stream the archive data directly 75 - if _, err := io.Copy(w, resp.Body); err != nil { 76 - l.Error("failed to write response", "err", err) 77 - } 78 - } 79 - 80 - func extractImmutableLink(linkHeader string) (string, error) { 81 - trimmed := strings.TrimPrefix(linkHeader, "<") 82 - trimmed = strings.TrimSuffix(trimmed, ">; rel=\"immutable\"") 83 - 84 - parsedLink, err := url.Parse(trimmed) 85 - if err != nil { 86 - return "", err 87 - } 88 - 89 - resolvedRef := parsedLink.Query().Get("ref") 90 - if resolvedRef == "" { 91 - return "", fmt.Errorf("no ref found in link header") 92 - } 93 - 94 - return resolvedRef, nil 95 }
··· 2 3 import ( 4 "fmt" 5 "net/http" 6 "net/url" 7 "strings" 8 9 + "tangled.org/core/api/tangled" 10 + xrpcclient "tangled.org/core/appview/xrpcclient" 11 + 12 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 13 "github.com/go-chi/chi/v5" 14 + "github.com/go-git/go-git/v5/plumbing" 15 ) 16 17 func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { ··· 29 scheme = "https" 30 } 31 host := fmt.Sprintf("%s://%s", scheme, f.Knot) 32 + xrpcc := &indigoxrpc.Client{ 33 + Host: host, 34 + } 35 didSlashRepo := f.DidSlashRepo() 36 + archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, didSlashRepo) 37 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 38 + l.Error("failed to call XRPC repo.archive", "err", xrpcerr) 39 rp.pages.Error503(w) 40 return 41 } 42 + // Set headers for file download, just pass along whatever the knot specifies 43 + safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 44 + filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename) 45 + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 46 + w.Header().Set("Content-Type", "application/gzip") 47 + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes))) 48 + // Write the archive data directly 49 + w.Write(archiveBytes) 50 }
+1 -1
appview/state/knotstream.go
··· 122 if ce == nil { 123 continue 124 } 125 - if ce.Email == ke.Address || ce.Email == record.CommitterDid { 126 count += int(ce.Count) 127 } 128 }
··· 122 if ce == nil { 123 continue 124 } 125 + if ce.Email == ke.Address { 126 count += int(ce.Count) 127 } 128 }
+11
appview/state/profile.go
··· 162 l.Error("failed to create timeline", "err", err) 163 } 164 165 s.pages.ProfileOverview(w, pages.ProfileOverviewParams{ 166 LoggedInUser: s.oauth.GetMultiAccountUser(r), 167 Card: profile,
··· 162 l.Error("failed to create timeline", "err", err) 163 } 164 165 + // populate commit counts in the timeline, using the punchcard 166 + now := time.Now() 167 + for _, p := range profile.Punchcard.Punches { 168 + years := now.Year() - p.Date.Year() 169 + months := int(now.Month() - p.Date.Month()) 170 + monthsAgo := years*12 + months 171 + if monthsAgo >= 0 && monthsAgo < len(timeline.ByMonth) { 172 + timeline.ByMonth[monthsAgo].Commits += p.Count 173 + } 174 + } 175 + 176 s.pages.ProfileOverview(w, pages.ProfileOverviewParams{ 177 LoggedInUser: s.oauth.GetMultiAccountUser(r), 178 Card: profile,
+10 -23
cmd/dolly/main.go
··· 2 3 import ( 4 "bytes" 5 - _ "embed" 6 "flag" 7 "fmt" 8 "image" ··· 17 "github.com/srwiley/oksvg" 18 "github.com/srwiley/rasterx" 19 "golang.org/x/image/draw" 20 "tangled.org/core/ico" 21 ) 22 23 func main() { 24 var ( 25 - size string 26 - fillColor string 27 - output string 28 - templatePath string 29 ) 30 31 - flag.StringVar(&templatePath, "template", "", "Path to dolly go-html template") 32 flag.StringVar(&size, "size", "512x512", "Output size in format WIDTHxHEIGHT (e.g., 512x512)") 33 flag.StringVar(&fillColor, "color", "#000000", "Fill color in hex format (e.g., #FF5733)") 34 flag.StringVar(&output, "output", "dolly.svg", "Output file path (format detected from extension: .svg, .png, or .ico)") 35 flag.Parse() 36 - 37 - if templatePath == "" { 38 - fmt.Fprintf(os.Stderr, "Empty template path") 39 - os.Exit(1) 40 - } 41 42 width, height, err := parseSize(size) 43 if err != nil { ··· 59 os.Exit(1) 60 } 61 62 - tpl, err := os.ReadFile(templatePath) 63 - if err != nil { 64 - fmt.Fprintf(os.Stderr, "Failed to read template from path %s: %v\n", templatePath, err) 65 - os.Exit(1) 66 - } 67 - 68 - svgData, err := dolly(string(tpl), fillColor) 69 if err != nil { 70 fmt.Fprintf(os.Stderr, "Error generating SVG: %v\n", err) 71 os.Exit(1) ··· 97 fmt.Printf("Successfully generated %s (%dx%d)\n", output, width, height) 98 } 99 100 - func dolly(tplString, hexColor string) ([]byte, error) { 101 - tpl, err := template.New("dolly").Parse(tplString) 102 if err != nil { 103 return nil, err 104 } 105 106 var svgData bytes.Buffer 107 - if err := tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", map[string]any{ 108 - "FillColor": hexColor, 109 - "Classes": "", 110 }); err != nil { 111 return nil, err 112 }
··· 2 3 import ( 4 "bytes" 5 "flag" 6 "fmt" 7 "image" ··· 16 "github.com/srwiley/oksvg" 17 "github.com/srwiley/rasterx" 18 "golang.org/x/image/draw" 19 + "tangled.org/core/appview/pages" 20 "tangled.org/core/ico" 21 ) 22 23 func main() { 24 var ( 25 + size string 26 + fillColor string 27 + output string 28 ) 29 30 flag.StringVar(&size, "size", "512x512", "Output size in format WIDTHxHEIGHT (e.g., 512x512)") 31 flag.StringVar(&fillColor, "color", "#000000", "Fill color in hex format (e.g., #FF5733)") 32 flag.StringVar(&output, "output", "dolly.svg", "Output file path (format detected from extension: .svg, .png, or .ico)") 33 flag.Parse() 34 35 width, height, err := parseSize(size) 36 if err != nil { ··· 52 os.Exit(1) 53 } 54 55 + svgData, err := dolly(fillColor) 56 if err != nil { 57 fmt.Fprintf(os.Stderr, "Error generating SVG: %v\n", err) 58 os.Exit(1) ··· 84 fmt.Printf("Successfully generated %s (%dx%d)\n", output, width, height) 85 } 86 87 + func dolly(hexColor string) ([]byte, error) { 88 + tpl, err := template.New("dolly"). 89 + ParseFS(pages.Files, "templates/fragments/dolly/logo.html") 90 if err != nil { 91 return nil, err 92 } 93 94 var svgData bytes.Buffer 95 + if err := tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", pages.DollyParams{ 96 + FillColor: hexColor, 97 }); err != nil { 98 return nil, err 99 }
+3 -36
docs/DOCS.md
··· 375 KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555 376 ``` 377 378 - If you run a Linux distribution that uses systemd, you can 379 - use the provided service file to run the server. Copy 380 - [`knotserver.service`](https://tangled.org/tangled.org/core/blob/master/systemd/knotserver.service) 381 to `/etc/systemd/system/`. Then, run: 382 383 ``` ··· 692 NODE_ENV: "production" 693 MY_ENV_VAR: "MY_ENV_VALUE" 694 ``` 695 - 696 - By default, the following environment variables set: 697 - 698 - - `CI` - Always set to `true` to indicate a CI environment 699 - - `TANGLED_PIPELINE_ID` - The AT URI of the current pipeline 700 - - `TANGLED_REPO_KNOT` - The repository's knot hostname 701 - - `TANGLED_REPO_DID` - The DID of the repository owner 702 - - `TANGLED_REPO_NAME` - The name of the repository 703 - - `TANGLED_REPO_DEFAULT_BRANCH` - The default branch of the 704 - repository 705 - - `TANGLED_REPO_URL` - The full URL to the repository 706 - 707 - These variables are only available when the pipeline is 708 - triggered by a push: 709 - 710 - - `TANGLED_REF` - The full git reference (e.g., 711 - `refs/heads/main` or `refs/tags/v1.0.0`) 712 - - `TANGLED_REF_NAME` - The short name of the reference 713 - (e.g., `main` or `v1.0.0`) 714 - - `TANGLED_REF_TYPE` - The type of reference, either 715 - `branch` or `tag` 716 - - `TANGLED_SHA` - The commit SHA that triggered the pipeline 717 - - `TANGLED_COMMIT_SHA` - Alias for `TANGLED_SHA` 718 - 719 - These variables are only available when the pipeline is 720 - triggered by a pull request: 721 - 722 - - `TANGLED_PR_SOURCE_BRANCH` - The source branch of the pull 723 - request 724 - - `TANGLED_PR_TARGET_BRANCH` - The target branch of the pull 725 - request 726 - - `TANGLED_PR_SOURCE_SHA` - The commit SHA of the source 727 - branch 728 729 ### Steps 730
··· 375 KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555 376 ``` 377 378 + If you run a Linux distribution that uses systemd, you can use the provided 379 + service file to run the server. Copy 380 + [`knotserver.service`](/systemd/knotserver.service) 381 to `/etc/systemd/system/`. Then, run: 382 383 ``` ··· 692 NODE_ENV: "production" 693 MY_ENV_VAR: "MY_ENV_VALUE" 694 ``` 695 696 ### Steps 697
+1 -1
input.css
··· 96 @apply border border-gray-400 block rounded bg-gray-50 focus:ring-black p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400; 97 } 98 textarea { 99 - @apply border border-gray-400 block rounded bg-gray-50 focus:outline-none focus:ring-1 focus:ring-black p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400; 100 } 101 details summary::-webkit-details-marker { 102 display: none;
··· 96 @apply border border-gray-400 block rounded bg-gray-50 focus:ring-black p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400; 97 } 98 textarea { 99 + @apply border border-gray-400 block rounded bg-gray-50 focus:ring-black p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400; 100 } 101 details summary::-webkit-details-marker { 102 display: none;
-4
knotserver/git/git.go
··· 76 return &g, nil 77 } 78 79 - func (g *GitRepo) Hash() plumbing.Hash { 80 - return g.h 81 - } 82 - 83 // re-open a repository and update references 84 func (g *GitRepo) Refresh() error { 85 refreshed, err := PlainOpen(g.path)
··· 76 return &g, nil 77 } 78 79 // re-open a repository and update references 80 func (g *GitRepo) Refresh() error { 81 refreshed, err := PlainOpen(g.path)
-35
knotserver/xrpc/repo_archive.go
··· 4 "compress/gzip" 5 "fmt" 6 "net/http" 7 - "net/url" 8 "strings" 9 10 "github.com/go-git/go-git/v5/plumbing" 11 12 - "tangled.org/core/api/tangled" 13 "tangled.org/core/knotserver/git" 14 xrpcerr "tangled.org/core/xrpc/errors" 15 ) ··· 49 repoParts := strings.Split(repo, "/") 50 repoName := repoParts[len(repoParts)-1] 51 52 - immutableLink, err := x.buildImmutableLink(repo, format, gr.Hash().String(), prefix) 53 - if err != nil { 54 - x.Logger.Error( 55 - "failed to build immutable link", 56 - "err", err.Error(), 57 - "repo", repo, 58 - "format", format, 59 - "ref", gr.Hash().String(), 60 - "prefix", prefix, 61 - ) 62 - } 63 - 64 safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 65 66 var archivePrefix string ··· 73 filename := fmt.Sprintf("%s-%s.tar.gz", repoName, safeRefFilename) 74 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 75 w.Header().Set("Content-Type", "application/gzip") 76 - w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink)) 77 78 gw := gzip.NewWriter(w) 79 defer gw.Close() ··· 94 return 95 } 96 } 97 - 98 - func (x *Xrpc) buildImmutableLink(repo string, format string, ref string, prefix string) (string, error) { 99 - scheme := "https" 100 - if x.Config.Server.Dev { 101 - scheme = "http" 102 - } 103 - 104 - u, err := url.Parse(scheme + "://" + x.Config.Server.Hostname + "/xrpc/" + tangled.RepoArchiveNSID) 105 - if err != nil { 106 - return "", err 107 - } 108 - 109 - params := url.Values{} 110 - params.Set("repo", repo) 111 - params.Set("format", format) 112 - params.Set("ref", ref) 113 - params.Set("prefix", prefix) 114 - 115 - return fmt.Sprintf("%s?%s", u.String(), params.Encode()), nil 116 - }
··· 4 "compress/gzip" 5 "fmt" 6 "net/http" 7 "strings" 8 9 "github.com/go-git/go-git/v5/plumbing" 10 11 "tangled.org/core/knotserver/git" 12 xrpcerr "tangled.org/core/xrpc/errors" 13 ) ··· 47 repoParts := strings.Split(repo, "/") 48 repoName := repoParts[len(repoParts)-1] 49 50 safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 51 52 var archivePrefix string ··· 59 filename := fmt.Sprintf("%s-%s.tar.gz", repoName, safeRefFilename) 60 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 61 w.Header().Set("Content-Type", "application/gzip") 62 63 gw := gzip.NewWriter(w) 64 defer gw.Close() ··· 79 return 80 } 81 }
+18 -25
nix/pkgs/dolly.nix
··· 1 { 2 - lib, 3 buildGoApplication, 4 modules, 5 - writeShellScriptBin, 6 - }: let 7 - src = lib.fileset.toSource { 8 - root = ../..; 9 - fileset = lib.fileset.unions [ 10 - ../../go.mod 11 - ../../ico 12 - ../../cmd/dolly/main.go 13 - ../../appview/pages/templates/fragments/dolly/logo.html 14 - ]; 15 - }; 16 - dolly-unwrapped = buildGoApplication { 17 - pname = "dolly-unwrapped"; 18 - version = "0.1.0"; 19 - inherit src modules; 20 - doCheck = false; 21 - subPackages = ["cmd/dolly"]; 22 - }; 23 - in 24 - writeShellScriptBin "dolly" '' 25 - exec ${dolly-unwrapped}/bin/dolly \ 26 - -template ${src}/appview/pages/templates/fragments/dolly/logo.html \ 27 - "$@" 28 - ''
··· 1 { 2 buildGoApplication, 3 modules, 4 + src, 5 + }: 6 + buildGoApplication { 7 + pname = "dolly"; 8 + version = "0.1.0"; 9 + inherit src modules; 10 + 11 + # patch the static dir 12 + postUnpack = '' 13 + pushd source 14 + mkdir -p appview/pages/static 15 + touch appview/pages/static/x 16 + popd 17 + ''; 18 + 19 + doCheck = false; 20 + subPackages = ["cmd/dolly"]; 21 + }