Monorepo for Tangled
0

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>

Seongmin Lee (May 11, 2026, 1:14 AM +0900) 1253c663 0730149d

+118 -129
-5
appview/pulls/compose.go
··· 157 157 } 158 158 } 159 159 160 - func (s *Pulls) MarkdownPreview(w http.ResponseWriter, r *http.Request) { 161 - body := r.FormValue("body") 162 - s.pages.MarkdownPreviewFragment(w, body) 163 - } 164 - 165 160 func (s *Pulls) RefreshCompose(w http.ResponseWriter, r *http.Request) { 166 161 l := s.logger.With("handler", "RefreshCompose") 167 162
-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
··· 212 212 r.Delete("/", s.DeleteComment) 213 213 }) 214 214 215 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/markup", func(r chi.Router) { 216 + r.Post("/preview", s.MarkdownPreview) 217 + }) 215 218 r.Get("/profile/popover", s.ProfilePopover) 216 219 217 220 r.Route("/profile", func(r chi.Router) {
+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"]')
+69
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 + {{ $tabClasses := "group flex items-center gap-2 px-3 py-1 text-sm whitespace-nowrap rounded hover:no-underline data-[active=true]:bg-white data-[active=true]:dark:bg-gray-800 data-[active=true]:shadow-sm data-[active=true]:cursor-default data-[active=false]:hover:text-gray-900 data-[active=false]:dark:hover:text-white" }} 12 + <div class="inline-flex items-center gap-1 p-1 bg-slate-100 dark:bg-gray-900 rounded-md text-gray-600 dark:text-gray-300 self-start border dark:border-gray-700"> 13 + <button type="button" data-md-mode="write" 14 + data-active="true" 15 + class="{{ $tabClasses }}"> 16 + {{ i "pencil" "w-3.5 h-3.5 inline group-[.htmx-request]:hidden" }} 17 + Write 18 + </button> 19 + <button type="button" data-md-mode="preview" 20 + data-active="false" 21 + hx-post="/markup/preview" 22 + hx-vals='js:{body: this.closest("[data-md-editor]").querySelector("textarea").value}' 23 + hx-params="body" 24 + hx-target="next [data-md-preview]" 25 + hx-swap="innerHTML" 26 + hx-indicator="this" 27 + hx-disabled-elt="this" 28 + class="{{ $tabClasses }}"> 29 + {{ i "eye" "w-3.5 h-3.5 inline group-[.htmx-request]:hidden" }} 30 + {{ i "loader-circle" "w-3.5 h-3.5 animate-spin hidden group-[.htmx-request]:inline" }} 31 + Preview 32 + </button> 33 + </div> 34 + <div data-md-panel="write"> 35 + <textarea 36 + name="{{ $name }}" 37 + rows="{{ $rows }}" 38 + class="w-full resize-y dark:bg-gray-800 dark:text-white dark:border-gray-700" 39 + {{if $required}}required{{end}} 40 + {{if $autofocus}}autofocus{{end}} 41 + placeholder="{{ $placeholder }}" 42 + >{{with $value}}{{.}}{{end}}</textarea> 43 + </div> 44 + <div data-md-panel="preview" class="hidden"> 45 + <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"> 46 + <span class="text-gray-400 dark:text-gray-500 italic">Loading preview...</span> 47 + </div> 48 + </div> 49 + </div> 50 + <script> 51 + (() => { 52 + if (window.__mdEditorWired) return; 53 + window.__mdEditorWired = true; 54 + document.body.addEventListener('click', (e) => { 55 + const btn = e.target.closest('[data-md-mode]'); 56 + if (!btn) return; 57 + const editor = btn.closest('[data-md-editor]'); 58 + if (!editor) return; 59 + const mode = btn.dataset.mdMode; 60 + editor.querySelectorAll('[data-md-panel]').forEach(p => { 61 + p.classList.toggle('hidden', p.dataset.mdPanel !== mode); 62 + }); 63 + editor.querySelectorAll('[data-md-mode]').forEach(b => { 64 + b.dataset.active = (b === btn) ? 'true' : 'false'; 65 + }); 66 + }); 67 + })(); 68 + </script> 69 + {{ end }}
+1 -1
appview/pages/templates/layouts/base.html
··· 20 20 <meta name="author" content="Tangled" /> 21 21 <meta name="robots" content="index, follow" /> 22 22 23 - <script defer src="/static/htmx.min.js"></script> 23 + <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js" integrity="sha384-H5SrcfygHmAuTDZphMHqBJLc3FhssKjG7w/CeCpFReSfwBWDTKpkzPP8c+cLsK+V" crossorigin="anonymous"></script> 24 24 <script defer src="/static/htmx-ext-ws.min.js"></script> 25 25 <script defer src="/static/actor-typeahead.js" type="module"></script> 26 26 <script defer src="/static/topbar-search.js"></script>
+5 -7
appview/pages/templates/strings/string.html
··· 104 104 <div class="text-sm pb-2 text-gray-500 dark:text-gray-400"> 105 105 {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }} 106 106 </div> 107 - <textarea 108 - name="body" 109 - class="w-full p-2 rounded" 110 - placeholder="Add to the discussion. Markdown is supported." 111 - rows="5" 112 - required 113 - ></textarea> 107 + {{ template "fragments/markdownEditor" 108 + (dict "Name" "body" 109 + "BlobName" "blob" 110 + "Required" true 111 + "Placeholder" "Add to the discussion. Markdown is supported.") }} 114 112 <div id="comment-error" class="error"></div> 115 113 </div> 116 114 <div class="flex gap-2 mt-2">
+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 <div id="comment-error" class="error"></div> 18 18 {{ template "replyActions" . }} 19 19 </form>
+9 -13
appview/pages/templates/repo/issues/fragments/newComment.html
··· 2 2 {{ if .LoggedInUser }} 3 3 <form 4 4 hx-post="/comment" 5 - hx-trigger="submit, click from:#close-button, keydown[commentButtonEnabled() && (ctrlKey || metaKey) && key=='Enter'] from:#comment-textarea" 5 + hx-trigger="submit, click from:#close-button, keydown[commentButtonEnabled() && (ctrlKey || metaKey) && key=='Enter'] from:(find textarea)" 6 6 hx-disabled-elt="find button[type='submit']" 7 - hx-on::after-request="if(event.detail.successful) this.reset()" 7 + hx-on::after-request="if(event.target === this && event.detail.successful) this.reset()" 8 + hx-swap="none" 8 9 class="group/form" 9 10 > 10 11 <input name="subject-uri" type="hidden" value="{{ .Issue.AtUri }}"> 11 - <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full"> 12 + <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full" hx-on:keyup="updateCommentForm()"> 12 13 <div class="text-sm pb-2 text-gray-500 dark:text-gray-400"> 13 14 {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }} 14 15 </div> 15 - <textarea 16 - id="comment-textarea" 17 - name="body" 18 - class="w-full p-2 rounded" 19 - placeholder="Add to the discussion. Markdown is supported." 20 - onkeyup="updateCommentForm()" 21 - rows="5" 22 - required 23 - ></textarea> 16 + {{ template "fragments/markdownEditor" 17 + (dict "Name" "body" 18 + "BlobName" "blob" 19 + "Placeholder" "Add to the discussion. Markdown is supported.") }} 24 20 <div id="comment-error" class="error"></div> 25 21 <div id="issue-action" class="error"></div> 26 22 </div> ··· 81 77 } 82 78 83 79 function updateCommentForm() { 84 - const textarea = document.getElementById('comment-textarea'); 80 + const textarea = document.querySelector('form[hx-post="/comment"] textarea'); 85 81 const commentButton = document.getElementById('comment-button'); 86 82 const closeButtonText = document.getElementById('close-button-text'); 87 83
+6 -7
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
··· 12 12 > 13 13 <input name="subject-uri" type="hidden" value="{{ .Pull.AtUri }}"> 14 14 <input name="pull-round-idx" type="hidden" value="{{ .RoundNumber }}"> 15 - <textarea 16 - id="comment-textarea" 17 - name="body" 18 - class="w-full p-2 rounded border" 19 - rows=8 20 - placeholder="Add to the discussion..."></textarea 21 - > 15 + {{ template "fragments/markdownEditor" 16 + (dict "Name" "body" 17 + "BlobName" "blob" 18 + "Rows" 8 19 + "Required" true 20 + "Placeholder" "Add to the discussion...") }} 22 21 {{ template "replyActions" . }} 23 22 <div id="pull-comment"></div> 24 23 </form>
+2 -77
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 uppercase 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 - {{ end }} 47 - 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 - {{ $tabClasses := "group flex items-center gap-2 px-3 py-1 text-sm whitespace-nowrap rounded hover:no-underline data-[active=true]:bg-white data-[active=true]:dark:bg-gray-800 data-[active=true]:shadow-sm data-[active=true]:cursor-default data-[active=false]:hover:text-gray-900 data-[active=false]:dark:hover:text-white" }} 57 - <div class="inline-flex items-center gap-1 p-1 bg-slate-100 dark:bg-gray-900 rounded-md text-gray-600 dark:text-gray-300 self-start border dark:border-gray-700"> 58 - <button type="button" data-md-mode="write" 59 - data-active="true" 60 - class="{{ $tabClasses }}"> 61 - {{ i "pencil" "w-3.5 h-3.5 inline group-[.htmx-request]:hidden" }} 62 - Write 63 - </button> 64 - <button type="button" data-md-mode="preview" 65 - data-active="false" 66 - hx-post="{{ $previewUrl }}" 67 - hx-vals='js:{body: document.querySelector("[data-md-editor=\"{{ $id }}\"] textarea").value}' 68 - hx-params="body" 69 - hx-target="[data-md-editor='{{ $id }}'] [data-md-preview]" 70 - hx-swap="innerHTML" 71 - hx-indicator="this" 72 - class="{{ $tabClasses }}"> 73 - {{ i "eye" "w-3.5 h-3.5 inline group-[.htmx-request]:hidden" }} 74 - {{ i "loader-circle" "w-3.5 h-3.5 animate-spin hidden group-[.htmx-request]:inline" }} 75 - Preview 76 - </button> 77 - </div> 78 - <div data-md-panel="write"> 79 - <textarea 80 - id="{{ $id }}" 81 - name="{{ $name }}" 82 - rows="{{ $rows }}" 83 - class="w-full resize-y dark:bg-gray-800 dark:text-white dark:border-gray-700" 84 - placeholder="{{ $placeholder }}" 85 - >{{ $value }}</textarea> 86 - </div> 87 - <div data-md-panel="preview" class="hidden"> 88 - <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"> 89 - <span class="text-gray-400 dark:text-gray-500 italic">Loading preview...</span> 90 - </div> 91 - </div> 92 - </div> 93 40 {{ end }} 94 41 95 42 {{ define "pullSubmitRow" }} ··· 121 68 </span> 122 69 </button> 123 70 </div> 124 - {{ end }} 125 - 126 - {{ define "markdownEditorScript" }} 127 - <script> 128 - (() => { 129 - if (window.__mdEditorWired) return; 130 - window.__mdEditorWired = true; 131 - document.body.addEventListener('click', (e) => { 132 - const btn = e.target.closest('[data-md-mode]'); 133 - if (!btn) return; 134 - const editor = btn.closest('[data-md-editor]'); 135 - if (!editor) return; 136 - const mode = btn.dataset.mdMode; 137 - editor.querySelectorAll('[data-md-panel]').forEach(p => { 138 - p.classList.toggle('hidden', p.dataset.mdPanel !== mode); 139 - }); 140 - editor.querySelectorAll('[data-md-mode]').forEach(b => { 141 - b.dataset.active = (b === btn) ? 'true' : 'false'; 142 - }); 143 - }); 144 - })(); 145 - </script> 146 71 {{ 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 }}