Monorepo for Tangled tangled.org
5

Configure Feed

Select the types of activity you want to include in your feed.

appview: use markdownEditor fragment for all markdown body inputs

Signed-off-by: Seongmin Lee <git@boltless.me>

author
Seongmin Lee
date (Jun 21, 2026, 8:28 PM +0900) commit 8fad3408 parent b20e461b change-id upxxpvvr
+112 -124
+6 -5
appview/pages/templates/fragments/comment/edit.html
··· 8 8 hx-disabled-elt="find button[type='submit']" 9 9 > 10 10 <input name="aturi" type="hidden" value="{{ .Comment.AtUri }}"> 11 - <textarea 12 - name="body" 13 - class="w-full p-2 rounded border border-gray-200 dark:border-gray-700" 14 - rows="5" 15 - autofocus>{{ .Comment.EditableBody }}</textarea> 11 + {{ template "fragments/markdownEditor" 12 + (dict "Name" "body" 13 + "Value" .Comment.EditableBody 14 + "Placeholder" "Describe this pull request. Markdown is supported.") }} 16 15 <div id="comment-error" class="error"></div> 17 16 {{ template "editActions" $ }} 18 17 </form> ··· 42 41 hx-get="/comment?aturi={{ .Comment.AtUri }}" 43 42 hx-target="closest form" 44 43 hx-swap="outerHTML" 44 + hx-indicator="this" 45 + hx-disabled-elt="this" 45 46 > 46 47 {{ i "x" "size-4" }} 47 48 Cancel
+6 -6
appview/pages/templates/fragments/comment/reply.html
··· 8 8 hx-disabled-elt="find button[type='submit']" 9 9 > 10 10 {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }} 11 - <textarea 12 - name="body" 13 - class="w-full p-2" 14 - placeholder="Leave a reply..." 15 - autofocus 16 - rows="3"></textarea> 11 + {{ template "fragments/markdownEditor" 12 + (dict "Name" "body" 13 + "BlobName" "blob" 14 + "Rows" 3 15 + "AutoFocus" true 16 + "Placeholder" "Leave a reply...") }} 17 17 {{ template "replyActions" . }} 18 18 </form> 19 19 {{ end }}
+1 -1
appview/pages/templates/fragments/line-quote-button.html
··· 18 18 const btnEnd = document.getElementById('line-quote-btn-end'); 19 19 20 20 const textarea = () => 21 - document.getElementById('comment-textarea'); 21 + document.querySelector('form[hx-post="/comment"] textarea'); 22 22 23 23 const lineOf = (el) => 24 24 el?.closest?.('span[id*="-O"]')
+66
appview/pages/templates/fragments/markdownEditor.html
··· 1 + {{ define "fragments/markdownEditor" }} 2 + {{ $name := .Name }} 3 + {{ $value := .Value }} 4 + {{ $blobName := .BlobName }} 5 + {{ $blobValues := .BlobValues }} 6 + {{ $rows := (or .Rows 5) }} 7 + {{ $required := .Required }} 8 + {{ $autofocus := .AutoFocus }} 9 + {{ $placeholder := .Placeholder }} 10 + <div class="flex flex-col gap-2" data-md-editor> 11 + <div class="btn-group self-start text-gray-600 dark:text-gray-300"> 12 + <button type="button" data-md-mode="write" 13 + class="btn-group-item active group"> 14 + {{ i "pencil" "w-3.5 h-3.5" }} 15 + Write 16 + </button> 17 + <button type="button" data-md-mode="preview" 18 + hx-post="/markup/preview" 19 + hx-vals='js:{body: this.closest("[data-md-editor]").querySelector("textarea").value}' 20 + hx-params="body" 21 + hx-target="next [data-md-preview]" 22 + hx-swap="innerHTML" 23 + hx-indicator="this" 24 + hx-disabled-elt="this" 25 + class="btn-group-item group"> 26 + {{ i "eye" "w-3.5 h-3.5 inline group-[.htmx-request]:hidden" }} 27 + {{ i "loader-circle" "w-3.5 h-3.5 animate-spin hidden group-[.htmx-request]:inline" }} 28 + Preview 29 + </button> 30 + </div> 31 + <div data-md-panel="write"> 32 + <textarea 33 + name="{{ $name }}" 34 + rows="{{ $rows }}" 35 + class="w-full resize-y dark:bg-gray-800 dark:text-white dark:border-gray-700" 36 + {{if $required}}required{{end}} 37 + {{if $autofocus}}autofocus{{end}} 38 + placeholder="{{ $placeholder }}" 39 + >{{with $value}}{{.}}{{end}}</textarea> 40 + </div> 41 + <div data-md-panel="preview" class="hidden"> 42 + <div data-md-preview class="min-h-[6rem] p-3 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-900/30"> 43 + <span class="text-gray-400 dark:text-gray-500 italic">Loading preview...</span> 44 + </div> 45 + </div> 46 + </div> 47 + <script> 48 + (() => { 49 + if (window.__mdEditorWired) return; 50 + window.__mdEditorWired = true; 51 + document.body.addEventListener('click', (e) => { 52 + const btn = e.target.closest('[data-md-mode]'); 53 + if (!btn) return; 54 + const editor = btn.closest('[data-md-editor]'); 55 + if (!editor) return; 56 + const mode = btn.dataset.mdMode; 57 + editor.querySelectorAll('[data-md-panel]').forEach(p => { 58 + p.classList.toggle('hidden', p.dataset.mdPanel !== mode); 59 + }); 60 + editor.querySelectorAll('[data-md-mode]').forEach(b => { 61 + b.classList.toggle('active', b === btn); 62 + }); 63 + }); 64 + })(); 65 + </script> 66 + {{ end }}
+7 -12
appview/pages/templates/repo/issues/fragments/newComment.html
··· 3 3 <form 4 4 hx-post="/comment" 5 5 hx-swap="none" 6 - hx-trigger="submit, click from:#close-button, keydown[commentButtonEnabled() && (ctrlKey || metaKey) && key=='Enter'] from:#comment-textarea" 6 + hx-trigger="submit, click from:#close-button, keydown[commentButtonEnabled() && (ctrlKey || metaKey) && key=='Enter'] from:(find textarea)" 7 7 hx-disabled-elt="find button[type='submit']" 8 8 hx-on::before-request="document.getElementById('comment-error').innerHTML = ''" 9 9 hx-swap="none" 10 10 class="group/form " 11 11 > 12 12 <input name="subject-uri" type="hidden" value="{{ .Issue.AtUri }}"> 13 - <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full border border-gray-200 dark:border-gray-700"> 13 + <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full border border-gray-200 dark:border-gray-700" hx-on:keyup="updateCommentForm()"> 14 14 <div class="text-sm pb-2 text-gray-500 dark:text-gray-400"> 15 15 {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }} 16 16 </div> 17 - <textarea 18 - id="comment-textarea" 19 - name="body" 20 - class="w-full p-2 rounded" 21 - placeholder="Add to the discussion. Markdown is supported." 22 - onkeyup="updateCommentForm()" 23 - rows="5" 24 - required 25 - ></textarea> 17 + {{ template "fragments/markdownEditor" 18 + (dict "Name" "body" 19 + "BlobName" "blob" 20 + "Placeholder" "Add to the discussion. Markdown is supported.") }} 26 21 </div> 27 22 28 23 <div class="flex gap-2 mt-2 items-center"> ··· 85 80 } 86 81 87 82 function updateCommentForm() { 88 - const textarea = document.getElementById('comment-textarea'); 83 + const textarea = document.querySelector('form[hx-post="/comment"] textarea'); 89 84 const commentButton = document.getElementById('comment-button'); 90 85 const closeButtonText = document.getElementById('close-button-text'); 91 86
+6 -7
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
··· 10 10 > 11 11 <input name="subject-uri" type="hidden" value="{{ .Pull.AtUri }}"> 12 12 <input name="pull-round-idx" type="hidden" value="{{ .RoundNumber }}"> 13 - <textarea 14 - id="comment-textarea" 15 - name="body" 16 - class="w-full p-2 rounded border" 17 - rows=8 18 - placeholder="Add to the discussion..."></textarea 19 - > 13 + {{ template "fragments/markdownEditor" 14 + (dict "Name" "body" 15 + "BlobName" "blob" 16 + "Rows" 8 17 + "Required" true 18 + "Placeholder" "Add to the discussion...") }} 20 19 {{ template "replyActions" . }} 21 20 <div id="pull-comment"></div> 22 21 </form>
+2 -74
appview/pages/templates/repo/pulls/fragments/pullStepDetails.html
··· 1 1 {{ define "repo/pulls/fragments/pullStepDetails" }} 2 2 {{ $hasSidePanel := and .LabelDefs .RepoInfo.Roles.IsPushAllowed }} 3 - {{ $previewUrl := printf "/%s/pulls/new/preview" .RepoInfo.FullName }} 4 3 {{ $labelCtx := dict "Defs" .LabelDefs "State" .LabelState "RepoInfo" .RepoInfo "Subject" "" "LoggedInUser" .LoggedInUser }} 5 4 6 5 <section class="flex flex-col md:flex-row gap-6"> 7 6 <div class="flex-1 min-w-0 flex flex-col gap-4"> 8 - {{ template "pullStepDetailsSingle" (dict "Root" . "PreviewUrl" $previewUrl) }} 7 + {{ template "pullStepDetailsSingle" (dict "Root" .) }} 9 8 {{ template "pullSubmitRow" . }} 10 9 </div> 11 10 ··· 16 15 </aside> 17 16 {{ end }} 18 17 </section> 19 - 20 - {{ template "markdownEditorScript" }} 21 18 {{ end }} 22 19 23 20 {{ define "pullStepDetailsSingle" }} 24 21 {{ $root := .Root }} 25 - {{ $previewUrl := .PreviewUrl }} 26 22 <div class="flex flex-col gap-1"> 27 23 <label for="title" class="text-xs tracking-wide text-gray-800 dark:text-gray-200">Title</label> 28 24 <input ··· 35 31 /> 36 32 </div> 37 33 38 - {{ template "markdownEditor" (dict 39 - "Id" "pull-body" 34 + {{ template "fragments/markdownEditor" (dict 40 35 "Name" "body" 41 36 "Value" $root.Body 42 37 "Rows" 6 43 38 "Placeholder" "Describe your change. Markdown is supported." 44 - "PreviewUrl" $previewUrl 45 39 ) }} 46 40 {{ end }} 47 41 48 - {{ define "markdownEditor" }} 49 - {{ $id := .Id }} 50 - {{ $name := .Name }} 51 - {{ $value := .Value }} 52 - {{ $rows := .Rows }} 53 - {{ $placeholder := .Placeholder }} 54 - {{ $previewUrl := .PreviewUrl }} 55 - <div class="flex flex-col gap-2" data-md-editor="{{ $id }}"> 56 - <div class="btn-group self-start text-gray-600 dark:text-gray-300"> 57 - <button type="button" data-md-mode="write" 58 - class="btn-group-item active group"> 59 - {{ i "pencil" "w-3.5 h-3.5" }} 60 - Write 61 - </button> 62 - <button type="button" data-md-mode="preview" 63 - hx-post="{{ $previewUrl }}" 64 - hx-vals='js:{body: document.querySelector("[data-md-editor=\"{{ $id }}\"] textarea").value}' 65 - hx-params="body" 66 - hx-target="[data-md-editor='{{ $id }}'] [data-md-preview]" 67 - hx-swap="innerHTML" 68 - hx-indicator="this" 69 - class="btn-group-item group"> 70 - {{ i "eye" "w-3.5 h-3.5 inline group-[.htmx-request]:hidden" }} 71 - {{ i "loader-circle" "w-3.5 h-3.5 animate-spin hidden group-[.htmx-request]:inline" }} 72 - Preview 73 - </button> 74 - </div> 75 - <div data-md-panel="write"> 76 - <textarea 77 - id="{{ $id }}" 78 - name="{{ $name }}" 79 - rows="{{ $rows }}" 80 - class="w-full resize-y dark:bg-gray-800 dark:text-white dark:border-gray-700" 81 - placeholder="{{ $placeholder }}" 82 - >{{ $value }}</textarea> 83 - </div> 84 - <div data-md-panel="preview" class="hidden"> 85 - <div data-md-preview class="min-h-[6rem] p-3 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-900/30"> 86 - <span class="text-gray-400 dark:text-gray-500 italic">Loading preview...</span> 87 - </div> 88 - </div> 89 - </div> 90 - {{ end }} 91 - 92 42 {{ define "pullSubmitRow" }} 93 43 <div class="flex items-center justify-end gap-4 mt-auto"> 94 44 {{ if and .MergeCheck .MergeCheck.IsConflicted }} ··· 119 69 </button> 120 70 </div> 121 71 {{ end }} 122 - 123 - {{ define "markdownEditorScript" }} 124 - <script> 125 - (() => { 126 - if (window.__mdEditorWired) return; 127 - window.__mdEditorWired = true; 128 - document.body.addEventListener('click', (e) => { 129 - const btn = e.target.closest('[data-md-mode]'); 130 - if (!btn) return; 131 - const editor = btn.closest('[data-md-editor]'); 132 - if (!editor) return; 133 - const mode = btn.dataset.mdMode; 134 - editor.querySelectorAll('[data-md-panel]').forEach(p => { 135 - p.classList.toggle('hidden', p.dataset.mdPanel !== mode); 136 - }); 137 - editor.querySelectorAll('[data-md-mode]').forEach(b => { 138 - b.classList.toggle('active', b === btn); 139 - }); 140 - }); 141 - })(); 142 - </script> 143 - {{ end }}
+1 -5
appview/pages/templates/repo/pulls/fragments/pullStepReview.html
··· 98 98 {{ define "pullReviewStackedCommits" }} 99 99 {{ $root := . }} 100 100 {{ $commits := .Comparison.FormatPatch }} 101 - {{ $previewUrl := printf "/%s/pulls/new/preview" .RepoInfo.FullName }} 102 101 {{ $hasSidePanel := and $root.LabelDefs $root.RepoInfo.Roles.IsPushAllowed }} 103 102 <ul class="flex flex-col gap-2"> 104 103 {{ range $idx, $p := $commits }} ··· 148 147 placeholder="{{ $p.Title }}" 149 148 /> 150 149 </div> 151 - {{ template "markdownEditor" (dict 152 - "Id" (printf "stack-body-%s" $cid) 150 + {{ template "fragments/markdownEditor" (dict 153 151 "Name" $bodyName 154 152 "Value" $bodyValue 155 153 "Rows" 4 156 154 "Placeholder" "Describe this pull request. Markdown is supported." 157 - "LabelText" "description" 158 - "PreviewUrl" $previewUrl 159 155 ) }} 160 156 </div> 161 157 {{ if $hasSidePanel }}
+5 -7
appview/pages/templates/strings/string.html
··· 103 103 <div class="text-sm pb-2 text-gray-500 dark:text-gray-400"> 104 104 {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }} 105 105 </div> 106 - <textarea 107 - name="body" 108 - class="w-full p-2 rounded" 109 - placeholder="Add to the discussion. Markdown is supported." 110 - rows="5" 111 - required 112 - ></textarea> 106 + {{ template "fragments/markdownEditor" 107 + (dict "Name" "body" 108 + "BlobName" "blob" 109 + "Required" true 110 + "Placeholder" "Add to the discussion. Markdown is supported.") }} 113 111 <div id="comment-error" class="error"></div> 114 112 </div> 115 113 <div class="flex gap-2 mt-2">
-5
appview/pulls/compose.go
··· 156 156 } 157 157 } 158 158 159 - func (s *Pulls) MarkdownPreview(w http.ResponseWriter, r *http.Request) { 160 - body := r.FormValue("body") 161 - s.pages.MarkdownPreviewFragment(w, body) 162 - } 163 - 164 159 func (s *Pulls) RefreshCompose(w http.ResponseWriter, r *http.Request) { 165 160 l := s.logger.With("handler", "RefreshCompose") 166 161
-1
appview/pulls/router.go
··· 14 14 r.Get("/", s.NewPull) 15 15 r.Get("/refresh", s.RefreshCompose) 16 16 r.Post("/refresh", s.RefreshCompose) 17 - r.Post("/preview", s.MarkdownPreview) 18 17 r.Post("/", s.NewPull) 19 18 }) 20 19
+1 -1
appview/state/comment.go
··· 71 71 Comment: comment, 72 72 }) 73 73 if err != nil { 74 - l.Error("failed to render") 74 + l.Error("failed to render", "err", err) 75 75 } 76 76 } 77 77
+8
appview/state/markup.go
··· 1 + package state 2 + 3 + import "net/http" 4 + 5 + func (s *State) MarkdownPreview(w http.ResponseWriter, r *http.Request) { 6 + body := r.FormValue("body") 7 + s.pages.MarkdownPreviewFragment(w, body) 8 + }
+3
appview/state/router.go
··· 231 231 r.Delete("/", s.DeleteComment) 232 232 }) 233 233 234 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/markup", func(r chi.Router) { 235 + r.Post("/preview", s.MarkdownPreview) 236 + }) 234 237 r.Get("/profile/popover", s.ProfilePopover) 235 238 236 239 r.Route("/profile", func(r chi.Router) {