+79
-20
api/tangled/cbor_gen.go
+79
-20
api/tangled/cbor_gen.go
···
7934
7934
}
7935
7935
7936
7936
cw := cbg.NewCborWriter(w)
7937
-
fieldCount := 9
7937
+
fieldCount := 10
7938
7938
7939
7939
if t.Body == nil {
7940
7940
fieldCount--
7941
7941
}
7942
7942
7943
7943
if t.Mentions == nil {
7944
+
fieldCount--
7945
+
}
7946
+
7947
+
if t.Patch == nil {
7944
7948
fieldCount--
7945
7949
}
7946
7950
···
8008
8012
}
8009
8013
8010
8014
// t.Patch (string) (string)
8011
-
if len("patch") > 1000000 {
8012
-
return xerrors.Errorf("Value in field \"patch\" was too long")
8013
-
}
8015
+
if t.Patch != nil {
8014
8016
8015
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patch"))); err != nil {
8016
-
return err
8017
-
}
8018
-
if _, err := cw.WriteString(string("patch")); err != nil {
8019
-
return err
8020
-
}
8017
+
if len("patch") > 1000000 {
8018
+
return xerrors.Errorf("Value in field \"patch\" was too long")
8019
+
}
8021
8020
8022
-
if len(t.Patch) > 1000000 {
8023
-
return xerrors.Errorf("Value in field t.Patch was too long")
8024
-
}
8021
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patch"))); err != nil {
8022
+
return err
8023
+
}
8024
+
if _, err := cw.WriteString(string("patch")); err != nil {
8025
+
return err
8026
+
}
8027
+
8028
+
if t.Patch == nil {
8029
+
if _, err := cw.Write(cbg.CborNull); err != nil {
8030
+
return err
8031
+
}
8032
+
} else {
8033
+
if len(*t.Patch) > 1000000 {
8034
+
return xerrors.Errorf("Value in field t.Patch was too long")
8035
+
}
8025
8036
8026
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Patch))); err != nil {
8027
-
return err
8028
-
}
8029
-
if _, err := cw.WriteString(string(t.Patch)); err != nil {
8030
-
return err
8037
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Patch))); err != nil {
8038
+
return err
8039
+
}
8040
+
if _, err := cw.WriteString(string(*t.Patch)); err != nil {
8041
+
return err
8042
+
}
8043
+
}
8031
8044
}
8032
8045
8033
8046
// t.Title (string) (string)
···
8147
8160
return err
8148
8161
}
8149
8162
8163
+
// t.PatchBlob (util.LexBlob) (struct)
8164
+
if len("patchBlob") > 1000000 {
8165
+
return xerrors.Errorf("Value in field \"patchBlob\" was too long")
8166
+
}
8167
+
8168
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patchBlob"))); err != nil {
8169
+
return err
8170
+
}
8171
+
if _, err := cw.WriteString(string("patchBlob")); err != nil {
8172
+
return err
8173
+
}
8174
+
8175
+
if err := t.PatchBlob.MarshalCBOR(cw); err != nil {
8176
+
return err
8177
+
}
8178
+
8150
8179
// t.References ([]string) (slice)
8151
8180
if t.References != nil {
8152
8181
···
8262
8291
case "patch":
8263
8292
8264
8293
{
8265
-
sval, err := cbg.ReadStringWithMax(cr, 1000000)
8294
+
b, err := cr.ReadByte()
8266
8295
if err != nil {
8267
8296
return err
8268
8297
}
8298
+
if b != cbg.CborNull[0] {
8299
+
if err := cr.UnreadByte(); err != nil {
8300
+
return err
8301
+
}
8269
8302
8270
-
t.Patch = string(sval)
8303
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
8304
+
if err != nil {
8305
+
return err
8306
+
}
8307
+
8308
+
t.Patch = (*string)(&sval)
8309
+
}
8271
8310
}
8272
8311
// t.Title (string) (string)
8273
8312
case "title":
···
8370
8409
}
8371
8410
8372
8411
t.CreatedAt = string(sval)
8412
+
}
8413
+
// t.PatchBlob (util.LexBlob) (struct)
8414
+
case "patchBlob":
8415
+
8416
+
{
8417
+
8418
+
b, err := cr.ReadByte()
8419
+
if err != nil {
8420
+
return err
8421
+
}
8422
+
if b != cbg.CborNull[0] {
8423
+
if err := cr.UnreadByte(); err != nil {
8424
+
return err
8425
+
}
8426
+
t.PatchBlob = new(util.LexBlob)
8427
+
if err := t.PatchBlob.UnmarshalCBOR(cr); err != nil {
8428
+
return xerrors.Errorf("unmarshaling t.PatchBlob pointer: %w", err)
8429
+
}
8430
+
}
8431
+
8373
8432
}
8374
8433
// t.References ([]string) (slice)
8375
8434
case "references":
+12
-9
api/tangled/repopull.go
+12
-9
api/tangled/repopull.go
···
17
17
} //
18
18
// RECORDTYPE: RepoPull
19
19
type RepoPull struct {
20
-
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"`
21
-
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
22
-
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
-
Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
24
-
Patch string `json:"patch" cborgen:"patch"`
25
-
References []string `json:"references,omitempty" cborgen:"references,omitempty"`
26
-
Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"`
27
-
Target *RepoPull_Target `json:"target" cborgen:"target"`
28
-
Title string `json:"title" cborgen:"title"`
20
+
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"`
21
+
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
22
+
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
+
Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
24
+
// patch: (deprecated) use patchBlob instead
25
+
Patch *string `json:"patch,omitempty" cborgen:"patch,omitempty"`
26
+
// patchBlob: patch content
27
+
PatchBlob *util.LexBlob `json:"patchBlob" cborgen:"patchBlob"`
28
+
References []string `json:"references,omitempty" cborgen:"references,omitempty"`
29
+
Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"`
30
+
Target *RepoPull_Target `json:"target" cborgen:"target"`
31
+
Title string `json:"title" cborgen:"title"`
29
32
}
30
33
31
34
// RepoPull_Source is a "source" in the sh.tangled.repo.pull schema.
+18
-11
appview/db/profile.go
+18
-11
appview/db/profile.go
···
20
20
timeline := models.ProfileTimeline{
21
21
ByMonth: make([]models.ByMonth, TimeframeMonths),
22
22
}
23
-
currentMonth := time.Now().Month()
23
+
now := time.Now()
24
24
timeframe := fmt.Sprintf("-%d months", TimeframeMonths)
25
25
26
26
pulls, err := GetPullsByOwnerDid(e, forDid, timeframe)
···
30
30
31
31
// group pulls by month
32
32
for _, pull := range pulls {
33
-
pullMonth := pull.Created.Month()
33
+
monthsAgo := monthsBetween(pull.Created, now)
34
34
35
-
if currentMonth-pullMonth >= TimeframeMonths {
35
+
if monthsAgo >= TimeframeMonths {
36
36
// shouldn't happen; but times are weird
37
37
continue
38
38
}
39
39
40
-
idx := currentMonth - pullMonth
40
+
idx := monthsAgo
41
41
items := &timeline.ByMonth[idx].PullEvents.Items
42
42
43
43
*items = append(*items, &pull)
···
53
53
}
54
54
55
55
for _, issue := range issues {
56
-
issueMonth := issue.Created.Month()
56
+
monthsAgo := monthsBetween(issue.Created, now)
57
57
58
-
if currentMonth-issueMonth >= TimeframeMonths {
58
+
if monthsAgo >= TimeframeMonths {
59
59
// shouldn't happen; but times are weird
60
60
continue
61
61
}
62
62
63
-
idx := currentMonth - issueMonth
63
+
idx := monthsAgo
64
64
items := &timeline.ByMonth[idx].IssueEvents.Items
65
65
66
66
*items = append(*items, &issue)
···
77
77
if repo.Source != "" {
78
78
sourceRepo, err = GetRepoByAtUri(e, repo.Source)
79
79
if err != nil {
80
-
return nil, err
80
+
// the source repo was not found, skip this bit
81
+
log.Println("profile", "err", err)
81
82
}
82
83
}
83
84
84
-
repoMonth := repo.Created.Month()
85
+
monthsAgo := monthsBetween(repo.Created, now)
85
86
86
-
if currentMonth-repoMonth >= TimeframeMonths {
87
+
if monthsAgo >= TimeframeMonths {
87
88
// shouldn't happen; but times are weird
88
89
continue
89
90
}
90
91
91
-
idx := currentMonth - repoMonth
92
+
idx := monthsAgo
92
93
93
94
items := &timeline.ByMonth[idx].RepoEvents
94
95
*items = append(*items, models.RepoEvent{
···
98
99
}
99
100
100
101
return &timeline, nil
102
+
}
103
+
104
+
func monthsBetween(from, to time.Time) int {
105
+
years := to.Year() - from.Year()
106
+
months := int(to.Month() - from.Month())
107
+
return years*12 + months
101
108
}
102
109
103
110
func UpsertProfile(tx *sql.Tx, profile *models.Profile) error {
+1
-1
appview/db/punchcard.go
+1
-1
appview/db/punchcard.go
+2
-2
appview/issues/opengraph.go
+2
-2
appview/issues/opengraph.go
···
193
193
dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2)
194
194
dollyY := statsY + iconBaselineOffset - dollySize/2 + 25
195
195
dollyColor := color.RGBA{180, 180, 180, 255} // light gray
196
-
err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor)
196
+
err = dollyArea.DrawDolly(dollyX, dollyY, dollySize, dollyColor)
197
197
if err != nil {
198
-
log.Printf("dolly silhouette not available (this is ok): %v", err)
198
+
log.Printf("dolly not available (this is ok): %v", err)
199
199
}
200
200
201
201
// Draw "opened by @author" and date at the bottom with more spacing
-5
appview/knots/knots.go
-5
appview/knots/knots.go
···
666
666
k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
667
667
return
668
668
}
669
-
if memberId.Handle.IsInvalidHandle() {
670
-
l.Error("failed to resolve member identity to handle")
671
-
k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
672
-
return
673
-
}
674
669
675
670
// remove from enforcer
676
671
err = k.Enforcer.RemoveKnotMember(domain, memberId.DID.String())
+4
appview/middleware/middleware.go
+4
appview/middleware/middleware.go
···
223
223
)
224
224
if err != nil {
225
225
log.Println("failed to resolve repo", "err", err)
226
+
w.WriteHeader(http.StatusNotFound)
226
227
mw.pages.ErrorKnot404(w)
227
228
return
228
229
}
···
240
241
f, err := mw.repoResolver.Resolve(r)
241
242
if err != nil {
242
243
log.Println("failed to fully resolve repo", err)
244
+
w.WriteHeader(http.StatusNotFound)
243
245
mw.pages.ErrorKnot404(w)
244
246
return
245
247
}
···
288
290
f, err := mw.repoResolver.Resolve(r)
289
291
if err != nil {
290
292
log.Println("failed to fully resolve repo", err)
293
+
w.WriteHeader(http.StatusNotFound)
291
294
mw.pages.ErrorKnot404(w)
292
295
return
293
296
}
···
324
327
f, err := mw.repoResolver.Resolve(r)
325
328
if err != nil {
326
329
log.Println("failed to fully resolve repo", err)
330
+
w.WriteHeader(http.StatusNotFound)
327
331
mw.pages.ErrorKnot404(w)
328
332
return
329
333
}
+39
appview/models/pipeline.go
+39
appview/models/pipeline.go
···
1
1
package models
2
2
3
3
import (
4
+
"fmt"
4
5
"slices"
6
+
"strings"
5
7
"time"
6
8
7
9
"github.com/bluesky-social/indigo/atproto/syntax"
···
50
52
}
51
53
52
54
return 0
55
+
}
56
+
57
+
// produces short summary of successes:
58
+
// - "0/4" when zero successes of 4 workflows
59
+
// - "4/4" when all successes of 4 workflows
60
+
// - "0/0" when no workflows run in this pipeline
61
+
func (p Pipeline) ShortStatusSummary() string {
62
+
counts := make(map[spindle.StatusKind]int)
63
+
for _, w := range p.Statuses {
64
+
counts[w.Latest().Status] += 1
65
+
}
66
+
67
+
total := len(p.Statuses)
68
+
successes := counts[spindle.StatusKindSuccess]
69
+
70
+
return fmt.Sprintf("%d/%d", successes, total)
71
+
}
72
+
73
+
// produces a string of the form "3/4 success, 2/4 failed, 1/4 pending"
74
+
func (p Pipeline) LongStatusSummary() string {
75
+
counts := make(map[spindle.StatusKind]int)
76
+
for _, w := range p.Statuses {
77
+
counts[w.Latest().Status] += 1
78
+
}
79
+
80
+
total := len(p.Statuses)
81
+
82
+
var result []string
83
+
// finish states first, followed by start states
84
+
states := append(spindle.FinishStates[:], spindle.StartStates[:]...)
85
+
for _, state := range states {
86
+
if count, ok := counts[state]; ok {
87
+
result = append(result, fmt.Sprintf("%d/%d %s", count, total, state.String()))
88
+
}
89
+
}
90
+
91
+
return strings.Join(result, ", ")
53
92
}
54
93
55
94
func (p Pipeline) Counts() map[string]int {
+1
-1
appview/models/pull.go
+1
-1
appview/models/pull.go
···
83
83
Repo *Repo
84
84
}
85
85
86
+
// NOTE: This method does not include patch blob in returned atproto record
86
87
func (p Pull) AsRecord() tangled.RepoPull {
87
88
var source *tangled.RepoPull_Source
88
89
if p.PullSource != nil {
···
113
114
Repo: p.RepoAt.String(),
114
115
Branch: p.TargetBranch,
115
116
},
116
-
Patch: p.LatestPatch(),
117
117
Source: source,
118
118
}
119
119
return record
+9
-9
appview/ogcard/card.go
+9
-9
appview/ogcard/card.go
···
334
334
return nil
335
335
}
336
336
337
-
func (c *Card) DrawDollySilhouette(x, y, size int, iconColor color.Color) error {
337
+
func (c *Card) DrawDolly(x, y, size int, iconColor color.Color) error {
338
338
tpl, err := template.New("dolly").
339
-
ParseFS(pages.Files, "templates/fragments/dolly/silhouette.html")
339
+
ParseFS(pages.Files, "templates/fragments/dolly/logo.html")
340
340
if err != nil {
341
-
return fmt.Errorf("failed to read dolly silhouette template: %w", err)
341
+
return fmt.Errorf("failed to read dolly template: %w", err)
342
342
}
343
343
344
344
var svgData bytes.Buffer
345
-
if err = tpl.ExecuteTemplate(&svgData, "fragments/dolly/silhouette", nil); err != nil {
346
-
return fmt.Errorf("failed to execute dolly silhouette template: %w", err)
345
+
if err = tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", nil); err != nil {
346
+
return fmt.Errorf("failed to execute dolly template: %w", err)
347
347
}
348
348
349
349
icon, err := BuildSVGIconFromData(svgData.Bytes(), iconColor)
···
453
453
454
454
// Handle SVG separately
455
455
if contentType == "image/svg+xml" || strings.HasSuffix(url, ".svg") {
456
-
return c.convertSVGToPNG(bodyBytes)
456
+
return convertSVGToPNG(bodyBytes)
457
457
}
458
458
459
459
// Support content types are in-sync with the allowed custom avatar file types
···
493
493
}
494
494
495
495
// convertSVGToPNG converts SVG data to a PNG image
496
-
func (c *Card) convertSVGToPNG(svgData []byte) (image.Image, bool) {
496
+
func convertSVGToPNG(svgData []byte) (image.Image, bool) {
497
497
// Parse the SVG
498
498
icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData))
499
499
if err != nil {
···
547
547
draw.CatmullRom.Scale(scaledImg, scaledImg.Bounds(), img, srcBounds, draw.Src, nil)
548
548
549
549
// Draw the image with circular clipping
550
-
for cy := 0; cy < size; cy++ {
551
-
for cx := 0; cx < size; cx++ {
550
+
for cy := range size {
551
+
for cx := range size {
552
552
// Calculate distance from center
553
553
dx := float64(cx - center)
554
554
dy := float64(cy - center)
+20
-7
appview/pages/funcmap.go
+20
-7
appview/pages/funcmap.go
···
334
334
},
335
335
"deref": func(v any) any {
336
336
val := reflect.ValueOf(v)
337
-
if val.Kind() == reflect.Ptr && !val.IsNil() {
337
+
if val.Kind() == reflect.Pointer && !val.IsNil() {
338
338
return val.Elem().Interface()
339
339
}
340
340
return nil
···
366
366
return p.AvatarUrl(handle, "")
367
367
},
368
368
"langColor": enry.GetColor,
369
-
"layoutSide": func() string {
370
-
return "col-span-1 md:col-span-2 lg:col-span-3"
371
-
},
372
-
"layoutCenter": func() string {
373
-
return "col-span-1 md:col-span-8 lg:col-span-6"
374
-
},
369
+
"reverse": func(s any) any {
370
+
if s == nil {
371
+
return nil
372
+
}
373
+
374
+
v := reflect.ValueOf(s)
375
+
376
+
if v.Kind() != reflect.Slice {
377
+
return s
378
+
}
379
+
380
+
length := v.Len()
381
+
reversed := reflect.MakeSlice(v.Type(), length, length)
375
382
383
+
for i := range length {
384
+
reversed.Index(i).Set(v.Index(length - 1 - i))
385
+
}
386
+
387
+
return reversed.Interface()
388
+
},
376
389
"normalizeForHtmlId": func(s string) string {
377
390
normalized := strings.ReplaceAll(s, ":", "_")
378
391
normalized = strings.ReplaceAll(normalized, ".", "_")
+14
-1
appview/pages/pages.go
+14
-1
appview/pages/pages.go
···
210
210
return tpl.ExecuteTemplate(w, "layouts/base", params)
211
211
}
212
212
213
+
type DollyParams struct {
214
+
Classes string
215
+
FillColor string
216
+
}
217
+
218
+
func (p *Pages) Dolly(w io.Writer, params DollyParams) error {
219
+
return p.executePlain("fragments/dolly/logo", w, params)
220
+
}
221
+
213
222
func (p *Pages) Favicon(w io.Writer) error {
214
-
return p.executePlain("fragments/dolly/silhouette", w, nil)
223
+
return p.Dolly(w, DollyParams{
224
+
Classes: "text-black dark:text-white",
225
+
})
215
226
}
216
227
217
228
type LoginParams struct {
···
1092
1103
MergeCheck types.MergeCheckResponse
1093
1104
ResubmitCheck ResubmitResult
1094
1105
Pipelines map[string]models.Pipeline
1106
+
Diff *types.NiceDiff
1107
+
DiffOpts types.DiffOpts
1095
1108
1096
1109
OrderedReactionKinds []models.ReactionKind
1097
1110
Reactions map[models.ReactionKind]models.ReactionDisplayData
+9
-29
appview/pages/templates/brand/brand.html
+9
-29
appview/pages/templates/brand/brand.html
···
4
4
<div class="grid grid-cols-10">
5
5
<header class="col-span-full md:col-span-10 px-6 py-2 mb-4">
6
6
<h1 class="text-2xl font-bold dark:text-white mb-1">Brand</h1>
7
-
<p class="text-gray-600 dark:text-gray-400 mb-1">
7
+
<p class="text-gray-500 dark:text-gray-300 mb-1">
8
8
Assets and guidelines for using Tangled's logo and brand elements.
9
9
</p>
10
10
</header>
···
14
14
15
15
<!-- Introduction Section -->
16
16
<section>
17
-
<p class="text-gray-600 dark:text-gray-400 mb-2">
17
+
<p class="text-gray-500 dark:text-gray-300 mb-2">
18
18
Tangled's logo and mascot is <strong>Dolly</strong>, the first ever <em>cloned</em> mammal. Please
19
19
follow the below guidelines when using Dolly and the logotype.
20
20
</p>
21
-
<p class="text-gray-600 dark:text-gray-400 mb-2">
21
+
<p class="text-gray-500 dark:text-gray-300 mb-2">
22
22
All assets are served as SVGs, and can be downloaded by right-clicking and clicking "Save image as".
23
23
</p>
24
24
</section>
···
34
34
</div>
35
35
<div class="order-1 lg:order-2">
36
36
<h2 class="text-xl font-semibold dark:text-white mb-3">Black logotype</h2>
37
-
<p class="text-gray-600 dark:text-gray-400 mb-4">For use on light-colored backgrounds.</p>
37
+
<p class="text-gray-500 dark:text-gray-300 mb-4">For use on light-colored backgrounds.</p>
38
38
<p class="text-gray-700 dark:text-gray-300">
39
39
This is the preferred version of the logotype, featuring dark text and elements, ideal for light
40
40
backgrounds and designs.
···
53
53
</div>
54
54
<div class="order-1 lg:order-2">
55
55
<h2 class="text-xl font-semibold dark:text-white mb-3">White logotype</h2>
56
-
<p class="text-gray-600 dark:text-gray-400 mb-4">For use on dark-colored backgrounds.</p>
56
+
<p class="text-gray-500 dark:text-gray-300 mb-4">For use on dark-colored backgrounds.</p>
57
57
<p class="text-gray-700 dark:text-gray-300">
58
58
This version features white text and elements, ideal for dark backgrounds
59
59
and inverted designs.
···
81
81
</div>
82
82
<div class="order-1 lg:order-2">
83
83
<h2 class="text-xl font-semibold dark:text-white mb-3">Mark only</h2>
84
-
<p class="text-gray-600 dark:text-gray-400 mb-4">
84
+
<p class="text-gray-500 dark:text-gray-300 mb-4">
85
85
When a smaller 1:1 logo or icon is needed, Dolly's face may be used on its own.
86
86
</p>
87
87
<p class="text-gray-700 dark:text-gray-300 mb-4">
···
123
123
</div>
124
124
<div class="order-1 lg:order-2">
125
125
<h2 class="text-xl font-semibold dark:text-white mb-3">Colored backgrounds</h2>
126
-
<p class="text-gray-600 dark:text-gray-400 mb-4">
126
+
<p class="text-gray-500 dark:text-gray-300 mb-4">
127
127
White logo mark on colored backgrounds.
128
128
</p>
129
129
<p class="text-gray-700 dark:text-gray-300 mb-4">
···
165
165
</div>
166
166
<div class="order-1 lg:order-2">
167
167
<h2 class="text-xl font-semibold dark:text-white mb-3">Lighter backgrounds</h2>
168
-
<p class="text-gray-600 dark:text-gray-400 mb-4">
168
+
<p class="text-gray-500 dark:text-gray-300 mb-4">
169
169
Dark logo mark on lighter, pastel backgrounds.
170
170
</p>
171
171
<p class="text-gray-700 dark:text-gray-300 mb-4">
···
186
186
</div>
187
187
<div class="order-1 lg:order-2">
188
188
<h2 class="text-xl font-semibold dark:text-white mb-3">Recoloring</h2>
189
-
<p class="text-gray-600 dark:text-gray-400 mb-4">
189
+
<p class="text-gray-500 dark:text-gray-300 mb-4">
190
190
Custom coloring of the logotype is permitted.
191
191
</p>
192
192
<p class="text-gray-700 dark:text-gray-300 mb-4">
···
194
194
</p>
195
195
<p class="text-gray-700 dark:text-gray-300 text-sm">
196
196
<strong>Example:</strong> Gray/sand colored logotype on a light yellow/tan background.
197
-
</p>
198
-
</div>
199
-
</section>
200
-
201
-
<!-- Silhouette Section -->
202
-
<section class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
203
-
<div class="order-2 lg:order-1">
204
-
<div class="border border-gray-200 dark:border-gray-700 p-8 sm:p-16 bg-gray-50 dark:bg-gray-100 rounded">
205
-
<img src="https://assets.tangled.network/tangled_dolly_silhouette.svg"
206
-
alt="Dolly silhouette"
207
-
class="w-full max-w-32 mx-auto" />
208
-
</div>
209
-
</div>
210
-
<div class="order-1 lg:order-2">
211
-
<h2 class="text-xl font-semibold dark:text-white mb-3">Dolly silhouette</h2>
212
-
<p class="text-gray-600 dark:text-gray-400 mb-4">A minimalist version of Dolly.</p>
213
-
<p class="text-gray-700 dark:text-gray-300">
214
-
The silhouette can be used where a subtle brand presence is needed,
215
-
or as a background element. Works on any background color with proper contrast.
216
-
For example, we use this as the site's favicon.
217
197
</p>
218
198
</div>
219
199
</section>
+14
-2
appview/pages/templates/fragments/dolly/logo.html
+14
-2
appview/pages/templates/fragments/dolly/logo.html
···
2
2
<svg
3
3
version="1.1"
4
4
id="svg1"
5
-
class="{{ . }}"
5
+
class="{{ .Classes }}"
6
6
width="25"
7
7
height="25"
8
8
viewBox="0 0 25 25"
···
17
17
xmlns:svg="http://www.w3.org/2000/svg"
18
18
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
19
19
xmlns:cc="http://creativecommons.org/ns#">
20
+
<style>
21
+
.dolly {
22
+
color: #000000;
23
+
}
24
+
25
+
@media (prefers-color-scheme: dark) {
26
+
.dolly {
27
+
color: #ffffff;
28
+
}
29
+
}
30
+
</style>
20
31
<sodipodi:namedview
21
32
id="namedview1"
22
33
pagecolor="#ffffff"
···
51
62
id="g1"
52
63
transform="translate(-0.42924038,-0.87777209)">
53
64
<path
54
-
fill="currentColor"
65
+
class="dolly"
66
+
fill="{{ or .FillColor "currentColor" }}"
55
67
style="stroke-width:0.111183;"
56
68
d="m 16.775491,24.987061 c -0.78517,-0.0064 -1.384202,-0.234614 -2.033994,-0.631295 -0.931792,-0.490188 -1.643475,-1.31368 -2.152014,-2.221647 C 11.781409,23.136647 10.701392,23.744942 9.4922931,24.0886 8.9774725,24.238111 8.0757679,24.389777 6.5811304,23.84827 4.4270703,23.124679 2.8580086,20.883331 3.0363279,18.599583 3.0037061,17.652919 3.3488675,16.723769 3.8381157,15.925061 2.5329485,15.224503 1.4686756,14.048584 1.0611184,12.606459 0.81344502,11.816973 0.82385989,10.966486 0.91519098,10.154906 1.2422711,8.2387903 2.6795811,6.5725716 4.5299585,5.9732484 5.2685364,4.290122 6.8802592,3.0349975 8.706276,2.7794663 c 1.2124148,-0.1688264 2.46744,0.084987 3.52811,0.7011837 1.545426,-1.7139736 4.237779,-2.2205077 6.293579,-1.1676231 1.568222,0.7488935 2.689625,2.3113526 2.961888,4.0151464 1.492195,0.5977882 2.749007,1.8168898 3.242225,3.3644951 0.329805,0.9581836 0.340709,2.0135956 0.127128,2.9974286 -0.381606,1.535184 -1.465322,2.842146 -2.868035,3.556463 0.0034,0.273204 0.901506,2.243045 0.751284,3.729647 -0.03281,1.858525 -1.211631,3.619894 -2.846433,4.475452 -0.953967,0.556812 -2.084452,0.546309 -3.120531,0.535398 z m -4.470079,-5.349839 c 1.322246,-0.147248 2.189053,-1.300106 2.862307,-2.338363 0.318287,-0.472954 0.561404,-1.002348 0.803,-1.505815 0.313265,0.287151 0.578698,0.828085 1.074141,0.956909 0.521892,0.162542 1.133743,0.03052 1.45325,-0.443554 0.611414,-1.140449 0.31004,-2.516537 -0.04602,-3.698347 C 18.232844,11.92927 17.945151,11.232927 17.397785,10.751793 17.514522,9.9283111 17.026575,9.0919791 16.332883,8.6609491 15.741721,9.1323278 14.842258,9.1294949 14.271975,8.6252369 13.178927,9.7400102 12.177239,9.7029996 11.209704,8.8195135 10.992255,8.6209543 10.577326,10.031484 9.1211947,9.2324497 8.2846288,9.9333947 7.6359672,10.607693 7.0611981,11.578553 6.5026891,12.62523 5.9177873,13.554793 5.867393,14.69141 c -0.024234,0.66432 0.4948601,1.360337 1.1982269,1.306329 0.702996,0.06277 1.1815208,-0.629091 1.7138087,-0.916491 0.079382,0.927141 0.1688108,1.923227 0.4821259,2.828358 0.3596254,1.171275 1.6262605,1.915695 2.8251855,1.745211 0.08481,-0.0066 0.218672,-0.01769 0.218672,-0.0176 z m 0.686342,-3.497495 c -0.643126,-0.394168 -0.33365,-1.249599 -0.359402,-1.870938 0.064,-0.749774 0.115321,-1.538054 0.452402,-2.221125 0.356724,-0.487008 1.226721,-0.299139 1.265134,0.325689 -0.02558,0.628509 -0.314101,1.25416 -0.279646,1.9057 -0.07482,0.544043 0.05418,1.155133 -0.186476,1.652391 -0.197455,0.275121 -0.599638,0.355105 -0.892012,0.208283 z m -2.808766,-0.358124 c -0.605767,-0.328664 -0.4133176,-1.155655 -0.5083256,-1.73063 0.078762,-0.66567 0.013203,-1.510085 0.5705316,-1.976886 0.545037,-0.380109 1.286917,0.270803 1.029164,0.868384 -0.274913,0.755214 -0.09475,1.580345 -0.08893,2.34609 -0.104009,0.451702 -0.587146,0.691508 -1.002445,0.493042 z"
57
69
id="path4"
-95
appview/pages/templates/fragments/dolly/silhouette.html
-95
appview/pages/templates/fragments/dolly/silhouette.html
···
1
-
{{ define "fragments/dolly/silhouette" }}
2
-
<svg
3
-
version="1.1"
4
-
id="svg1"
5
-
width="25"
6
-
height="25"
7
-
viewBox="0 0 25 25"
8
-
sodipodi:docname="tangled_dolly_face_only_black_on_trans.svg"
9
-
inkscape:export-filename="tangled_dolly_silhouette_black_on_trans.svg"
10
-
inkscape:export-xdpi="96"
11
-
inkscape:export-ydpi="96"
12
-
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
13
-
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14
-
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15
-
xmlns="http://www.w3.org/2000/svg"
16
-
xmlns:svg="http://www.w3.org/2000/svg"
17
-
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
18
-
xmlns:cc="http://creativecommons.org/ns#">
19
-
<style>
20
-
.dolly {
21
-
color: #000000;
22
-
}
23
-
24
-
@media (prefers-color-scheme: dark) {
25
-
.dolly {
26
-
color: #ffffff;
27
-
}
28
-
}
29
-
</style>
30
-
<sodipodi:namedview
31
-
id="namedview1"
32
-
pagecolor="#ffffff"
33
-
bordercolor="#000000"
34
-
borderopacity="0.25"
35
-
inkscape:showpageshadow="2"
36
-
inkscape:pageopacity="0.0"
37
-
inkscape:pagecheckerboard="true"
38
-
inkscape:deskcolor="#d5d5d5"
39
-
inkscape:zoom="64"
40
-
inkscape:cx="4.96875"
41
-
inkscape:cy="13.429688"
42
-
inkscape:window-width="3840"
43
-
inkscape:window-height="2160"
44
-
inkscape:window-x="0"
45
-
inkscape:window-y="0"
46
-
inkscape:window-maximized="0"
47
-
inkscape:current-layer="g1"
48
-
borderlayer="true">
49
-
<inkscape:page
50
-
x="0"
51
-
y="0"
52
-
width="25"
53
-
height="25"
54
-
id="page2"
55
-
margin="0"
56
-
bleed="0" />
57
-
</sodipodi:namedview>
58
-
<g
59
-
inkscape:groupmode="layer"
60
-
inkscape:label="Image"
61
-
id="g1"
62
-
transform="translate(-0.42924038,-0.87777209)">
63
-
<path
64
-
class="dolly"
65
-
fill="currentColor"
66
-
style="stroke-width:0.111183"
67
-
d="m 16.775491,24.987061 c -0.78517,-0.0064 -1.384202,-0.234614 -2.033994,-0.631295 -0.931792,-0.490188 -1.643475,-1.31368 -2.152014,-2.221647 C 11.781409,23.136647 10.701392,23.744942 9.4922931,24.0886 8.9774725,24.238111 8.0757679,24.389777 6.5811304,23.84827 4.4270703,23.124679 2.8580086,20.883331 3.0363279,18.599583 3.0037061,17.652919 3.3488675,16.723769 3.8381157,15.925061 2.5329485,15.224503 1.4686756,14.048584 1.0611184,12.606459 0.81344502,11.816973 0.82385989,10.966486 0.91519098,10.154906 1.2422711,8.2387903 2.6795811,6.5725716 4.5299585,5.9732484 5.2685364,4.290122 6.8802592,3.0349975 8.706276,2.7794663 c 1.2124148,-0.1688264 2.46744,0.084987 3.52811,0.7011837 1.545426,-1.7139736 4.237779,-2.2205077 6.293579,-1.1676231 1.568222,0.7488935 2.689625,2.3113526 2.961888,4.0151464 1.492195,0.5977882 2.749007,1.8168898 3.242225,3.3644951 0.329805,0.9581836 0.340709,2.0135956 0.127128,2.9974286 -0.381606,1.535184 -1.465322,2.842146 -2.868035,3.556463 0.0034,0.273204 0.901506,2.243045 0.751284,3.729647 -0.03281,1.858525 -1.211631,3.619894 -2.846433,4.475452 -0.953967,0.556812 -2.084452,0.546309 -3.120531,0.535398 z m -4.470079,-5.349839 c 1.322246,-0.147248 2.189053,-1.300106 2.862307,-2.338363 0.318287,-0.472954 0.561404,-1.002348 0.803,-1.505815 0.313265,0.287151 0.578698,0.828085 1.074141,0.956909 0.521892,0.162542 1.133743,0.03052 1.45325,-0.443554 0.611414,-1.140449 0.31004,-2.516537 -0.04602,-3.698347 C 18.232844,11.92927 17.945151,11.232927 17.397785,10.751793 17.514522,9.9283111 17.026575,9.0919791 16.332883,8.6609491 15.741721,9.1323278 14.842258,9.1294949 14.271975,8.6252369 13.178927,9.7400102 12.177239,9.7029996 11.209704,8.8195135 10.992255,8.6209543 10.577326,10.031484 9.1211947,9.2324497 8.2846288,9.9333947 7.6359672,10.607693 7.0611981,11.578553 6.5026891,12.62523 5.9177873,13.554793 5.867393,14.69141 c -0.024234,0.66432 0.4948601,1.360337 1.1982269,1.306329 0.702996,0.06277 1.1815208,-0.629091 1.7138087,-0.916491 0.079382,0.927141 0.1688108,1.923227 0.4821259,2.828358 0.3596254,1.171275 1.6262605,1.915695 2.8251855,1.745211 0.08481,-0.0066 0.218672,-0.01769 0.218672,-0.0176 z"
68
-
id="path7"
69
-
sodipodi:nodetypes="sccccccccccccccccccsscccccccccscccccccsc" />
70
-
</g>
71
-
<metadata
72
-
id="metadata1">
73
-
<rdf:RDF>
74
-
<cc:Work
75
-
rdf:about="">
76
-
<cc:license
77
-
rdf:resource="http://creativecommons.org/licenses/by/4.0/" />
78
-
</cc:Work>
79
-
<cc:License
80
-
rdf:about="http://creativecommons.org/licenses/by/4.0/">
81
-
<cc:permits
82
-
rdf:resource="http://creativecommons.org/ns#Reproduction" />
83
-
<cc:permits
84
-
rdf:resource="http://creativecommons.org/ns#Distribution" />
85
-
<cc:requires
86
-
rdf:resource="http://creativecommons.org/ns#Notice" />
87
-
<cc:requires
88
-
rdf:resource="http://creativecommons.org/ns#Attribution" />
89
-
<cc:permits
90
-
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
91
-
</cc:License>
92
-
</rdf:RDF>
93
-
</metadata>
94
-
</svg>
95
-
{{ end }}
+1
-1
appview/pages/templates/fragments/logotype.html
+1
-1
appview/pages/templates/fragments/logotype.html
···
1
1
{{ define "fragments/logotype" }}
2
2
<span class="flex items-center gap-2">
3
-
{{ template "fragments/dolly/logo" "size-16 text-black dark:text-white" }}
3
+
{{ template "fragments/dolly/logo" (dict "Classes" "size-16 text-black dark:text-white") }}
4
4
<span class="font-bold text-4xl not-italic">tangled</span>
5
5
<span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1">
6
6
alpha
+1
-1
appview/pages/templates/fragments/logotypeSmall.html
+1
-1
appview/pages/templates/fragments/logotypeSmall.html
···
1
1
{{ define "fragments/logotypeSmall" }}
2
2
<span class="flex items-center gap-2">
3
-
{{ template "fragments/dolly/logo" "size-8 text-black dark:text-white" }}
3
+
{{ template "fragments/dolly/logo" (dict "Classes" "size-8 text-black dark:text-white")}}
4
4
<span class="font-bold text-xl not-italic">tangled</span>
5
5
<span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1">
6
6
alpha
+1
appview/pages/templates/fragments/tabSelector.html
+1
appview/pages/templates/fragments/tabSelector.html
+1
-1
appview/pages/templates/knots/index.html
+1
-1
appview/pages/templates/knots/index.html
···
105
105
{{ define "docsButton" }}
106
106
<a
107
107
class="btn flex items-center gap-2"
108
-
href="https://tangled.org/@tangled.org/core/blob/master/docs/knot-hosting.md">
108
+
href="https://docs.tangled.org/knot-self-hosting-guide.html#knot-self-hosting-guide">
109
109
{{ i "book" "size-4" }}
110
110
docs
111
111
</a>
+4
appview/pages/templates/layouts/base.html
+4
appview/pages/templates/layouts/base.html
···
11
11
<script defer src="/static/htmx-ext-ws.min.js"></script>
12
12
<script defer src="/static/actor-typeahead.js" type="module"></script>
13
13
14
+
<link rel="icon" href="/static/logos/dolly.ico" sizes="48x48"/>
15
+
<link rel="icon" href="/static/logos/dolly.svg" sizes="any" type="image/svg+xml"/>
16
+
<link rel="apple-touch-icon" href="/static/logos/dolly.png"/>
17
+
14
18
<!-- preconnect to image cdn -->
15
19
<link rel="preconnect" href="https://avatar.tangled.sh" />
16
20
<link rel="preconnect" href="https://camo.tangled.sh" />
+1
-5
appview/pages/templates/layouts/fragments/topbar.html
+1
-5
appview/pages/templates/layouts/fragments/topbar.html
···
3
3
<div class="flex justify-between p-0 items-center">
4
4
<div id="left-items">
5
5
<a href="/" hx-boost="true" class="text-2xl no-underline hover:no-underline flex items-center gap-2">
6
-
{{ template "fragments/dolly/logo" "size-8 text-black dark:text-white" }}
7
-
<span class="font-bold text-xl not-italic hidden md:inline">tangled</span>
8
-
<span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1 hidden md:inline">
9
-
alpha
10
-
</span>
6
+
{{ template "fragments/logotypeSmall" }}
11
7
</a>
12
8
</div>
13
9
+1
-1
appview/pages/templates/layouts/repobase.html
+1
-1
appview/pages/templates/layouts/repobase.html
···
1
1
{{ define "title" }}{{ .RepoInfo.FullName }}{{ end }}
2
2
3
3
{{ define "content" }}
4
-
<section id="repo-header" class="mb-4 p-2 dark:text-white">
4
+
<section id="repo-header" class="mb-2 py-2 px-4 dark:text-white">
5
5
<div class="text-lg flex flex-col sm:flex-row items-start gap-4 justify-between">
6
6
<!-- left items -->
7
7
<div class="flex flex-col gap-2">
+2
-2
appview/pages/templates/repo/fragments/diff.html
+2
-2
appview/pages/templates/repo/fragments/diff.html
···
17
17
{{ else }}
18
18
{{ range $idx, $hunk := $diff }}
19
19
{{ with $hunk }}
20
-
<details open id="file-{{ .Name.New }}" class="group border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm" tabindex="{{ add $idx 1 }}">
21
-
<summary class="list-none cursor-pointer sticky top-0">
20
+
<details open id="file-{{ .Id }}" class="group border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm" tabindex="{{ add $idx 1 }}">
21
+
<summary class="list-none cursor-pointer sticky top-12">
22
22
<div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between">
23
23
<div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto">
24
24
<span class="group-open:hidden inline">{{ i "chevron-right" "w-4 h-4" }}</span>
+1
-8
appview/pages/templates/repo/fragments/diffChangedFiles.html
+1
-8
appview/pages/templates/repo/fragments/diffChangedFiles.html
···
1
1
{{ define "repo/fragments/diffChangedFiles" }}
2
-
{{ $stat := .Stat }}
3
2
{{ $fileTree := fileTree .ChangedFiles }}
4
3
<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">
5
-
<div class="diff-stat">
6
-
<div class="flex gap-2 items-center">
7
-
<strong class="text-sm uppercase dark:text-gray-200">Changed files</strong>
8
-
{{ template "repo/fragments/diffStatPill" $stat }}
9
-
</div>
10
-
{{ template "repo/fragments/fileTree" $fileTree }}
11
-
</div>
4
+
{{ template "repo/fragments/fileTree" $fileTree }}
12
5
</section>
13
6
{{ end }}
+22
-25
appview/pages/templates/repo/fragments/diffOpts.html
+22
-25
appview/pages/templates/repo/fragments/diffOpts.html
···
1
1
{{ define "repo/fragments/diffOpts" }}
2
-
<section class="flex flex-col gap-2 overflow-x-auto text-sm px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm">
3
-
<strong class="text-sm uppercase dark:text-gray-200">options</strong>
4
-
{{ $active := "unified" }}
5
-
{{ if .Split }}
6
-
{{ $active = "split" }}
7
-
{{ end }}
2
+
{{ $active := "unified" }}
3
+
{{ if .Split }}
4
+
{{ $active = "split" }}
5
+
{{ end }}
8
6
9
-
{{ $unified :=
10
-
(dict
11
-
"Key" "unified"
12
-
"Value" "unified"
13
-
"Icon" "square-split-vertical"
14
-
"Meta" "") }}
15
-
{{ $split :=
16
-
(dict
17
-
"Key" "split"
18
-
"Value" "split"
19
-
"Icon" "square-split-horizontal"
20
-
"Meta" "") }}
21
-
{{ $values := list $unified $split }}
7
+
{{ $unified :=
8
+
(dict
9
+
"Key" "unified"
10
+
"Value" "unified"
11
+
"Icon" "square-split-vertical"
12
+
"Meta" "") }}
13
+
{{ $split :=
14
+
(dict
15
+
"Key" "split"
16
+
"Value" "split"
17
+
"Icon" "square-split-horizontal"
18
+
"Meta" "") }}
19
+
{{ $values := list $unified $split }}
22
20
23
-
{{ template "fragments/tabSelector"
24
-
(dict
25
-
"Name" "diff"
26
-
"Values" $values
27
-
"Active" $active) }}
28
-
</section>
21
+
{{ template "fragments/tabSelector"
22
+
(dict
23
+
"Name" "diff"
24
+
"Values" $values
25
+
"Active" $active) }}
29
26
{{ end }}
30
27
+35
-35
appview/pages/templates/repo/fragments/splitDiff.html
+35
-35
appview/pages/templates/repo/fragments/splitDiff.html
···
3
3
{{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800" -}}
4
4
{{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}}
5
5
{{- $lineNrSepStyle := "pr-2 border-r border-gray-200 dark:border-gray-700" -}}
6
-
{{- $containerStyle := "flex min-w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}}
6
+
{{- $containerStyle := "inline-flex w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}}
7
7
{{- $emptyStyle := "bg-gray-200/30 dark:bg-gray-700/30" -}}
8
8
{{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400" -}}
9
9
{{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}}
10
10
{{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}}
11
11
{{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}}
12
12
<div class="grid grid-cols-2 divide-x divide-gray-200 dark:divide-gray-700">
13
-
<pre class="overflow-x-auto col-span-1"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">···</div>
13
+
<div class="overflow-x-auto col-span-1 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">···</span>
14
14
{{- range .LeftLines -}}
15
15
{{- if .IsEmpty -}}
16
-
<div class="{{ $emptyStyle }} {{ $containerStyle }}">
17
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></div>
18
-
<div class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></div>
19
-
<div class="px-2 invisible" aria-hidden="true">{{ .Content }}</div>
20
-
</div>
16
+
<span class="{{ $emptyStyle }} {{ $containerStyle }}">
17
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></span>
18
+
<span class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></span>
19
+
<span class="px-2 invisible" aria-hidden="true">{{ .Content }}</span>
20
+
</span>
21
21
{{- else if eq .Op.String "-" -}}
22
-
<div class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}">
23
-
<div class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></div>
24
-
<div class="{{ $opStyle }}">{{ .Op.String }}</div>
25
-
<div class="px-2">{{ .Content }}</div>
26
-
</div>
22
+
<span class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}">
23
+
<span class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></span>
24
+
<span class="{{ $opStyle }}">{{ .Op.String }}</span>
25
+
<span class="px-2 whitespace-pre">{{ .Content }}</span>
26
+
</span>
27
27
{{- else if eq .Op.String " " -}}
28
-
<div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}">
29
-
<div class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></div>
30
-
<div class="{{ $opStyle }}">{{ .Op.String }}</div>
31
-
<div class="px-2">{{ .Content }}</div>
32
-
</div>
28
+
<span class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}">
29
+
<span class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></span>
30
+
<span class="{{ $opStyle }}">{{ .Op.String }}</span>
31
+
<span class="px-2 whitespace-pre">{{ .Content }}</span>
32
+
</span>
33
33
{{- end -}}
34
34
{{- end -}}
35
-
{{- end -}}</div></div></pre>
35
+
{{- end -}}</div></div></div>
36
36
37
-
<pre class="overflow-x-auto col-span-1"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">···</div>
37
+
<div class="overflow-x-auto col-span-1 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">···</span>
38
38
{{- range .RightLines -}}
39
39
{{- if .IsEmpty -}}
40
-
<div class="{{ $emptyStyle }} {{ $containerStyle }}">
41
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></div>
42
-
<div class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></div>
43
-
<div class="px-2 invisible" aria-hidden="true">{{ .Content }}</div>
44
-
</div>
40
+
<span class="{{ $emptyStyle }} {{ $containerStyle }}">
41
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></span>
42
+
<span class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></span>
43
+
<span class="px-2 invisible" aria-hidden="true">{{ .Content }}</span>
44
+
</span>
45
45
{{- else if eq .Op.String "+" -}}
46
-
<div class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}">
47
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></div>
48
-
<div class="{{ $opStyle }}">{{ .Op.String }}</div>
49
-
<div class="px-2" >{{ .Content }}</div>
50
-
</div>
46
+
<span class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}">
47
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></span>
48
+
<span class="{{ $opStyle }}">{{ .Op.String }}</span>
49
+
<span class="px-2 whitespace-pre">{{ .Content }}</span>
50
+
</span>
51
51
{{- else if eq .Op.String " " -}}
52
-
<div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}">
53
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></div>
54
-
<div class="{{ $opStyle }}">{{ .Op.String }}</div>
55
-
<div class="px-2">{{ .Content }}</div>
56
-
</div>
52
+
<span class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}">
53
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a> </span>
54
+
<span class="{{ $opStyle }}">{{ .Op.String }}</span>
55
+
<span class="px-2 whitespace-pre">{{ .Content }}</span>
56
+
</span>
57
57
{{- end -}}
58
58
{{- end -}}
59
-
{{- end -}}</div></div></pre>
59
+
{{- end -}}</div></div></div>
60
60
</div>
61
61
{{ end }}
+21
-22
appview/pages/templates/repo/fragments/unifiedDiff.html
+21
-22
appview/pages/templates/repo/fragments/unifiedDiff.html
···
1
1
{{ define "repo/fragments/unifiedDiff" }}
2
2
{{ $name := .Id }}
3
-
<pre class="overflow-x-auto"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">···</div>
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">···</span>
4
4
{{- $oldStart := .OldPosition -}}
5
5
{{- $newStart := .NewPosition -}}
6
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
7
{{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}}
8
8
{{- $lineNrSepStyle1 := "" -}}
9
9
{{- $lineNrSepStyle2 := "pr-2 border-r border-gray-200 dark:border-gray-700" -}}
10
-
{{- $containerStyle := "flex min-w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}}
10
+
{{- $containerStyle := "inline-flex w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}}
11
11
{{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 " -}}
12
12
{{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}}
13
13
{{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}}
14
14
{{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}}
15
15
{{- range .Lines -}}
16
16
{{- if eq .Op.String "+" -}}
17
-
<div class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{$newStart}}">
18
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><span aria-hidden="true" class="invisible">{{$newStart}}</span></div>
19
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></div>
20
-
<div class="{{ $opStyle }}">{{ .Op.String }}</div>
21
-
<div class="px-2">{{ .Line }}</div>
22
-
</div>
17
+
<span class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{$newStart}}">
18
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><span aria-hidden="true" class="invisible">{{$newStart}}</span></span>
19
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></span>
20
+
<span class="{{ $opStyle }}">{{ .Op.String }}</span>
21
+
<span class="px-2 whitespace-pre">{{ .Line }}</span>
22
+
</span>
23
23
{{- $newStart = add64 $newStart 1 -}}
24
24
{{- end -}}
25
25
{{- if eq .Op.String "-" -}}
26
-
<div class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}">
27
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></div>
28
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><span aria-hidden="true" class="invisible">{{$oldStart}}</span></div>
29
-
<div class="{{ $opStyle }}">{{ .Op.String }}</div>
30
-
<div class="px-2">{{ .Line }}</div>
31
-
</div>
26
+
<span class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}">
27
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></span>
28
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><span aria-hidden="true" class="invisible">{{$oldStart}}</span></span>
29
+
<span class="{{ $opStyle }}">{{ .Op.String }}</span>
30
+
<span class="px-2 whitespace-pre">{{ .Line }}</span>
31
+
</span>
32
32
{{- $oldStart = add64 $oldStart 1 -}}
33
33
{{- end -}}
34
34
{{- if eq .Op.String " " -}}
35
-
<div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}-N{{$newStart}}">
36
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $oldStart }}</a></div>
37
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $newStart }}</a></div>
38
-
<div class="{{ $opStyle }}">{{ .Op.String }}</div>
39
-
<div class="px-2">{{ .Line }}</div>
40
-
</div>
35
+
<span class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}-N{{$newStart}}">
36
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $oldStart }}</a></span>
37
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $newStart }}</a></span>
38
+
<span class="{{ $opStyle }}">{{ .Op.String }}</span>
39
+
<span class="px-2 whitespace-pre">{{ .Line }}</span>
40
+
</span>
41
41
{{- $newStart = add64 $newStart 1 -}}
42
42
{{- $oldStart = add64 $oldStart 1 -}}
43
43
{{- end -}}
44
44
{{- end -}}
45
-
{{- end -}}</div></div></pre>
45
+
{{- end -}}</div></div></div>
46
46
{{ end }}
47
-
+35
-22
appview/pages/templates/repo/issues/fragments/commentList.html
+35
-22
appview/pages/templates/repo/issues/fragments/commentList.html
···
1
1
{{ define "repo/issues/fragments/commentList" }}
2
-
<div class="flex flex-col gap-8">
2
+
<div class="flex flex-col gap-4">
3
3
{{ range $item := .CommentList }}
4
4
{{ template "commentListing" (list $ .) }}
5
5
{{ end }}
···
19
19
<div class="rounded border border-gray-200 dark:border-gray-700 w-full overflow-hidden shadow-sm bg-gray-50 dark:bg-gray-800/50">
20
20
{{ template "topLevelComment" $params }}
21
21
22
-
<div class="relative ml-4 border-l-2 border-gray-200 dark:border-gray-700">
22
+
<div class="relative ml-10 border-l-2 border-gray-200 dark:border-gray-700">
23
23
{{ range $index, $reply := $comment.Replies }}
24
-
<div class="relative ">
25
-
<!-- Horizontal connector -->
26
-
<div class="absolute left-0 top-6 w-4 h-1 bg-gray-200 dark:bg-gray-700"></div>
27
-
28
-
<div class="pl-2">
29
-
{{
30
-
template "replyComment"
31
-
(dict
32
-
"RepoInfo" $root.RepoInfo
33
-
"LoggedInUser" $root.LoggedInUser
34
-
"Issue" $root.Issue
35
-
"Comment" $reply)
36
-
}}
37
-
</div>
24
+
<div class="-ml-4">
25
+
{{
26
+
template "replyComment"
27
+
(dict
28
+
"RepoInfo" $root.RepoInfo
29
+
"LoggedInUser" $root.LoggedInUser
30
+
"Issue" $root.Issue
31
+
"Comment" $reply)
32
+
}}
38
33
</div>
39
34
{{ end }}
40
35
</div>
···
44
39
{{ end }}
45
40
46
41
{{ define "topLevelComment" }}
47
-
<div class="rounded px-6 py-4 bg-white dark:bg-gray-800">
48
-
{{ template "repo/issues/fragments/issueCommentHeader" . }}
49
-
{{ template "repo/issues/fragments/issueCommentBody" . }}
42
+
<div class="rounded px-6 py-4 bg-white dark:bg-gray-800 flex gap-2 ">
43
+
<div class="flex-shrink-0">
44
+
<img
45
+
src="{{ tinyAvatar .Comment.Did }}"
46
+
alt=""
47
+
class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900"
48
+
/>
49
+
</div>
50
+
<div class="flex-1 min-w-0">
51
+
{{ template "repo/issues/fragments/issueCommentHeader" . }}
52
+
{{ template "repo/issues/fragments/issueCommentBody" . }}
53
+
</div>
50
54
</div>
51
55
{{ end }}
52
56
53
57
{{ define "replyComment" }}
54
-
<div class="p-4 w-full mx-auto overflow-hidden">
55
-
{{ template "repo/issues/fragments/issueCommentHeader" . }}
56
-
{{ template "repo/issues/fragments/issueCommentBody" . }}
58
+
<div class="py-4 pr-4 w-full mx-auto overflow-hidden flex gap-2 ">
59
+
<div class="flex-shrink-0">
60
+
<img
61
+
src="{{ tinyAvatar .Comment.Did }}"
62
+
alt=""
63
+
class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900"
64
+
/>
65
+
</div>
66
+
<div class="flex-1 min-w-0">
67
+
{{ template "repo/issues/fragments/issueCommentHeader" . }}
68
+
{{ template "repo/issues/fragments/issueCommentBody" . }}
69
+
</div>
57
70
</div>
58
71
{{ end }}
-63
appview/pages/templates/repo/issues/fragments/globalIssueListing.html
-63
appview/pages/templates/repo/issues/fragments/globalIssueListing.html
···
1
-
{{ define "repo/issues/fragments/globalIssueListing" }}
2
-
<div class="flex flex-col gap-2">
3
-
{{ range .Issues }}
4
-
<div class="rounded drop-shadow-sm bg-white px-6 py-4 dark:bg-gray-800 dark:border-gray-700">
5
-
<div class="pb-2 mb-3">
6
-
<div class="flex items-center gap-3 mb-2">
7
-
<a
8
-
href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}"
9
-
class="text-blue-600 dark:text-blue-400 font-medium hover:underline text-sm"
10
-
>
11
-
{{ resolve .Repo.Did }}/{{ .Repo.Name }}
12
-
</a>
13
-
</div>
14
-
<a
15
-
href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}/issues/{{ .IssueId }}"
16
-
class="no-underline hover:underline"
17
-
>
18
-
{{ .Title | description }}
19
-
<span class="text-gray-500">#{{ .IssueId }}</span>
20
-
</a>
21
-
</div>
22
-
<div class="text-sm text-gray-500 dark:text-gray-400 flex flex-wrap items-center gap-1">
23
-
{{ $bgColor := "bg-gray-800 dark:bg-gray-700" }}
24
-
{{ $icon := "ban" }}
25
-
{{ $state := "closed" }}
26
-
{{ if .Open }}
27
-
{{ $bgColor = "bg-green-600 dark:bg-green-700" }}
28
-
{{ $icon = "circle-dot" }}
29
-
{{ $state = "open" }}
30
-
{{ end }}
31
-
32
-
<span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }} text-sm">
33
-
{{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }}
34
-
<span class="text-white dark:text-white">{{ $state }}</span>
35
-
</span>
36
-
37
-
<span class="ml-1">
38
-
{{ template "user/fragments/picHandleLink" .Did }}
39
-
</span>
40
-
41
-
<span class="before:content-['ยท']">
42
-
{{ template "repo/fragments/time" .Created }}
43
-
</span>
44
-
45
-
<span class="before:content-['ยท']">
46
-
{{ $s := "s" }}
47
-
{{ if eq (len .Comments) 1 }}
48
-
{{ $s = "" }}
49
-
{{ end }}
50
-
<a href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}/issues/{{ .IssueId }}" class="text-gray-500 dark:text-gray-400">{{ len .Comments }} comment{{$s}}</a>
51
-
</span>
52
-
53
-
{{ $state := .Labels }}
54
-
{{ range $k, $d := $.LabelDefs }}
55
-
{{ range $v, $s := $state.GetValSet $d.AtUri.String }}
56
-
{{ template "labels/fragments/label" (dict "def" $d "val" $v "withPrefix" true) }}
57
-
{{ end }}
58
-
{{ end }}
59
-
</div>
60
-
</div>
61
-
{{ end }}
62
-
</div>
63
-
{{ end }}
+2
-1
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
+2
-1
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
···
1
1
{{ define "repo/issues/fragments/issueCommentHeader" }}
2
2
<div class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400 ">
3
-
{{ template "user/fragments/picHandleLink" .Comment.Did }}
3
+
{{ resolve .Comment.Did }}
4
4
{{ template "hats" $ }}
5
+
<span class="before:content-['ยท']"></span>
5
6
{{ template "timestamp" . }}
6
7
{{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }}
7
8
{{ if and $isCommentOwner (not .Comment.Deleted) }}
+2
-2
appview/pages/templates/repo/issues/fragments/issueListing.html
+2
-2
appview/pages/templates/repo/issues/fragments/issueListing.html
···
21
21
{{ $state = "open" }}
22
22
{{ end }}
23
23
24
-
<span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }} text-sm">
24
+
<span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }}">
25
25
{{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }}
26
-
<span class="text-white dark:text-white">{{ $state }}</span>
26
+
<span class="text-white dark:text-white text-sm">{{ $state }}</span>
27
27
</span>
28
28
29
29
<span class="ml-1">
+1
-1
appview/pages/templates/repo/issues/fragments/putIssue.html
+1
-1
appview/pages/templates/repo/issues/fragments/putIssue.html
+3
-3
appview/pages/templates/repo/issues/fragments/replyIssueCommentPlaceholder.html
+3
-3
appview/pages/templates/repo/issues/fragments/replyIssueCommentPlaceholder.html
···
1
1
{{ define "repo/issues/fragments/replyIssueCommentPlaceholder" }}
2
-
<div class="p-2 border-t flex gap-2 items-center border-gray-300 dark:border-gray-700">
2
+
<div class="py-2 px-6 border-t flex gap-2 items-center border-gray-300 dark:border-gray-700">
3
3
{{ if .LoggedInUser }}
4
4
<img
5
5
src="{{ tinyAvatar .LoggedInUser.Did }}"
6
6
alt=""
7
-
class="rounded-full h-6 w-6 mr-1 border border-gray-300 dark:border-gray-700"
7
+
class="rounded-full size-8 mr-1 border-2 border-gray-300 dark:border-gray-700"
8
8
/>
9
9
{{ end }}
10
10
<input
11
-
class="w-full py-2 border-none focus:outline-none"
11
+
class="w-full p-0 border-none focus:outline-none"
12
12
placeholder="Leave a reply..."
13
13
hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/reply"
14
14
hx-trigger="focus"
+5
-5
appview/pages/templates/repo/issues/issue.html
+5
-5
appview/pages/templates/repo/issues/issue.html
···
58
58
{{ $icon = "circle-dot" }}
59
59
{{ end }}
60
60
<div class="inline-flex items-center gap-2">
61
-
<div id="state"
62
-
class="inline-flex items-center rounded px-3 py-1 {{ $bgColor }}">
63
-
{{ i $icon "w-4 h-4 mr-1.5 text-white" }}
64
-
<span class="text-white">{{ .Issue.State }}</span>
65
-
</div>
61
+
<span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }}">
62
+
{{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }}
63
+
<span class="text-white dark:text-white text-sm">{{ .Issue.State }}</span>
64
+
</span>
65
+
66
66
<span class="text-gray-500 dark:text-gray-400 text-sm flex flex-wrap items-center gap-1">
67
67
opened by
68
68
{{ template "user/fragments/picHandleLink" .Issue.Did }}
+60
-69
appview/pages/templates/repo/pipelines/fragments/pipelineSymbol.html
+60
-69
appview/pages/templates/repo/pipelines/fragments/pipelineSymbol.html
···
1
1
{{ define "repo/pipelines/fragments/pipelineSymbol" }}
2
-
<div class="cursor-pointer">
3
-
{{ $c := .Counts }}
4
-
{{ $statuses := .Statuses }}
5
-
{{ $total := len $statuses }}
6
-
{{ $success := index $c "success" }}
7
-
{{ $fail := index $c "failed" }}
8
-
{{ $timeout := index $c "timeout" }}
9
-
{{ $empty := eq $total 0 }}
10
-
{{ $allPass := eq $success $total }}
11
-
{{ $allFail := eq $fail $total }}
12
-
{{ $allTimeout := eq $timeout $total }}
13
-
14
-
{{ if $empty }}
15
-
<div class="flex gap-1 items-center">
16
-
{{ i "hourglass" "size-4 text-gray-600 dark:text-gray-400 " }}
17
-
<span>0/{{ $total }}</span>
18
-
</div>
19
-
{{ else if $allPass }}
20
-
<div class="flex gap-1 items-center">
21
-
{{ i "check" "size-4 text-green-600" }}
22
-
<span>{{ $total }}/{{ $total }}</span>
23
-
</div>
24
-
{{ else if $allFail }}
25
-
<div class="flex gap-1 items-center">
26
-
{{ i "x" "size-4 text-red-500" }}
27
-
<span>0/{{ $total }}</span>
28
-
</div>
29
-
{{ else if $allTimeout }}
30
-
<div class="flex gap-1 items-center">
31
-
{{ i "clock-alert" "size-4 text-orange-500" }}
32
-
<span>0/{{ $total }}</span>
33
-
</div>
2
+
<div class="cursor-pointer flex gap-2 items-center">
3
+
{{ template "symbol" .Pipeline }}
4
+
{{ if .ShortSummary }}
5
+
{{ .Pipeline.ShortStatusSummary }}
34
6
{{ else }}
35
-
{{ $radius := f64 8 }}
36
-
{{ $circumference := mulf64 2.0 (mulf64 3.1416 $radius) }}
37
-
{{ $offset := 0.0 }}
38
-
<div class="flex gap-1 items-center">
39
-
<svg class="w-4 h-4 transform -rotate-90" viewBox="0 0 20 20">
40
-
<circle cx="10" cy="10" r="{{ $radius }}" fill="none" stroke="#f3f4f633" stroke-width="2"/>
7
+
{{ .Pipeline.LongStatusSummary }}
8
+
{{ end }}
9
+
</div>
10
+
{{ end }}
41
11
42
-
{{ range $kind, $count := $c }}
43
-
{{ $color := "" }}
44
-
{{ if or (eq $kind "pending") (eq $kind "running") }}
45
-
{{ $color = "#eab308" }} {{/* amber-500 */}}
46
-
{{ else if eq $kind "success" }}
47
-
{{ $color = "#10b981" }} {{/* green-500 */}}
48
-
{{ else if eq $kind "cancelled" }}
49
-
{{ $color = "#6b7280" }} {{/* gray-500 */}}
50
-
{{ else if eq $kind "timeout" }}
51
-
{{ $color = "#fb923c" }} {{/* orange-400 */}}
52
-
{{ else }}
53
-
{{ $color = "#ef4444" }} {{/* red-500 for failed or unknown */}}
54
-
{{ end }}
12
+
{{ define "symbol" }}
13
+
{{ $c := .Counts }}
14
+
{{ $statuses := .Statuses }}
15
+
{{ $total := len $statuses }}
16
+
{{ $success := index $c "success" }}
17
+
{{ $fail := index $c "failed" }}
18
+
{{ $timeout := index $c "timeout" }}
19
+
{{ $empty := eq $total 0 }}
20
+
{{ $allPass := eq $success $total }}
21
+
{{ $allFail := eq $fail $total }}
22
+
{{ $allTimeout := eq $timeout $total }}
55
23
56
-
{{ $percent := divf64 (f64 $count) (f64 $total) }}
57
-
{{ $length := mulf64 $percent $circumference }}
58
-
59
-
<circle
60
-
cx="10" cy="10" r="{{ $radius }}"
61
-
fill="none"
62
-
stroke="{{ $color }}"
63
-
stroke-width="2"
64
-
stroke-dasharray="{{ printf "%.2f %.2f" $length (subf64 $circumference $length) }}"
65
-
stroke-dashoffset="{{ printf "%.2f" (negf64 $offset) }}"
66
-
/>
67
-
{{ $offset = addf64 $offset $length }}
68
-
{{ end }}
69
-
</svg>
70
-
<span>{{ $success }}/{{ $total }}</span>
71
-
</div>
72
-
{{ end }}
73
-
</div>
24
+
{{ if $empty }}
25
+
{{ i "hourglass" "size-4 text-gray-600 dark:text-gray-400 " }}
26
+
{{ else if $allPass }}
27
+
{{ i "check" "size-4 text-green-600 dark:text-green-500" }}
28
+
{{ else if $allFail }}
29
+
{{ i "x" "size-4 text-red-600 dark:text-red-500" }}
30
+
{{ else if $allTimeout }}
31
+
{{ i "clock-alert" "size-4 text-orange-500" }}
32
+
{{ else }}
33
+
{{ $radius := f64 8 }}
34
+
{{ $circumference := mulf64 2.0 (mulf64 3.1416 $radius) }}
35
+
{{ $offset := 0.0 }}
36
+
<svg class="w-4 h-4 transform -rotate-90" viewBox="0 0 20 20">
37
+
<circle cx="10" cy="10" r="{{ $radius }}" fill="none" class="stroke-gray-200 dark:stroke-gray-700" stroke-width="2"/>
38
+
{{ range $kind, $count := $c }}
39
+
{{ $colorClass := "" }}
40
+
{{ if or (eq $kind "pending") (eq $kind "running") }}
41
+
{{ $colorClass = "stroke-yellow-600 dark:stroke-yellow-500" }}
42
+
{{ else if eq $kind "success" }}
43
+
{{ $colorClass = "stroke-green-600 dark:stroke-green-500" }}
44
+
{{ else if eq $kind "cancelled" }}
45
+
{{ $colorClass = "stroke-gray-600 dark:stroke-gray-500" }}
46
+
{{ else if eq $kind "timeout" }}
47
+
{{ $colorClass = "stroke-orange-600 dark:stroke-orange-500" }}
48
+
{{ else }}
49
+
{{ $colorClass = "stroke-red-600 dark:stroke-red-500" }}
50
+
{{ end }}
51
+
{{ $percent := divf64 (f64 $count) (f64 $total) }}
52
+
{{ $length := mulf64 $percent $circumference }}
53
+
<circle
54
+
cx="10" cy="10" r="{{ $radius }}"
55
+
fill="none"
56
+
class="{{ $colorClass }}"
57
+
stroke-width="2"
58
+
stroke-dasharray="{{ printf "%.2f %.2f" $length (subf64 $circumference $length) }}"
59
+
stroke-dashoffset="{{ printf "%.2f" (negf64 $offset) }}"
60
+
/>
61
+
{{ $offset = addf64 $offset $length }}
62
+
{{ end }}
63
+
</svg>
64
+
{{ end }}
74
65
{{ end }}
+1
-1
appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html
+1
-1
appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html
···
4
4
<div class="relative inline-block">
5
5
<details class="relative">
6
6
<summary class="cursor-pointer list-none">
7
-
{{ template "repo/pipelines/fragments/pipelineSymbol" .Pipeline }}
7
+
{{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" true) }}
8
8
</summary>
9
9
{{ template "repo/pipelines/fragments/tooltip" $ }}
10
10
</details>
+1
-1
appview/pages/templates/repo/pipelines/pipelines.html
+1
-1
appview/pages/templates/repo/pipelines/pipelines.html
···
23
23
</p>
24
24
<p>
25
25
<span class="{{ $bullet }}">2</span>Configure your CI/CD
26
-
<a href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/pipeline.md" class="underline">pipeline</a>.
26
+
<a href="https://docs.tangled.org/spindles.html#pipelines" class="underline">pipeline</a>.
27
27
</p>
28
28
<p><span class="{{ $bullet }}">3</span>Trigger a workflow with a push or a pull-request!</p>
29
29
</div>
+17
-17
appview/pages/templates/repo/pulls/fragments/pullActions.html
+17
-17
appview/pages/templates/repo/pulls/fragments/pullActions.html
···
22
22
{{ $isLastRound := eq $roundNumber $lastIdx }}
23
23
{{ $isSameRepoBranch := .Pull.IsBranchBased }}
24
24
{{ $isUpToDate := .ResubmitCheck.No }}
25
-
<div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2 relative">
25
+
<div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2 relative p-2">
26
26
<button
27
27
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment"
28
28
hx-target="#actions-{{$roundNumber}}"
29
29
hx-swap="outerHtml"
30
-
class="btn p-2 flex items-center gap-2 no-underline hover:no-underline group">
31
-
{{ i "message-square-plus" "w-4 h-4" }}
32
-
<span>comment</span>
30
+
class="btn-flat p-2 flex items-center gap-2 no-underline hover:no-underline group">
31
+
{{ i "message-square-plus" "w-4 h-4 inline group-[.htmx-request]:hidden" }}
33
32
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
33
+
comment
34
34
</button>
35
35
{{ if .BranchDeleteStatus }}
36
36
<button
37
37
hx-delete="/{{ .BranchDeleteStatus.Repo.Did }}/{{ .BranchDeleteStatus.Repo.Name }}/branches"
38
38
hx-vals='{"branch": "{{ .BranchDeleteStatus.Branch }}" }'
39
39
hx-swap="none"
40
-
class="btn p-2 flex items-center gap-2 no-underline hover:no-underline group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
40
+
class="btn-flat p-2 flex items-center gap-2 no-underline hover:no-underline group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
41
41
{{ i "git-branch" "w-4 h-4" }}
42
42
<span>delete branch</span>
43
43
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
···
52
52
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge"
53
53
hx-swap="none"
54
54
hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?"
55
-
class="btn p-2 flex items-center gap-2 group" {{ $disabled }}>
56
-
{{ i "git-merge" "w-4 h-4" }}
57
-
<span>merge{{if $stackCount}} {{$stackCount}}{{end}}</span>
55
+
class="btn-flat p-2 flex items-center gap-2 group" {{ $disabled }}>
56
+
{{ i "git-merge" "w-4 h-4 inline group-[.htmx-request]:hidden" }}
58
57
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
58
+
merge{{if $stackCount}} {{$stackCount}}{{end}}
59
59
</button>
60
60
{{ end }}
61
61
···
74
74
{{ end }}
75
75
76
76
hx-disabled-elt="#resubmitBtn"
77
-
class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed group" {{ $disabled }}
77
+
class="btn-flat p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed group" {{ $disabled }}
78
78
79
79
{{ if $disabled }}
80
80
title="Update this branch to resubmit this pull request"
···
82
82
title="Resubmit this pull request"
83
83
{{ end }}
84
84
>
85
-
{{ i "rotate-ccw" "w-4 h-4" }}
86
-
<span>resubmit</span>
85
+
{{ i "rotate-ccw" "w-4 h-4 inline group-[.htmx-request]:hidden" }}
87
86
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
87
+
resubmit
88
88
</button>
89
89
{{ end }}
90
90
···
92
92
<button
93
93
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/close"
94
94
hx-swap="none"
95
-
class="btn p-2 flex items-center gap-2 group">
96
-
{{ i "ban" "w-4 h-4" }}
97
-
<span>close</span>
95
+
class="btn-flat p-2 flex items-center gap-2 group">
96
+
{{ i "ban" "w-4 h-4 inline group-[.htmx-request]:hidden" }}
98
97
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
98
+
close
99
99
</button>
100
100
{{ end }}
101
101
···
103
103
<button
104
104
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/reopen"
105
105
hx-swap="none"
106
-
class="btn p-2 flex items-center gap-2 group">
107
-
{{ i "refresh-ccw-dot" "w-4 h-4" }}
108
-
<span>reopen</span>
106
+
class="btn-flat p-2 flex items-center gap-2 group">
107
+
{{ i "refresh-ccw-dot" "w-4 h-4 inline group-[.htmx-request]:hidden" }}
109
108
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
109
+
reopen
110
110
</button>
111
111
{{ end }}
112
112
</div>
+6
-7
appview/pages/templates/repo/pulls/fragments/pullHeader.html
+6
-7
appview/pages/templates/repo/pulls/fragments/pullHeader.html
···
1
1
{{ define "repo/pulls/fragments/pullHeader" }}
2
-
<header class="pb-4">
2
+
<header class="pb-2">
3
3
<h1 class="text-2xl dark:text-white">
4
4
{{ .Pull.Title | description }}
5
5
<span class="text-gray-500 dark:text-gray-400">#{{ .Pull.PullId }}</span>
···
17
17
{{ $icon = "git-merge" }}
18
18
{{ end }}
19
19
20
-
<section class="mt-2">
20
+
<section>
21
21
<div class="flex items-center gap-2">
22
-
<div
23
-
id="state"
24
-
class="inline-flex items-center rounded px-3 py-1 {{ $bgColor }}"
22
+
<span
23
+
class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }} text-sm"
25
24
>
26
-
{{ i $icon "w-4 h-4 mr-1.5 text-white" }}
25
+
{{ i $icon "w-3 h-3 mr-1.5 text-white" }}
27
26
<span class="text-white">{{ .Pull.State.String }}</span>
28
-
</div>
27
+
</span>
29
28
<span class="text-gray-500 dark:text-gray-400 text-sm flex flex-wrap items-center gap-1">
30
29
opened by
31
30
{{ template "user/fragments/picHandleLink" .Pull.OwnerDid }}
+39
-24
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
+39
-24
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
···
1
1
{{ define "repo/pulls/fragments/pullNewComment" }}
2
2
<div
3
3
id="pull-comment-card-{{ .RoundNumber }}"
4
-
class="bg-white dark:bg-gray-800 rounded drop-shadow-sm p-4 relative w-full flex flex-col gap-2">
5
-
<div class="text-sm text-gray-500 dark:text-gray-400">
6
-
{{ resolve .LoggedInUser.Did }}
7
-
</div>
4
+
class="w-full flex flex-col gap-2">
5
+
{{ template "user/fragments/picHandleLink" .LoggedInUser.Did }}
8
6
<form
9
7
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/comment"
10
-
hx-indicator="#create-comment-spinner"
11
8
hx-swap="none"
12
-
class="w-full flex flex-wrap gap-2"
9
+
hx-on::after-request="if(event.detail.successful) this.reset()"
10
+
hx-disabled-elt="#reply-{{ .RoundNumber }}"
11
+
class="w-full flex flex-wrap gap-2 group"
13
12
>
14
13
<textarea
15
14
name="body"
16
15
class="w-full p-2 rounded border border-gray-200"
16
+
rows=8
17
17
placeholder="Add to the discussion..."></textarea
18
18
>
19
-
<button type="submit" class="btn flex items-center gap-2">
20
-
{{ i "message-square" "w-4 h-4" }}
21
-
<span>comment</span>
22
-
<span id="create-comment-spinner" class="group">
23
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
24
-
</span>
25
-
</button>
26
-
<button
27
-
type="button"
28
-
class="btn flex items-center gap-2 group"
29
-
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/actions"
30
-
hx-swap="outerHTML"
31
-
hx-target="#pull-comment-card-{{ .RoundNumber }}"
32
-
>
33
-
{{ i "x" "w-4 h-4" }}
34
-
<span>cancel</span>
35
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
36
-
</button>
19
+
{{ template "replyActions" . }}
37
20
<div id="pull-comment"></div>
38
21
</form>
39
22
</div>
40
23
{{ end }}
24
+
25
+
{{ define "replyActions" }}
26
+
<div class="flex flex-wrap items-stretch justify-end gap-2 text-gray-500 dark:text-gray-400 text-sm w-full">
27
+
{{ template "cancel" . }}
28
+
{{ template "reply" . }}
29
+
</div>
30
+
{{ end }}
31
+
32
+
{{ define "cancel" }}
33
+
<button
34
+
type="button"
35
+
class="btn text-red-500 dark:text-red-400 flex gap-2 items-center group"
36
+
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/actions"
37
+
hx-swap="outerHTML"
38
+
hx-target="#actions-{{.RoundNumber}}"
39
+
>
40
+
{{ i "x" "w-4 h-4" }}
41
+
<span>cancel</span>
42
+
</button>
43
+
{{ end }}
44
+
45
+
{{ define "reply" }}
46
+
<button
47
+
type="submit"
48
+
id="reply-{{ .RoundNumber }}"
49
+
class="btn-create flex items-center gap-2">
50
+
{{ i "reply" "w-4 h-4 inline group-[.htmx-request]:hidden" }}
51
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
52
+
reply
53
+
</button>
54
+
{{ end }}
55
+
+20
appview/pages/templates/repo/pulls/fragments/replyPullCommentPlaceholder.html
+20
appview/pages/templates/repo/pulls/fragments/replyPullCommentPlaceholder.html
···
1
+
{{ define "repo/pulls/fragments/replyPullCommentPlaceholder" }}
2
+
<div class="py-2 px-6 border-t flex gap-2 items-center border-gray-300 dark:border-gray-700">
3
+
{{ if .LoggedInUser }}
4
+
<img
5
+
src="{{ tinyAvatar .LoggedInUser.Did }}"
6
+
alt=""
7
+
class="rounded-full size-8 mr-1 border-2 border-gray-300 dark:border-gray-700"
8
+
/>
9
+
{{ end }}
10
+
<input
11
+
class="w-full p-0 border-none focus:outline-none"
12
+
placeholder="Leave a reply..."
13
+
hx-get="/{{ .Submission.ID }}/reply"
14
+
hx-trigger="focus"
15
+
hx-target="closest div"
16
+
hx-swap="outerHTML"
17
+
>
18
+
</input>
19
+
</div>
20
+
{{ end }}
+1
-1
appview/pages/templates/repo/pulls/fragments/summarizedPullHeader.html
+1
-1
appview/pages/templates/repo/pulls/fragments/summarizedPullHeader.html
···
18
18
{{ $lastSubmission := index .Submissions $latestRound }}
19
19
{{ $commentCount := len $lastSubmission.Comments }}
20
20
{{ if and $pipeline $pipeline.Id }}
21
-
{{ template "repo/pipelines/fragments/pipelineSymbol" $pipeline }}
21
+
{{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" true) }}
22
22
<span class="before:content-['ยท'] before:select-none text-gray-500 dark:text-gray-400"></span>
23
23
{{ end }}
24
24
<span>
+334
-77
appview/pages/templates/repo/pulls/pull.html
+334
-77
appview/pages/templates/repo/pulls/pull.html
···
6
6
{{ template "repo/pulls/fragments/og" (dict "RepoInfo" .RepoInfo "Pull" .Pull) }}
7
7
{{ end }}
8
8
9
+
{{ define "mainLayout" }}
10
+
<div class="px-1 col-span-full flex-grow flex flex-col gap-4">
11
+
{{ block "contentLayout" . }}
12
+
{{ block "content" . }}{{ end }}
13
+
{{ end }}
14
+
</div>
15
+
{{ end }}
16
+
9
17
{{ define "repoContentLayout" }}
10
-
<div class="grid grid-cols-1 md:grid-cols-10 gap-4 w-full">
11
-
<div class="col-span-1 md:col-span-8">
12
-
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto dark:text-white">
18
+
<div class="grid grid-cols-1 md:grid-cols-10 gap-y-2 gap-x-4 w-full">
19
+
<div class="col-span-1 md:col-span-7">
20
+
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto dark:text-white h-full">
13
21
{{ block "repoContent" . }}{{ end }}
14
22
</section>
15
23
{{ block "repoAfter" . }}{{ end }}
16
24
</div>
17
-
<div class="col-span-1 md:col-span-2 flex flex-col gap-6">
25
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
18
26
{{ template "repo/fragments/labelPanel"
19
27
(dict "RepoInfo" $.RepoInfo
20
28
"Defs" $.LabelDefs
···
26
34
"Backlinks" $.Backlinks) }}
27
35
{{ template "repo/fragments/externalLinkPanel" $.Pull.AtUri }}
28
36
</div>
37
+
38
+
<style>
39
+
#filesToggle:checked ~ div label[for="filesToggle"] .show-text { display: none; }
40
+
#filesToggle:checked ~ div label[for="filesToggle"] .hide-text { display: inline; }
41
+
#filesToggle:not(:checked) ~ div label[for="filesToggle"] .hide-text { display: none; }
42
+
43
+
#filesToggle:checked ~ div div#files { width: 10vw; margin-right: 1rem; }
44
+
#filesToggle:not(:checked) ~ div div#files { width: 0; display: hidden; margin-right: 0; }
45
+
46
+
#subsToggle:checked ~ div div#subs { width: 25vw; margin-left: 1rem; }
47
+
#subsToggle:not(:checked) ~ div div#subs { width: 0; display: hidden; margin-left: 0; }
48
+
</style>
49
+
50
+
<!-- Checkboxes must come first as siblings -->
51
+
<input type="checkbox" id="filesToggle" class="peer/files hidden" checked/>
52
+
<input type="checkbox" id="subsToggle" class="peer/subs hidden" checked/>
53
+
54
+
<!-- Top bar with controls -->
55
+
<div class="sticky top-0 z-30 bg-slate-100 dark:bg-gray-900 flex items-center gap-2 col-span-full h-12">
56
+
<label for="filesToggle" class="inline-flex items-center justify-center rounded cursor-pointer p-2 text-normal font-normal normalcase">
57
+
<span class="show-text">{{ i "panel-left-open" "size-5" }}</span>
58
+
<span class="hide-text">{{ i "panel-left-close" "size-5" }}</span>
59
+
</label>
60
+
{{ template "repo/fragments/diffStatPill" .Diff.Stat }}
61
+
{{ .Diff.Stat.FilesChanged }} changed file{{ if ne .Diff.Stat.FilesChanged 1 }}s{{ end }}
62
+
<div class="flex-grow"></div>
63
+
{{ template "repo/fragments/diffOpts" .DiffOpts }}
64
+
<label for="subsToggle" class="inline-flex items-center justify-center rounded cursor-pointer p-2">
65
+
{{ i "message-square-more" "size-5" }}
66
+
</label>
67
+
</div>
68
+
69
+
<div class="flex col-span-full">
70
+
<!-- left panel -->
71
+
<div id="files" class="w-0 overflow-hidden sticky top-12 max-h-screen overflow-y-auto pb-12">
72
+
{{ template "repo/fragments/diffChangedFiles" .Diff }}
73
+
</div>
74
+
75
+
<!-- main content -->
76
+
<div class="flex-1 min-w-0 sticky top-12 pb-12">
77
+
{{ template "repo/fragments/diff" (list .Diff .DiffOpts) }}
78
+
</div>
79
+
80
+
<!-- right panel -->
81
+
<div id="subs" class="w-0 overflow-hidden max-h-screen flex flex-col sticky top-12 pb-12">
82
+
<div class="z-20 sticky top-0 rounded-t p-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
83
+
<h2 class="font-bold uppercase">history</h2>
84
+
</div>
85
+
<div class="flex flex-col-reverse gap-4 overflow-y-auto">
86
+
{{ template "submissions2" . }}
87
+
</div>
88
+
</div>
89
+
</div>
29
90
</div>
30
91
{{ end }}
31
92
32
93
{{ define "repoContent" }}
33
94
{{ template "repo/pulls/fragments/pullHeader" . }}
34
-
35
95
{{ if .Pull.IsStacked }}
36
96
<div class="mt-8">
37
97
{{ template "repo/pulls/fragments/pullStack" . }}
···
40
100
{{ end }}
41
101
42
102
{{ define "repoAfter" }}
43
-
<section id="submissions" class="mt-4">
44
-
<div class="flex flex-col gap-4">
45
-
{{ block "submissions" . }} {{ end }}
103
+
<div id="pull-close"></div>
104
+
<div id="pull-reopen"></div>
105
+
{{ end }}
106
+
107
+
{{ define "submissions2" }}
108
+
{{ $lastIdx := sub (len .Pull.Submissions) 1 }}
109
+
{{ range $ridx, $item := reverse .Pull.Submissions }}
110
+
{{ $idx := sub $lastIdx $ridx }}
111
+
<div class="rounded border border-gray-200 dark:border-gray-700 w-full shadow-sm bg-gray-50 dark:bg-gray-800/50">
112
+
{{ with $item }}
113
+
{{ $patches := .AsFormatPatch }}
114
+
{{ $round := .RoundNumber }}
115
+
<div class="rounded px-6 py-4 bg-white dark:bg-gray-800 flex gap-2">
116
+
<div class="flex-shrink-0">
117
+
<img
118
+
src="{{ tinyAvatar $.Pull.OwnerDid }}"
119
+
alt=""
120
+
class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900"
121
+
/>
122
+
</div>
123
+
<!-- right column: name and body in two rows -->
124
+
<div class="flex-1 min-w-0 flex flex-col gap-1">
125
+
<div class="flex gap-2 items-center justify-between mb-1">
126
+
<span class="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
127
+
{{ resolve $.Pull.OwnerDid }} submitted v{{ $round }}
128
+
<span class="select-none before:content-['\00B7']"></span>
129
+
<a class="text-gray-500 dark:text-gray-400 hover:text-gray-500" href="#round-#{{ $round }}">{{ template "repo/fragments/shortTimeAgo" .Created }}</a>
130
+
</span>
131
+
{{ if ne $idx 0 }}
132
+
<a class="flex items-center gap-2 no-underline hover:no-underline text-sm"
133
+
hx-boost="true"
134
+
href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{$round}}/interdiff">
135
+
{{ i "chevrons-left-right-ellipsis" "w-4 h-4 rotate-90" }}
136
+
<span class="hidden md:inline">interdiff</span>
137
+
</a>
138
+
{{ end }}
139
+
</div>
140
+
<details class="group">
141
+
<summary class="list-none cursor-pointer flex items-center gap-2">
142
+
<span>{{ i "git-commit-horizontal" "w-4 h-4" }}</span>
143
+
{{ len $patches }} commit{{ if ne (len $patches) 1 }}s{{ end }}
144
+
</summary>
145
+
{{ range $patches }}
146
+
<div id="commit-{{.SHA}}" class="py-1 relative w-full md:max-w-3/5 md:w-fit flex flex-col text-gray-600 dark:text-gray-300">
147
+
<div class="flex items-baseline gap-2">
148
+
<div>
149
+
<!-- attempt to resolve $fullRepo: this is possible only on non-deleted forks and branches -->
150
+
{{ $fullRepo := "" }}
151
+
{{ if and $.Pull.IsForkBased $.Pull.PullSource.Repo }}
152
+
{{ $fullRepo = printf "%s/%s" $.Pull.OwnerDid $.Pull.PullSource.Repo.Name }}
153
+
{{ else if $.Pull.IsBranchBased }}
154
+
{{ $fullRepo = $.RepoInfo.FullName }}
155
+
{{ end }}
156
+
157
+
<!-- if $fullRepo was resolved, link to it, otherwise just span without a link -->
158
+
{{ if $fullRepo }}
159
+
<a href="/{{ $fullRepo }}/commit/{{ .SHA }}" class="font-mono text-gray-600 dark:text-gray-300">{{ slice .SHA 0 8 }}</a>
160
+
{{ else }}
161
+
<span class="font-mono">{{ slice .SHA 0 8 }}</span>
162
+
{{ end }}
163
+
</div>
164
+
165
+
<div>
166
+
<span>{{ .Title | description }}</span>
167
+
{{ if gt (len .Body) 0 }}
168
+
<button
169
+
class="py-1/2 px-1 mx-2 bg-gray-200 hover:bg-gray-400 rounded dark:bg-gray-700 dark:hover:bg-gray-600"
170
+
hx-on:click="document.getElementById('body-{{$round}}-{{.SHA}}').classList.toggle('hidden')"
171
+
>
172
+
{{ i "ellipsis" "w-3 h-3" }}
173
+
</button>
174
+
{{ end }}
175
+
{{ if gt (len .Body) 0 }}
176
+
<p id="body-{{$round}}-{{.SHA}}" class="hidden mt-1 text-sm pb-2">{{ nl2br .Body }}</p>
177
+
{{ end }}
178
+
</div>
179
+
</div>
180
+
</div>
181
+
{{ end }}
182
+
</details>
183
+
<div>
184
+
{{ block "pipelineStatus" (list $ .) }} {{ end }}
185
+
</div>
186
+
{{ if eq $lastIdx .RoundNumber }}
187
+
{{ block "mergeCheck" $ }} {{ end }}
188
+
{{ end }}
189
+
</div>
190
+
</div>
191
+
<div class="relative ml-10 border-l-2 border-gray-200 dark:border-gray-700">
192
+
{{ range $cidx, $c := .Comments }}
193
+
<div id="comment-{{$c.ID}}" class="flex gap-2 -ml-4 py-4 w-full mx-auto">
194
+
<!-- left column: profile picture -->
195
+
<div class="flex-shrink-0">
196
+
<img
197
+
src="{{ tinyAvatar $c.OwnerDid }}"
198
+
alt=""
199
+
class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900"
200
+
/>
201
+
</div>
202
+
<!-- right column: name and body in two rows -->
203
+
<div class="flex-1 min-w-0">
204
+
<!-- Row 1: Author and timestamp -->
205
+
<div class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1">
206
+
<span>{{ resolve $c.OwnerDid }}</span>
207
+
<span class="before:content-['ยท']"></span>
208
+
<a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.ID}}">{{ template "repo/fragments/time" $c.Created }}</a>
209
+
</div>
210
+
<!-- Row 2: Body text -->
211
+
<div class="prose dark:prose-invert mt-1">
212
+
{{ $c.Body | markdown }}
213
+
</div>
214
+
</div>
215
+
</div>
216
+
{{ end }}
217
+
</div>
218
+
{{ end }}
219
+
{{ if eq $lastIdx .RoundNumber }}
220
+
{{ block "mergeStatus" $ }} {{ end }}
221
+
{{ block "resubmitStatus" $ }} {{ end }}
222
+
{{ end }}
223
+
{{ if $.LoggedInUser }}
224
+
{{ template "repo/pulls/fragments/pullActions"
225
+
(dict
226
+
"LoggedInUser" $.LoggedInUser
227
+
"Pull" $.Pull
228
+
"RepoInfo" $.RepoInfo
229
+
"RoundNumber" .RoundNumber
230
+
"MergeCheck" $.MergeCheck
231
+
"ResubmitCheck" $.ResubmitCheck
232
+
"BranchDeleteStatus" $.BranchDeleteStatus
233
+
"Stack" $.Stack) }}
234
+
{{ else }}
235
+
<div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded drop-shadow-sm p-2 relative flex gap-2 items-center">
236
+
<a href="/signup" class="btn-create py-0 hover:no-underline hover:text-white flex items-center gap-2">
237
+
sign up
238
+
</a>
239
+
<span class="text-gray-500 dark:text-gray-400">or</span>
240
+
<a href="/login" class="underline">login</a>
241
+
to add to the discussion
242
+
</div>
243
+
{{ end }}
244
+
</div>
245
+
{{ end }}
246
+
{{ end }}
247
+
248
+
{{ define "newComment" }}
249
+
{{ $root := index . 0 }}
250
+
{{ $submission := index . 1 }}
251
+
<form
252
+
id="comment-form"
253
+
hx-post="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ $submission.RoundNumber }}/comment"
254
+
hx-on::after-request="if(event.detail.successful) this.reset()"
255
+
>
256
+
<div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full">
257
+
<div class="text-sm pb-2 text-gray-500 dark:text-gray-400">
258
+
{{ template "user/fragments/picHandleLink" $root.LoggedInUser.Did }}
46
259
</div>
47
-
</section>
260
+
<textarea
261
+
id="comment-textarea"
262
+
name="body"
263
+
class="w-full p-2 rounded border border-gray-200 dark:border-gray-700"
264
+
placeholder="Add to the discussion"
265
+
rows="8"
266
+
></textarea>
267
+
<div id="pull-comment"></div>
268
+
</div>
269
+
{{ template "replyActions" . }}
270
+
</form>
271
+
{{ end }}
48
272
49
-
<div id="pull-close"></div>
50
-
<div id="pull-reopen"></div>
273
+
{{ define "replyActions" }}
274
+
<div class="flex flex-wrap items-stretch justify-end gap-2 text-gray-500 dark:text-gray-400 text-sm">
275
+
{{ template "cancel" . }}
276
+
{{ template "reply" . }}
277
+
</div>
278
+
{{ end }}
279
+
280
+
{{ define "cancel" }}
281
+
<button
282
+
class="btn text-red-500 dark:text-red-400 flex gap-2 items-center group"
283
+
hx-get="TODO"
284
+
hx-target="TODO"
285
+
hx-swap="outerHTML">
286
+
{{ i "x" "size-4" }}
287
+
cancel
288
+
</button>
289
+
{{ end }}
290
+
291
+
{{ define "reply" }}
292
+
<button
293
+
id="TODO"
294
+
type="submit"
295
+
class="btn-create flex items-center gap-2 no-underline hover:no-underline">
296
+
{{ i "reply" "w-4 h-4 inline group-[.htmx-request]:hidden" }}
297
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
298
+
reply
299
+
</button>
51
300
{{ end }}
52
301
53
302
{{ define "submissions" }}
···
214
463
{{ end }}
215
464
{{ end }}
216
465
466
+
{{ define "mergeCheck" }}
467
+
{{ $isOpen := .Pull.State.IsOpen }}
468
+
{{ if and $isOpen .MergeCheck .MergeCheck.Error }}
469
+
<div class="flex items-center gap-2">
470
+
{{ i "triangle-alert" "w-4 h-4 text-red-500 dark:text-red-300" }}
471
+
{{ .MergeCheck.Error }}
472
+
</div>
473
+
{{ else if and $isOpen .MergeCheck .MergeCheck.IsConflicted }}
474
+
<details class="group">
475
+
<summary class="flex items-center justify-between cursor-pointer list-none">
476
+
<div class="flex items-center gap-2 ">
477
+
{{ i "triangle-alert" "w-4 h-4" }}
478
+
<span class="font-medium">merge conflicts detected</span>
479
+
</div>
480
+
<div>
481
+
<span class="group-open:hidden inline">{{ i "chevrons-up-down" "w-4 h-4" }}</span>
482
+
<span class="hidden group-open:inline">{{ i "chevrons-down-up" "w-4 h-4" }}</span>
483
+
</div>
484
+
</summary>
485
+
{{ if gt (len .MergeCheck.Conflicts) 0 }}
486
+
<ul class="space-y-1 mt-2">
487
+
{{ range .MergeCheck.Conflicts }}
488
+
{{ if .Filename }}
489
+
<li class="flex items-center">
490
+
{{ i "file-warning" "inline-flex w-4 h-4 mr-1.5 text-red-500 dark:text-red-300 flex-shrink-0" }}
491
+
<span class="font-mono" style="word-break: keep-all; overflow-wrap: break-word;">{{ .Filename }}</span>
492
+
</li>
493
+
{{ else if .Reason }}
494
+
<li class="flex items-center">
495
+
{{ i "file-warning" "w-4 h-4 mr-1.5 text-red-500 dark:text-red-300" }}
496
+
<span>{{.Reason}}</span>
497
+
</li>
498
+
{{ end }}
499
+
{{ end }}
500
+
</ul>
501
+
{{ end }}
502
+
</details>
503
+
{{ else if and $isOpen .MergeCheck }}
504
+
<div class="flex items-center gap-2">
505
+
{{ i "check" "w-4 h-4 text-green-600 dark:text-green-500" }}
506
+
<span>no conflicts, ready to merge</span>
507
+
</div>
508
+
{{ end }}
509
+
{{ end }}
510
+
217
511
{{ define "mergeStatus" }}
218
512
{{ if .Pull.State.IsClosed }}
219
-
<div class="bg-gray-50 dark:bg-gray-700 border border-black dark:border-gray-500 rounded drop-shadow-sm px-6 py-2 relative w-fit">
513
+
<div class="bg-gray-50 dark:bg-gray-700 border border-black dark:border-gray-500 rounded drop-shadow-sm px-6 py-2 relative">
220
514
<div class="flex items-center gap-2 text-black dark:text-white">
221
515
{{ i "ban" "w-4 h-4" }}
222
516
<span class="font-medium">closed without merging</span
···
224
518
</div>
225
519
</div>
226
520
{{ else if .Pull.State.IsMerged }}
227
-
<div class="bg-purple-50 dark:bg-purple-900 border border-purple-500 rounded drop-shadow-sm px-6 py-2 relative w-fit">
521
+
<div class="bg-purple-50 dark:bg-purple-900 border border-purple-500 rounded drop-shadow-sm px-6 py-2 relative">
228
522
<div class="flex items-center gap-2 text-purple-500 dark:text-purple-300">
229
523
{{ i "git-merge" "w-4 h-4" }}
230
524
<span class="font-medium">pull request successfully merged</span
···
232
526
</div>
233
527
</div>
234
528
{{ else if .Pull.State.IsDeleted }}
235
-
<div class="bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-6 py-2 relative w-fit">
529
+
<div class="bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-6 py-2 relative">
236
530
<div class="flex items-center gap-2 text-red-500 dark:text-red-300">
237
531
{{ i "git-pull-request-closed" "w-4 h-4" }}
238
532
<span class="font-medium">This pull has been deleted (possibly by jj abandon or jj squash)</span>
239
533
</div>
240
534
</div>
241
-
{{ else if and .MergeCheck .MergeCheck.Error }}
242
-
<div class="bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-6 py-2 relative w-fit">
243
-
<div class="flex items-center gap-2 text-red-500 dark:text-red-300">
244
-
{{ i "triangle-alert" "w-4 h-4" }}
245
-
<span class="font-medium">{{ .MergeCheck.Error }}</span>
246
-
</div>
247
-
</div>
248
-
{{ else if and .MergeCheck .MergeCheck.IsConflicted }}
249
-
<div class="bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-6 py-2 relative w-fit">
250
-
<div class="flex flex-col gap-2 text-red-500 dark:text-red-300">
251
-
<div class="flex items-center gap-2">
252
-
{{ i "triangle-alert" "w-4 h-4" }}
253
-
<span class="font-medium">merge conflicts detected</span>
254
-
</div>
255
-
{{ if gt (len .MergeCheck.Conflicts) 0 }}
256
-
<ul class="space-y-1">
257
-
{{ range .MergeCheck.Conflicts }}
258
-
{{ if .Filename }}
259
-
<li class="flex items-center">
260
-
{{ i "file-warning" "w-4 h-4 mr-1.5 text-red-500 dark:text-red-300" }}
261
-
<span class="font-mono">{{ .Filename }}</span>
262
-
</li>
263
-
{{ else if .Reason }}
264
-
<li class="flex items-center">
265
-
{{ i "file-warning" "w-4 h-4 mr-1.5 text-red-500 dark:text-red-300" }}
266
-
<span>{{.Reason}}</span>
267
-
</li>
268
-
{{ end }}
269
-
{{ end }}
270
-
</ul>
271
-
{{ end }}
272
-
</div>
273
-
</div>
274
-
{{ else if .MergeCheck }}
275
-
<div class="bg-green-50 dark:bg-green-900 border border-green-500 rounded drop-shadow-sm px-6 py-2 relative w-fit">
276
-
<div class="flex items-center gap-2 text-green-500 dark:text-green-300">
277
-
{{ i "circle-check-big" "w-4 h-4" }}
278
-
<span class="font-medium">no conflicts, ready to merge</span>
279
-
</div>
280
-
</div>
281
535
{{ end }}
282
536
{{ end }}
283
537
284
538
{{ define "resubmitStatus" }}
285
539
{{ if .ResubmitCheck.Yes }}
286
-
<div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded drop-shadow-sm px-6 py-2 relative w-fit">
540
+
<div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded drop-shadow-sm px-6 py-2 relative">
287
541
<div class="flex items-center gap-2 text-amber-500 dark:text-amber-300">
288
542
{{ i "triangle-alert" "w-4 h-4" }}
289
543
<span class="font-medium">this branch has been updated, consider resubmitting</span>
···
299
553
{{ with $pipeline }}
300
554
{{ $id := .Id }}
301
555
{{ if .Statuses }}
302
-
<div class="max-w-80 grid grid-cols-1 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700">
303
-
{{ range $name, $all := .Statuses }}
304
-
<a href="/{{ $root.RepoInfo.FullName }}/pipelines/{{ $id }}/workflow/{{ $name }}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25">
305
-
<div
306
-
class="flex gap-2 items-center justify-between p-2">
307
-
{{ $lastStatus := $all.Latest }}
308
-
{{ $kind := $lastStatus.Status.String }}
556
+
<details>
557
+
<summary class="cursor-pointer list-none">{{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" false) }}</summary>
558
+
<div class="my-2 grid grid-cols-1 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700">
559
+
{{ range $name, $all := .Statuses }}
560
+
<a href="/{{ $root.RepoInfo.FullName }}/pipelines/{{ $id }}/workflow/{{ $name }}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25">
561
+
<div
562
+
class="flex gap-2 items-center justify-between p-2">
563
+
{{ $lastStatus := $all.Latest }}
564
+
{{ $kind := $lastStatus.Status.String }}
309
565
310
-
<div id="left" class="flex items-center gap-2 flex-shrink-0">
311
-
{{ template "repo/pipelines/fragments/workflowSymbol" $all }}
312
-
{{ $name }}
313
-
</div>
314
-
<div id="right" class="flex items-center gap-2 flex-shrink-0">
315
-
<span class="font-bold">{{ $kind }}</span>
316
-
{{ if .TimeTaken }}
317
-
{{ template "repo/fragments/duration" .TimeTaken }}
318
-
{{ else }}
319
-
{{ template "repo/fragments/shortTimeAgo" $lastStatus.Created }}
320
-
{{ end }}
321
-
</div>
566
+
<div id="left" class="flex items-center gap-2 flex-shrink-0">
567
+
{{ template "repo/pipelines/fragments/workflowSymbol" $all }}
568
+
{{ $name }}
569
+
</div>
570
+
<div id="right" class="flex items-center gap-2 flex-shrink-0">
571
+
<span class="font-bold">{{ $kind }}</span>
572
+
{{ if .TimeTaken }}
573
+
{{ template "repo/fragments/duration" .TimeTaken }}
574
+
{{ else }}
575
+
{{ template "repo/fragments/shortTimeAgo" $lastStatus.Created }}
576
+
{{ end }}
577
+
</div>
578
+
</div>
579
+
</a>
580
+
{{ end }}
322
581
</div>
323
-
</a>
324
-
{{ end }}
325
-
</div>
582
+
</details>
326
583
{{ end }}
327
584
{{ end }}
328
585
{{ end }}
+1
-1
appview/pages/templates/repo/pulls/pulls.html
+1
-1
appview/pages/templates/repo/pulls/pulls.html
···
136
136
{{ $pipeline := index $.Pipelines .LatestSha }}
137
137
{{ if and $pipeline $pipeline.Id }}
138
138
<span class="before:content-['ยท']"></span>
139
-
{{ template "repo/pipelines/fragments/pipelineSymbol" $pipeline }}
139
+
{{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" true) }}
140
140
{{ end }}
141
141
142
142
{{ $state := .Labels }}
+1
-1
appview/pages/templates/repo/settings/pipelines.html
+1
-1
appview/pages/templates/repo/settings/pipelines.html
···
22
22
<p class="text-gray-500 dark:text-gray-400">
23
23
Choose a spindle to execute your workflows on. Only repository owners
24
24
can configure spindles. Spindles can be selfhosted,
25
-
<a class="text-gray-500 dark:text-gray-400 underline" href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">
25
+
<a class="text-gray-500 dark:text-gray-400 underline" href="https://docs.tangled.org/spindles.html#self-hosting-guide">
26
26
click to learn more.
27
27
</a>
28
28
</p>
+1
-1
appview/pages/templates/spindles/index.html
+1
-1
appview/pages/templates/spindles/index.html
···
102
102
{{ define "docsButton" }}
103
103
<a
104
104
class="btn flex items-center gap-2"
105
-
href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">
105
+
href="https://docs.tangled.org/spindles.html#self-hosting-guide">
106
106
{{ i "book" "size-4" }}
107
107
docs
108
108
</a>
+1
-1
appview/pulls/opengraph.go
+1
-1
appview/pulls/opengraph.go
···
242
242
dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2)
243
243
dollyY := statsY + iconBaselineOffset - dollySize/2 + 25
244
244
dollyColor := color.RGBA{180, 180, 180, 255} // light gray
245
-
err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor)
245
+
err = dollyArea.DrawDolly(dollyX, dollyY, dollySize, dollyColor)
246
246
if err != nil {
247
247
log.Printf("dolly silhouette not available (this is ok): %v", err)
248
248
}
+59
-38
appview/pulls/pulls.go
+59
-38
appview/pulls/pulls.go
···
232
232
defs[l.AtUri().String()] = &l
233
233
}
234
234
235
-
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
235
+
patch := pull.LatestSubmission().CombinedPatch()
236
+
diff := patchutil.AsNiceDiff(patch, pull.TargetBranch)
237
+
var diffOpts types.DiffOpts
238
+
if d := r.URL.Query().Get("diff"); d == "split" {
239
+
diffOpts.Split = true
240
+
}
241
+
242
+
log.Println(s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
236
243
LoggedInUser: user,
237
244
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
238
245
Pull: pull,
···
243
250
MergeCheck: mergeCheckResponse,
244
251
ResubmitCheck: resubmitResult,
245
252
Pipelines: m,
253
+
Diff: &diff,
254
+
DiffOpts: diffOpts,
246
255
247
256
OrderedReactionKinds: models.OrderedReactionKinds,
248
257
Reactions: reactionMap,
249
258
UserReacted: userReactions,
250
259
251
260
LabelDefs: defs,
252
-
})
261
+
}))
253
262
}
254
263
255
264
func (s *Pulls) mergeCheck(r *http.Request, f *models.Repo, pull *models.Pull, stack models.Stack) types.MergeCheckResponse {
···
1241
1250
return
1242
1251
}
1243
1252
1253
+
blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch))
1254
+
if err != nil {
1255
+
log.Println("failed to upload patch", err)
1256
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1257
+
return
1258
+
}
1259
+
1244
1260
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1245
1261
Collection: tangled.RepoPullNSID,
1246
1262
Repo: user.Did,
···
1252
1268
Repo: string(repo.RepoAt()),
1253
1269
Branch: targetBranch,
1254
1270
},
1255
-
Patch: patch,
1271
+
PatchBlob: blob.Blob,
1256
1272
Source: recordPullSource,
1257
1273
CreatedAt: time.Now().Format(time.RFC3339),
1258
1274
},
···
1328
1344
// apply all record creations at once
1329
1345
var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem
1330
1346
for _, p := range stack {
1347
+
blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(p.LatestPatch()))
1348
+
if err != nil {
1349
+
log.Println("failed to upload patch blob", err)
1350
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1351
+
return
1352
+
}
1353
+
1331
1354
record := p.AsRecord()
1332
-
write := comatproto.RepoApplyWrites_Input_Writes_Elem{
1355
+
record.PatchBlob = blob.Blob
1356
+
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1333
1357
RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{
1334
1358
Collection: tangled.RepoPullNSID,
1335
1359
Rkey: &p.Rkey,
···
1337
1361
Val: &record,
1338
1362
},
1339
1363
},
1340
-
}
1341
-
writes = append(writes, &write)
1364
+
})
1342
1365
}
1343
1366
_, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{
1344
1367
Repo: user.Did,
···
1871
1894
return
1872
1895
}
1873
1896
1874
-
var recordPullSource *tangled.RepoPull_Source
1875
-
if pull.IsBranchBased() {
1876
-
recordPullSource = &tangled.RepoPull_Source{
1877
-
Branch: pull.PullSource.Branch,
1878
-
Sha: sourceRev,
1879
-
}
1897
+
blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch))
1898
+
if err != nil {
1899
+
log.Println("failed to upload patch blob", err)
1900
+
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
1901
+
return
1880
1902
}
1881
-
if pull.IsForkBased() {
1882
-
repoAt := pull.PullSource.RepoAt.String()
1883
-
recordPullSource = &tangled.RepoPull_Source{
1884
-
Branch: pull.PullSource.Branch,
1885
-
Repo: &repoAt,
1886
-
Sha: sourceRev,
1887
-
}
1888
-
}
1903
+
record := pull.AsRecord()
1904
+
record.PatchBlob = blob.Blob
1905
+
record.CreatedAt = time.Now().Format(time.RFC3339)
1889
1906
1890
1907
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1891
1908
Collection: tangled.RepoPullNSID,
···
1893
1910
Rkey: pull.Rkey,
1894
1911
SwapRecord: ex.Cid,
1895
1912
Record: &lexutil.LexiconTypeDecoder{
1896
-
Val: &tangled.RepoPull{
1897
-
Title: pull.Title,
1898
-
Target: &tangled.RepoPull_Target{
1899
-
Repo: string(repo.RepoAt()),
1900
-
Branch: pull.TargetBranch,
1901
-
},
1902
-
Patch: patch, // new patch
1903
-
Source: recordPullSource,
1904
-
CreatedAt: time.Now().Format(time.RFC3339),
1905
-
},
1913
+
Val: &record,
1906
1914
},
1907
1915
})
1908
1916
if err != nil {
···
1988
1996
}
1989
1997
defer tx.Rollback()
1990
1998
1999
+
client, err := s.oauth.AuthorizedClient(r)
2000
+
if err != nil {
2001
+
log.Println("failed to authorize client")
2002
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
2003
+
return
2004
+
}
2005
+
1991
2006
// pds updates to make
1992
2007
var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem
1993
2008
···
2021
2036
return
2022
2037
}
2023
2038
2039
+
blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch))
2040
+
if err != nil {
2041
+
log.Println("failed to upload patch blob", err)
2042
+
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
2043
+
return
2044
+
}
2024
2045
record := p.AsRecord()
2046
+
record.PatchBlob = blob.Blob
2025
2047
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
2026
2048
RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{
2027
2049
Collection: tangled.RepoPullNSID,
···
2056
2078
return
2057
2079
}
2058
2080
2081
+
blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch))
2082
+
if err != nil {
2083
+
log.Println("failed to upload patch blob", err)
2084
+
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
2085
+
return
2086
+
}
2059
2087
record := np.AsRecord()
2060
-
2088
+
record.PatchBlob = blob.Blob
2061
2089
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
2062
2090
RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{
2063
2091
Collection: tangled.RepoPullNSID,
···
2091
2119
if err != nil {
2092
2120
log.Println("failed to resubmit pull", err)
2093
2121
s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
2094
-
return
2095
-
}
2096
-
2097
-
client, err := s.oauth.AuthorizedClient(r)
2098
-
if err != nil {
2099
-
log.Println("failed to authorize client")
2100
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
2101
2122
return
2102
2123
}
2103
2124
+1
appview/repo/archive.go
+1
appview/repo/archive.go
···
18
18
l := rp.logger.With("handler", "DownloadArchive")
19
19
ref := chi.URLParam(r, "ref")
20
20
ref, _ = url.PathUnescape(ref)
21
+
ref = strings.TrimSuffix(ref, ".tar.gz")
21
22
f, err := rp.repoResolver.Resolve(r)
22
23
if err != nil {
23
24
l.Error("failed to get repo and knot", "err", err)
+1
-1
appview/repo/opengraph.go
+1
-1
appview/repo/opengraph.go
···
237
237
dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2)
238
238
dollyY := statsY + iconBaselineOffset - dollySize/2 + 25
239
239
dollyColor := color.RGBA{180, 180, 180, 255} // light gray
240
-
err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor)
240
+
err = dollyArea.DrawDolly(dollyX, dollyY, dollySize, dollyColor)
241
241
if err != nil {
242
242
log.Printf("dolly silhouette not available (this is ok): %v", err)
243
243
}
+26
-1
appview/reporesolver/resolver.go
+26
-1
appview/reporesolver/resolver.go
···
63
63
}
64
64
65
65
// get dir/ref
66
-
currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath()))
66
+
currentDir := extractCurrentDir(r.URL.EscapedPath())
67
67
ref := chi.URLParam(r, "ref")
68
68
69
69
repoAt := repo.RepoAt()
···
130
130
}
131
131
132
132
return repoInfo
133
+
}
134
+
135
+
// extractCurrentDir gets the current directory for markdown link resolution.
136
+
// for blob paths, returns the parent dir. for tree paths, returns the path itself.
137
+
//
138
+
// /@user/repo/blob/main/docs/README.md => docs
139
+
// /@user/repo/tree/main/docs => docs
140
+
func extractCurrentDir(fullPath string) string {
141
+
fullPath = strings.TrimPrefix(fullPath, "/")
142
+
143
+
blobPattern := regexp.MustCompile(`blob/[^/]+/(.*)$`)
144
+
if matches := blobPattern.FindStringSubmatch(fullPath); len(matches) > 1 {
145
+
return path.Dir(matches[1])
146
+
}
147
+
148
+
treePattern := regexp.MustCompile(`tree/[^/]+/(.*)$`)
149
+
if matches := treePattern.FindStringSubmatch(fullPath); len(matches) > 1 {
150
+
dir := strings.TrimSuffix(matches[1], "/")
151
+
if dir == "" {
152
+
return "."
153
+
}
154
+
return dir
155
+
}
156
+
157
+
return "."
133
158
}
134
159
135
160
// extractPathAfterRef gets the actual repository path
+22
appview/reporesolver/resolver_test.go
+22
appview/reporesolver/resolver_test.go
···
1
+
package reporesolver
2
+
3
+
import "testing"
4
+
5
+
func TestExtractCurrentDir(t *testing.T) {
6
+
tests := []struct {
7
+
path string
8
+
want string
9
+
}{
10
+
{"/@user/repo/blob/main/docs/README.md", "docs"},
11
+
{"/@user/repo/blob/main/README.md", "."},
12
+
{"/@user/repo/tree/main/docs", "docs"},
13
+
{"/@user/repo/tree/main/docs/", "docs"},
14
+
{"/@user/repo/tree/main", "."},
15
+
}
16
+
17
+
for _, tt := range tests {
18
+
if got := extractCurrentDir(tt.path); got != tt.want {
19
+
t.Errorf("extractCurrentDir(%q) = %q, want %q", tt.path, got, tt.want)
20
+
}
21
+
}
22
+
}
-5
appview/spindles/spindles.go
-5
appview/spindles/spindles.go
···
653
653
s.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
654
654
return
655
655
}
656
-
if memberId.Handle.IsInvalidHandle() {
657
-
l.Error("failed to resolve member identity to handle")
658
-
s.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
659
-
return
660
-
}
661
656
662
657
tx, err := s.Db.Begin()
663
658
if err != nil {
+29
appview/state/manifest.go
+29
appview/state/manifest.go
···
1
+
package state
2
+
3
+
import (
4
+
"encoding/json"
5
+
"net/http"
6
+
)
7
+
8
+
// https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest
9
+
// https://www.w3.org/TR/appmanifest/
10
+
var manifestData = map[string]any{
11
+
"name": "tangled",
12
+
"description": "tightly-knit social coding.",
13
+
"icons": []map[string]string{
14
+
{
15
+
"src": "/static/logos/dolly.svg",
16
+
"sizes": "144x144",
17
+
},
18
+
},
19
+
"start_url": "/",
20
+
"id": "https://tangled.org",
21
+
"display": "standalone",
22
+
"background_color": "#111827",
23
+
"theme_color": "#111827",
24
+
}
25
+
26
+
func (p *State) WebAppManifest(w http.ResponseWriter, r *http.Request) {
27
+
w.Header().Set("Content-Type", "application/manifest+json")
28
+
json.NewEncoder(w).Encode(manifestData)
29
+
}
+6
-4
appview/state/profile.go
+6
-4
appview/state/profile.go
···
163
163
}
164
164
165
165
// populate commit counts in the timeline, using the punchcard
166
-
currentMonth := time.Now().Month()
166
+
now := time.Now()
167
167
for _, p := range profile.Punchcard.Punches {
168
-
idx := currentMonth - p.Date.Month()
169
-
if int(idx) < len(timeline.ByMonth) {
170
-
timeline.ByMonth[idx].Commits += p.Count
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
171
173
}
172
174
}
173
175
+3
-3
appview/state/router.go
+3
-3
appview/state/router.go
···
32
32
s.pages,
33
33
)
34
34
35
-
router.Get("/favicon.svg", s.Favicon)
36
-
router.Get("/favicon.ico", s.Favicon)
37
-
router.Get("/pwa-manifest.json", s.PWAManifest)
35
+
router.Get("/pwa-manifest.json", s.WebAppManifest)
38
36
router.Get("/robots.txt", s.RobotsTxt)
39
37
40
38
userRouter := s.UserRouter(&middleware)
···
109
107
})
110
108
111
109
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
110
+
w.WriteHeader(http.StatusNotFound)
112
111
s.pages.Error404(w)
113
112
})
114
113
···
182
181
r.Get("/brand", s.Brand)
183
182
184
183
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
184
+
w.WriteHeader(http.StatusNotFound)
185
185
s.pages.Error404(w)
186
186
})
187
187
return r
-36
appview/state/state.go
-36
appview/state/state.go
···
202
202
return s.db.Close()
203
203
}
204
204
205
-
func (s *State) Favicon(w http.ResponseWriter, r *http.Request) {
206
-
w.Header().Set("Content-Type", "image/svg+xml")
207
-
w.Header().Set("Cache-Control", "public, max-age=31536000") // one year
208
-
w.Header().Set("ETag", `"favicon-svg-v1"`)
209
-
210
-
if match := r.Header.Get("If-None-Match"); match == `"favicon-svg-v1"` {
211
-
w.WriteHeader(http.StatusNotModified)
212
-
return
213
-
}
214
-
215
-
s.pages.Favicon(w)
216
-
}
217
-
218
205
func (s *State) RobotsTxt(w http.ResponseWriter, r *http.Request) {
219
206
w.Header().Set("Content-Type", "text/plain")
220
207
w.Header().Set("Cache-Control", "public, max-age=86400") // one day
···
223
210
Allow: /
224
211
`
225
212
w.Write([]byte(robotsTxt))
226
-
}
227
-
228
-
// https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest
229
-
const manifestJson = `{
230
-
"name": "tangled",
231
-
"description": "tightly-knit social coding.",
232
-
"icons": [
233
-
{
234
-
"src": "/favicon.svg",
235
-
"sizes": "144x144"
236
-
}
237
-
],
238
-
"start_url": "/",
239
-
"id": "org.tangled",
240
-
241
-
"display": "standalone",
242
-
"background_color": "#111827",
243
-
"theme_color": "#111827"
244
-
}`
245
-
246
-
func (p *State) PWAManifest(w http.ResponseWriter, r *http.Request) {
247
-
w.Header().Set("Content-Type", "application/json")
248
-
w.Write([]byte(manifestJson))
249
213
}
250
214
251
215
func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) {
+182
cmd/dolly/main.go
+182
cmd/dolly/main.go
···
1
+
package main
2
+
3
+
import (
4
+
"bytes"
5
+
"flag"
6
+
"fmt"
7
+
"image"
8
+
"image/color"
9
+
"image/png"
10
+
"os"
11
+
"path/filepath"
12
+
"strconv"
13
+
"strings"
14
+
"text/template"
15
+
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 {
37
+
fmt.Fprintf(os.Stderr, "Error parsing size: %v\n", err)
38
+
os.Exit(1)
39
+
}
40
+
41
+
// Detect format from file extension
42
+
ext := strings.ToLower(filepath.Ext(output))
43
+
format := strings.TrimPrefix(ext, ".")
44
+
45
+
if format != "svg" && format != "png" && format != "ico" {
46
+
fmt.Fprintf(os.Stderr, "Invalid file extension: %s. Must be .svg, .png, or .ico\n", ext)
47
+
os.Exit(1)
48
+
}
49
+
50
+
if fillColor != "currentColor" && !isValidHexColor(fillColor) {
51
+
fmt.Fprintf(os.Stderr, "Invalid color format: %s. Use hex format like #FF5733\n", fillColor)
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)
59
+
}
60
+
61
+
// Create output directory if it doesn't exist
62
+
dir := filepath.Dir(output)
63
+
if dir != "" && dir != "." {
64
+
if err := os.MkdirAll(dir, 0755); err != nil {
65
+
fmt.Fprintf(os.Stderr, "Error creating output directory: %v\n", err)
66
+
os.Exit(1)
67
+
}
68
+
}
69
+
70
+
switch format {
71
+
case "svg":
72
+
err = saveSVG(svgData, output, width, height)
73
+
case "png":
74
+
err = savePNG(svgData, output, width, height)
75
+
case "ico":
76
+
err = saveICO(svgData, output, width, height)
77
+
}
78
+
79
+
if err != nil {
80
+
fmt.Fprintf(os.Stderr, "Error saving file: %v\n", err)
81
+
os.Exit(1)
82
+
}
83
+
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
+
}
100
+
101
+
return svgData.Bytes(), nil
102
+
}
103
+
104
+
func svgToImage(svgData []byte, w, h int) (image.Image, error) {
105
+
icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData))
106
+
if err != nil {
107
+
return nil, fmt.Errorf("error parsing SVG: %v", err)
108
+
}
109
+
110
+
icon.SetTarget(0, 0, float64(w), float64(h))
111
+
rgba := image.NewRGBA(image.Rect(0, 0, w, h))
112
+
draw.Draw(rgba, rgba.Bounds(), &image.Uniform{color.Transparent}, image.Point{}, draw.Src)
113
+
scanner := rasterx.NewScannerGV(w, h, rgba, rgba.Bounds())
114
+
raster := rasterx.NewDasher(w, h, scanner)
115
+
icon.Draw(raster, 1.0)
116
+
117
+
return rgba, nil
118
+
}
119
+
120
+
func parseSize(size string) (int, int, error) {
121
+
parts := strings.Split(size, "x")
122
+
if len(parts) != 2 {
123
+
return 0, 0, fmt.Errorf("invalid size format, use WIDTHxHEIGHT")
124
+
}
125
+
126
+
width, err := strconv.Atoi(parts[0])
127
+
if err != nil {
128
+
return 0, 0, fmt.Errorf("invalid width: %v", err)
129
+
}
130
+
131
+
height, err := strconv.Atoi(parts[1])
132
+
if err != nil {
133
+
return 0, 0, fmt.Errorf("invalid height: %v", err)
134
+
}
135
+
136
+
if width <= 0 || height <= 0 {
137
+
return 0, 0, fmt.Errorf("width and height must be positive")
138
+
}
139
+
140
+
return width, height, nil
141
+
}
142
+
143
+
func isValidHexColor(hex string) bool {
144
+
if len(hex) != 7 || hex[0] != '#' {
145
+
return false
146
+
}
147
+
_, err := strconv.ParseUint(hex[1:], 16, 32)
148
+
return err == nil
149
+
}
150
+
151
+
func saveSVG(svgData []byte, filepath string, _, _ int) error {
152
+
return os.WriteFile(filepath, svgData, 0644)
153
+
}
154
+
155
+
func savePNG(svgData []byte, filepath string, width, height int) error {
156
+
img, err := svgToImage(svgData, width, height)
157
+
if err != nil {
158
+
return err
159
+
}
160
+
161
+
f, err := os.Create(filepath)
162
+
if err != nil {
163
+
return err
164
+
}
165
+
defer f.Close()
166
+
167
+
return png.Encode(f, img)
168
+
}
169
+
170
+
func saveICO(svgData []byte, filepath string, width, height int) error {
171
+
img, err := svgToImage(svgData, width, height)
172
+
if err != nil {
173
+
return err
174
+
}
175
+
176
+
icoData, err := ico.ImageToIco(img)
177
+
if err != nil {
178
+
return err
179
+
}
180
+
181
+
return os.WriteFile(filepath, icoData, 0644)
182
+
}
+86
-89
docs/DOCS.md
+86
-89
docs/DOCS.md
···
1
1
---
2
-
title: Tangled Documentation
2
+
title: Tangled docs
3
3
author: The Tangled Contributors
4
4
date: 21 Sun, Dec 2025
5
-
---
6
-
7
-
# Introduction
8
-
9
-
Tangled is a decentralized code hosting and collaboration
10
-
platform. Every component of Tangled is open-source and
11
-
selfhostable. [tangled.org](https://tangled.org) also
12
-
provides hosting and CI services that are free to use.
5
+
abstract: |
6
+
Tangled is a decentralized code hosting and collaboration
7
+
platform. Every component of Tangled is open-source and
8
+
self-hostable. [tangled.org](https://tangled.org) also
9
+
provides hosting and CI services that are free to use.
13
10
14
-
There are several models for decentralized code
15
-
collaboration platforms, ranging from ActivityPubโs
16
-
(Forgejo) federated model, to Radicleโs entirely P2P model.
17
-
Our approach attempts to be the best of both worlds by
18
-
adopting atprotoโa protocol for building decentralized
19
-
social applications with a central identity
11
+
There are several models for decentralized code
12
+
collaboration platforms, ranging from ActivityPubโs
13
+
(Forgejo) federated model, to Radicleโs entirely P2P model.
14
+
Our approach attempts to be the best of both worlds by
15
+
adopting the AT Protocolโa protocol for building decentralized
16
+
social applications with a central identity
20
17
21
-
Our approach to this is the idea of โknotsโ. Knots are
22
-
lightweight, headless servers that enable users to host Git
23
-
repositories with ease. Knots are designed for either single
24
-
or multi-tenant use which is perfect for self-hosting on a
25
-
Raspberry Pi at home, or larger โcommunityโ servers. By
26
-
default, Tangled provides managed knots where you can host
27
-
your repositories for free.
18
+
Our approach to this is the idea of โknotsโ. Knots are
19
+
lightweight, headless servers that enable users to host Git
20
+
repositories with ease. Knots are designed for either single
21
+
or multi-tenant use which is perfect for self-hosting on a
22
+
Raspberry Pi at home, or larger โcommunityโ servers. By
23
+
default, Tangled provides managed knots where you can host
24
+
your repositories for free.
28
25
29
-
The "appview" at tangled.org acts as a consolidated โviewโ
30
-
into the whole network, allowing users to access, clone and
31
-
contribute to repositories hosted across different knots
32
-
seamlessly.
26
+
The appview at tangled.org acts as a consolidated "view"
27
+
into the whole network, allowing users to access, clone and
28
+
contribute to repositories hosted across different knots
29
+
seamlessly.
30
+
---
33
31
34
-
# Quick Start Guide
32
+
# Quick start guide
35
33
36
-
## Login or Sign up
34
+
## Login or sign up
37
35
38
-
You can [login](https://tangled.org) by using your AT
36
+
You can [login](https://tangled.org) by using your AT Protocol
39
37
account. If you are unclear on what that means, simply head
40
38
to the [signup](https://tangled.org/signup) page and create
41
39
an account. By doing so, you will be choosing Tangled as
42
40
your account provider (you will be granted a handle of the
43
41
form `user.tngl.sh`).
44
42
45
-
In the AT network, users are free to choose their account
43
+
In the AT Protocol network, users are free to choose their account
46
44
provider (known as a "Personal Data Service", or PDS), and
47
45
login to applications that support AT accounts.
48
46
49
-
You can think of it as "one account for all of the
50
-
atmosphere"!
47
+
You can think of it as "one account for all of the atmosphere"!
51
48
52
49
If you already have an AT account (you may have one if you
53
50
signed up to Bluesky, for example), you can login with the
54
51
same handle on Tangled (so just use `user.bsky.social` on
55
52
the login page).
56
53
57
-
## Add an SSH Key
54
+
## Add an SSH key
58
55
59
56
Once you are logged in, you can start creating repositories
60
57
and pushing code. Tangled supports pushing git repositories
···
87
84
paste your public key, give it a descriptive name, and hit
88
85
save.
89
86
90
-
## Create a Repository
87
+
## Create a repository
91
88
92
89
Once your SSH key is added, create your first repository:
93
90
···
98
95
4. Choose a knotserver to host this repository on
99
96
5. Hit create
100
97
101
-
"Knots" are selfhostable, lightweight git servers that can
98
+
Knots are self-hostable, lightweight Git servers that can
102
99
host your repository. Unlike traditional code forges, your
103
100
code can live on any server. Read the [Knots](TODO) section
104
101
for more.
···
125
122
are hosted by tangled.org. If you use a custom knot, refer
126
123
to the [Knots](TODO) section.
127
124
128
-
## Push Your First Repository
125
+
## Push your first repository
129
126
130
-
Initialize a new git repository:
127
+
Initialize a new Git repository:
131
128
132
129
```bash
133
130
mkdir my-project
···
165
162
cd /path/to/your/existing/repo
166
163
```
167
164
168
-
You can inspect your existing git remote like so:
165
+
You can inspect your existing Git remote like so:
169
166
170
167
```bash
171
168
git remote -v
···
197
194
origin git@tangled.org:user.tngl.sh/my-project (push)
198
195
```
199
196
200
-
Push all your branches and tags to tangled:
197
+
Push all your branches and tags to Tangled:
201
198
202
199
```bash
203
200
git push -u origin --all
···
232
229
```
233
230
234
231
You also need to re-add the original URL as a push
235
-
destination (git replaces the push URL when you use `--add`
232
+
destination (Git replaces the push URL when you use `--add`
236
233
the first time):
237
234
238
235
```bash
···
249
246
```
250
247
251
248
Notice that there's one fetch URL (the primary remote) and
252
-
two push URLs. Now, whenever you push, git will
249
+
two push URLs. Now, whenever you push, Git will
253
250
automatically push to both remotes:
254
251
255
252
```bash
···
301
298
## Docker
302
299
303
300
Refer to
304
-
[@tangled.org/knot-docker](https://tangled.sh/@tangled.sh/knot-docker).
301
+
[@tangled.org/knot-docker](https://tangled.org/@tangled.org/knot-docker).
305
302
Note that this is community maintained.
306
303
307
304
## Manual setup
···
372
369
```
373
370
KNOT_REPO_SCAN_PATH=/home/git
374
371
KNOT_SERVER_HOSTNAME=knot.example.com
375
-
APPVIEW_ENDPOINT=https://tangled.sh
372
+
APPVIEW_ENDPOINT=https://tangled.org
376
373
KNOT_SERVER_OWNER=did:plc:foobar
377
374
KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:5444
378
375
KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555
···
603
600
- `nixery`: This uses an instance of
604
601
[Nixery](https://nixery.dev) to run steps, which allows
605
602
you to add [dependencies](#dependencies) from
606
-
[Nixpkgs](https://github.com/NixOS/nixpkgs). You can
603
+
Nixpkgs (https://github.com/NixOS/nixpkgs). You can
607
604
search for packages on https://search.nixos.org, and
608
605
there's a pretty good chance the package(s) you're looking
609
606
for will be there.
···
630
627
default, the depth is set to 1, meaning only the most
631
628
recent commit will be fetched, which is the commit that
632
629
triggered the workflow.
633
-
- `submodules`: If you use [git
634
-
submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules)
630
+
- `submodules`: If you use Git submodules
631
+
(https://git-scm.com/book/en/v2/Git-Tools-Submodules)
635
632
in your repository, setting this field to `true` will
636
633
recursively fetch all submodules. This is `false` by
637
634
default.
···
657
654
Say you want to fetch Node.js and Go from `nixpkgs`, and a
658
655
package called `my_pkg` you've made from your own registry
659
656
at your repository at
660
-
`https://tangled.sh/@example.com/my_pkg`. You can define
657
+
`https://tangled.org/@example.com/my_pkg`. You can define
661
658
those dependencies like so:
662
659
663
660
```yaml
···
779
776
780
777
If you want another example of a workflow, you can look at
781
778
the one [Tangled uses to build the
782
-
project](https://tangled.sh/@tangled.sh/core/blob/master/.tangled/workflows/build.yml).
779
+
project](https://tangled.org/@tangled.org/core/blob/master/.tangled/workflows/build.yml).
783
780
784
781
## Self-hosting guide
785
782
···
836
833
837
834
## Architecture
838
835
839
-
Spindle is a small CI runner service. Here's a high level overview of how it operates:
836
+
Spindle is a small CI runner service. Here's a high-level overview of how it operates:
840
837
841
-
* listens for [`sh.tangled.spindle.member`](/lexicons/spindle/member.json) and
838
+
* Listens for [`sh.tangled.spindle.member`](/lexicons/spindle/member.json) and
842
839
[`sh.tangled.repo`](/lexicons/repo.json) records on the Jetstream.
843
-
* when a new repo record comes through (typically when you add a spindle to a
840
+
* When a new repo record comes through (typically when you add a spindle to a
844
841
repo from the settings), spindle then resolves the underlying knot and
845
842
subscribes to repo events (see:
846
843
[`sh.tangled.pipeline`](/lexicons/pipeline.json)).
847
-
* the spindle engine then handles execution of the pipeline, with results and
848
-
logs beamed on the spindle event stream over wss
844
+
* The spindle engine then handles execution of the pipeline, with results and
845
+
logs beamed on the spindle event stream over WebSocket
849
846
850
847
### The engine
851
848
852
849
At present, the only supported backend is Docker (and Podman, if Docker
853
-
compatibility is enabled, so that `/run/docker.sock` is created). Spindle
850
+
compatibility is enabled, so that `/run/docker.sock` is created). spindle
854
851
executes each step in the pipeline in a fresh container, with state persisted
855
852
across steps within the `/tangled/workspace` directory.
856
853
···
858
855
[Nixery](https://nixery.dev), which is handy for caching layers for frequently
859
856
used packages.
860
857
861
-
The pipeline manifest is [specified here](/docs/spindle/pipeline.md).
858
+
The pipeline manifest is [specified here](https://docs.tangled.org/spindles.html#pipelines).
862
859
863
860
## Secrets with openbao
864
861
865
-
This document covers setting up Spindle to use OpenBao for secrets
862
+
This document covers setting up spindle to use OpenBao for secrets
866
863
management via OpenBao Proxy instead of the default SQLite backend.
867
864
868
865
### Overview
869
866
870
867
Spindle now uses OpenBao Proxy for secrets management. The proxy handles
871
-
authentication automatically using AppRole credentials, while Spindle
868
+
authentication automatically using AppRole credentials, while spindle
872
869
connects to the local proxy instead of directly to the OpenBao server.
873
870
874
871
This approach provides better security, automatic token renewal, and
···
876
873
877
874
### Installation
878
875
879
-
Install OpenBao from nixpkgs:
876
+
Install OpenBao from Nixpkgs:
880
877
881
878
```bash
882
879
nix shell nixpkgs#openbao # for a local server
···
1029
1026
}
1030
1027
}
1031
1028
1032
-
# Proxy listener for Spindle
1029
+
# Proxy listener for spindle
1033
1030
listener "tcp" {
1034
1031
address = "127.0.0.1:8201"
1035
1032
tls_disable = true
···
1062
1059
1063
1060
#### Configure spindle
1064
1061
1065
-
Set these environment variables for Spindle:
1062
+
Set these environment variables for spindle:
1066
1063
1067
1064
```bash
1068
1065
export SPINDLE_SERVER_SECRETS_PROVIDER=openbao
···
1070
1067
export SPINDLE_SERVER_SECRETS_OPENBAO_MOUNT=spindle
1071
1068
```
1072
1069
1073
-
On startup, the spindle will now connect to the local proxy,
1070
+
On startup, spindle will now connect to the local proxy,
1074
1071
which handles all authentication automatically.
1075
1072
1076
1073
### Production setup for proxy
···
1099
1096
# List all secrets
1100
1097
bao kv list spindle/
1101
1098
1102
-
# Add a test secret via Spindle API, then check it exists
1099
+
# Add a test secret via the spindle API, then check it exists
1103
1100
bao kv list spindle/repos/
1104
1101
1105
1102
# Get a specific secret
···
1112
1109
port 8200 or 8201)
1113
1110
- The proxy authenticates with OpenBao using AppRole
1114
1111
credentials
1115
-
- All Spindle requests go through the proxy, which injects
1112
+
- All spindle requests go through the proxy, which injects
1116
1113
authentication tokens
1117
1114
- Secrets are stored at
1118
1115
`spindle/repos/{sanitized_repo_path}/{secret_key}`
···
1131
1128
and the policy has the necessary permissions.
1132
1129
1133
1130
**404 route errors**: The spindle KV mount probably doesn't
1134
-
exist - run the mount creation step again.
1131
+
existโrun the mount creation step again.
1135
1132
1136
1133
**Proxy authentication failures**: Check the proxy logs and
1137
1134
verify the role-id and secret-id files are readable and
···
1159
1156
secret_id="$(cat /tmp/openbao/secret-id)"
1160
1157
```
1161
1158
1162
-
# Migrating knots & spindles
1159
+
# Migrating knots and spindles
1163
1160
1164
1161
Sometimes, non-backwards compatible changes are made to the
1165
1162
knot/spindle XRPC APIs. If you host a knot or a spindle, you
···
1172
1169
1173
1170
## Upgrading from v1.8.x
1174
1171
1175
-
After v1.8.2, the HTTP API for knot and spindles have been
1172
+
After v1.8.2, the HTTP API for knots and spindles has been
1176
1173
deprecated and replaced with XRPC. Repositories on outdated
1177
1174
knots will not be viewable from the appview. Upgrading is
1178
1175
straightforward however.
1179
1176
1180
1177
For knots:
1181
1178
1182
-
- Upgrade to latest tag (v1.9.0 or above)
1179
+
- Upgrade to the latest tag (v1.9.0 or above)
1183
1180
- Head to the [knot dashboard](https://tangled.org/settings/knots) and
1184
1181
hit the "retry" button to verify your knot
1185
1182
1186
1183
For spindles:
1187
1184
1188
-
- Upgrade to latest tag (v1.9.0 or above)
1185
+
- Upgrade to the latest tag (v1.9.0 or above)
1189
1186
- Head to the [spindle
1190
1187
dashboard](https://tangled.org/settings/spindles) and hit the
1191
1188
"retry" button to verify your spindle
···
1227
1224
# Hacking on Tangled
1228
1225
1229
1226
We highly recommend [installing
1230
-
nix](https://nixos.org/download/) (the package manager)
1231
-
before working on the codebase. The nix flake provides a lot
1227
+
Nix](https://nixos.org/download/) (the package manager)
1228
+
before working on the codebase. The Nix flake provides a lot
1232
1229
of helpers to get started and most importantly, builds and
1233
1230
dev shells are entirely deterministic.
1234
1231
···
1238
1235
nix develop
1239
1236
```
1240
1237
1241
-
Non-nix users can look at the `devShell` attribute in the
1238
+
Non-Nix users can look at the `devShell` attribute in the
1242
1239
`flake.nix` file to determine necessary dependencies.
1243
1240
1244
1241
## Running the appview
1245
1242
1246
-
The nix flake also exposes a few `app` attributes (run `nix
1243
+
The Nix flake also exposes a few `app` attributes (run `nix
1247
1244
flake show` to see a full list of what the flake provides),
1248
1245
one of the apps runs the appview with the `air`
1249
1246
live-reloader:
···
1258
1255
nix run .#watch-tailwind
1259
1256
```
1260
1257
1261
-
To authenticate with the appview, you will need redis and
1262
-
OAUTH JWKs to be setup:
1258
+
To authenticate with the appview, you will need Redis and
1259
+
OAuth JWKs to be set up:
1263
1260
1264
1261
```
1265
-
# oauth jwks should already be setup by the nix devshell:
1262
+
# OAuth JWKs should already be set up by the Nix devshell:
1266
1263
echo $TANGLED_OAUTH_CLIENT_SECRET
1267
1264
z42ty4RT1ovnTopY8B8ekz9NuziF2CuMkZ7rbRFpAR9jBqMc
1268
1265
···
1280
1277
# the secret key from above
1281
1278
export TANGLED_OAUTH_CLIENT_SECRET="z42tuP..."
1282
1279
1283
-
# run redis in at a new shell to store oauth sessions
1280
+
# Run Redis in a new shell to store OAuth sessions
1284
1281
redis-server
1285
1282
```
1286
1283
1287
1284
## Running knots and spindles
1288
1285
1289
1286
An end-to-end knot setup requires setting up a machine with
1290
-
`sshd`, `AuthorizedKeysCommand`, and git user, which is
1291
-
quite cumbersome. So the nix flake provides a
1287
+
`sshd`, `AuthorizedKeysCommand`, and a Git user, which is
1288
+
quite cumbersome. So the Nix flake provides a
1292
1289
`nixosConfiguration` to do so.
1293
1290
1294
1291
<details>
1295
-
<summary><strong>MacOS users will have to setup a Nix Builder first</strong></summary>
1292
+
<summary><strong>macOS users will have to set up a Nix Builder first</strong></summary>
1296
1293
1297
1294
In order to build Tangled's dev VM on macOS, you will
1298
1295
first need to set up a Linux Nix builder. The recommended
···
1303
1300
you are using Apple Silicon).
1304
1301
1305
1302
> IMPORTANT: You must build `darwin.linux-builder` somewhere other than inside
1306
-
> the tangled repo so that it doesn't conflict with the other VM. For example,
1303
+
> the Tangled repo so that it doesn't conflict with the other VM. For example,
1307
1304
> you can do
1308
1305
>
1309
1306
> ```shell
···
1316
1313
> avoid subtle problems.
1317
1314
1318
1315
Alternatively, you can use any other method to set up a
1319
-
Linux machine with `nix` installed that you can `sudo ssh`
1316
+
Linux machine with Nix installed that you can `sudo ssh`
1320
1317
into (in other words, root user on your Mac has to be able
1321
1318
to ssh into the Linux machine without entering a password)
1322
1319
and that has the same architecture as your Mac. See
···
1347
1344
with `ssh` exposed on port 2222.
1348
1345
1349
1346
Once the services are running, head to
1350
-
http://localhost:3000/settings/knots and hit verify. It should
1347
+
http://localhost:3000/settings/knots and hit "Verify". It should
1351
1348
verify the ownership of the services instantly if everything
1352
1349
went smoothly.
1353
1350
···
1371
1368
1372
1369
The above VM should already be running a spindle on
1373
1370
`localhost:6555`. Head to http://localhost:3000/settings/spindles and
1374
-
hit verify. You can then configure each repository to use
1371
+
hit "Verify". You can then configure each repository to use
1375
1372
this spindle and run CI jobs.
1376
1373
1377
1374
Of interest when debugging spindles:
1378
1375
1379
1376
```
1380
-
# service logs from journald:
1377
+
# Service logs from journald:
1381
1378
journalctl -xeu spindle
1382
1379
1383
1380
# CI job logs from disk:
1384
1381
ls /var/log/spindle
1385
1382
1386
-
# debugging spindle db:
1383
+
# Debugging spindle database:
1387
1384
sqlite3 /var/lib/spindle/spindle.db
1388
1385
1389
1386
# litecli has a nicer REPL interface:
···
1432
1429
1433
1430
### General notes
1434
1431
1435
-
- PRs get merged "as-is" (fast-forward) -- like applying a patch-series
1436
-
using `git am`. At present, there is no squashing -- so please author
1432
+
- PRs get merged "as-is" (fast-forward)โlike applying a patch-series
1433
+
using `git am`. At present, there is no squashingโso please author
1437
1434
your commits as they would appear on `master`, following the above
1438
1435
guidelines.
1439
1436
- If there is a lot of nesting, for example "appview:
···
1454
1451
## Code formatting
1455
1452
1456
1453
We use a variety of tools to format our code, and multiplex them with
1457
-
[`treefmt`](https://treefmt.com): all you need to do to format your changes
1454
+
[`treefmt`](https://treefmt.com). All you need to do to format your changes
1458
1455
is run `nix run .#fmt` (or just `treefmt` if you're in the devshell).
1459
1456
1460
1457
## Proposals for bigger changes
···
1482
1479
We'll use the issue thread to discuss and refine the idea before moving
1483
1480
forward.
1484
1481
1485
-
## Developer certificate of origin (DCO)
1482
+
## Developer Certificate of Origin (DCO)
1486
1483
1487
1484
We require all contributors to certify that they have the right to
1488
1485
submit the code they're contributing. To do this, we follow the
+6
docs/logo.html
+6
docs/logo.html
+3
docs/mode.html
+3
docs/mode.html
+7
docs/search.html
+7
docs/search.html
···
1
+
<form action="https://google.com/search" role="search" aria-label="Sitewide" class="w-full">
2
+
<input type="hidden" name="q" value="+[inurl:https://docs.tangled.org]">
3
+
<label>
4
+
<span style="display:none;">Search</span>
5
+
<input type="text" name="q" placeholder="Search docs ..." class="w-full font-normal">
6
+
</label>
7
+
</form>
+77
-36
docs/template.html
+77
-36
docs/template.html
···
20
20
<meta name="description" content="$description-meta$" />
21
21
$endif$
22
22
23
-
<title>$pagetitle$ - Tangled docs</title>
23
+
<title>$pagetitle$</title>
24
24
25
25
<style>
26
26
$styles.css()$
···
37
37
<link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin />
38
38
39
39
</head>
40
-
<body class="bg-white dark:bg-gray-900 min-h-screen flex flex-col min-h-screen">
40
+
<body class="bg-white dark:bg-gray-900 flex flex-col min-h-svh">
41
41
$for(include-before)$
42
42
$include-before$
43
43
$endfor$
44
44
45
45
$if(toc)$
46
-
<!-- mobile topbar toc -->
47
-
<details id="mobile-$idprefix$TOC" role="doc-toc" class="md:hidden bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 z-50 space-y-4 group px-6 py-4">
48
-
<summary class="cursor-pointer list-none text-sm font-semibold select-none flex gap-2 justify-between items-center dark:text-white">
46
+
<!-- mobile TOC trigger -->
47
+
<div class="md:hidden px-6 py-4 border-b border-gray-200 dark:border-gray-700">
48
+
<button
49
+
type="button"
50
+
popovertarget="mobile-toc-popover"
51
+
popovertargetaction="toggle"
52
+
class="w-full flex gap-2 items-center text-sm font-semibold dark:text-white"
53
+
>
54
+
${ menu.svg() }
49
55
$if(toc-title)$$toc-title$$else$Table of Contents$endif$
50
-
<span class="group-open:hidden inline">${ menu.svg() }</span>
51
-
<span class="hidden group-open:inline">${ x.svg() }</span>
52
-
</summary>
53
-
${ table-of-contents:toc.html() }
54
-
</details>
56
+
</button>
57
+
</div>
58
+
59
+
<div
60
+
id="mobile-toc-popover"
61
+
popover
62
+
class="mobile-toc-popover
63
+
bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700
64
+
h-full overflow-y-auto shadow-sm
65
+
px-6 py-4 fixed inset-x-0 top-0 w-fit max-w-4/5 m-0"
66
+
>
67
+
<div class="flex flex-col min-h-full">
68
+
<div class="flex-1 space-y-4">
69
+
<button
70
+
type="button"
71
+
popovertarget="mobile-toc-popover"
72
+
popovertargetaction="toggle"
73
+
class="w-full flex gap-2 items-center text-sm font-semibold dark:text-white mb-4">
74
+
${ x.svg() }
75
+
$if(toc-title)$$toc-title$$else$Table of Contents$endif$
76
+
</button>
77
+
${ logo.html() }
78
+
${ search.html() }
79
+
${ table-of-contents:toc.html() }
80
+
</div>
81
+
${ single-page:mode.html() }
82
+
</div>
83
+
</div>
84
+
55
85
<!-- desktop sidebar toc -->
56
-
<nav id="$idprefix$TOC" role="doc-toc" class="hidden md:block fixed left-0 top-0 w-80 h-screen bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 overflow-y-auto p-4 z-50">
57
-
$if(toc-title)$
58
-
<h2 id="$idprefix$toc-title" class="text-lg font-semibold mb-4 text-gray-900">$toc-title$</h2>
59
-
$endif$
60
-
${ table-of-contents:toc.html() }
86
+
<nav
87
+
id="$idprefix$TOC"
88
+
role="doc-toc"
89
+
class="hidden md:flex md:flex-col gap-4 fixed left-0 top-0 w-80 h-screen
90
+
bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700
91
+
p-4 z-50 overflow-y-auto">
92
+
${ logo.html() }
93
+
${ search.html() }
94
+
<div class="flex-1">
95
+
$if(toc-title)$
96
+
<h2 id="$idprefix$toc-title" class="text-lg font-semibold mb-4 text-gray-900">$toc-title$</h2>
97
+
$endif$
98
+
${ table-of-contents:toc.html() }
99
+
</div>
100
+
${ single-page:mode.html() }
61
101
</nav>
62
102
$endif$
63
103
64
104
<div class="$if(toc)$md:ml-80$endif$ flex-1 flex flex-col">
65
105
<main class="max-w-4xl w-full mx-auto p-6 flex-1">
66
106
$if(top)$
67
-
$-- only print title block if this is NOT the top page
107
+
$-- only print title block if this is NOT the top page
68
108
$else$
69
109
$if(title)$
70
-
<header id="title-block-header" class="mb-8 pb-8 border-b border-gray-200 dark:border-gray-700">
71
-
<h1 class="text-4xl font-bold mb-2 text-black dark:text-white">$title$</h1>
72
-
$if(subtitle)$
73
-
<p class="text-xl text-gray-500 dark:text-gray-400 mb-2">$subtitle$</p>
74
-
$endif$
75
-
$for(author)$
76
-
<p class="text-sm text-gray-500 dark:text-gray-400">$author$</p>
77
-
$endfor$
78
-
$if(date)$
79
-
<p class="text-sm text-gray-500 dark:text-gray-400">Updated on $date$</p>
80
-
$endif$
81
-
$if(abstract)$
82
-
<div class="mt-6 p-4 bg-gray-50 rounded-lg">
83
-
<div class="text-sm font-semibold text-gray-700 uppercase mb-2">$abstract-title$</div>
84
-
<div class="text-gray-700">$abstract$</div>
85
-
</div>
86
-
$endif$
87
-
$endif$
88
-
</header>
110
+
<header id="title-block-header" class="mb-8 pb-8 border-b border-gray-200 dark:border-gray-700">
111
+
<h1 class="text-4xl font-bold mb-2 text-black dark:text-white">$title$</h1>
112
+
$if(subtitle)$
113
+
<p class="text-xl text-gray-500 dark:text-gray-400 mb-2">$subtitle$</p>
114
+
$endif$
115
+
$for(author)$
116
+
<p class="text-sm text-gray-500 dark:text-gray-400">$author$</p>
117
+
$endfor$
118
+
$if(date)$
119
+
<p class="text-sm text-gray-500 dark:text-gray-400">Updated on $date$</p>
120
+
$endif$
121
+
$endif$
122
+
</header>
89
123
$endif$
124
+
125
+
$if(abstract)$
126
+
<article class="prose dark:prose-invert max-w-none">
127
+
$abstract$
128
+
</article>
129
+
$endif$
130
+
90
131
<article class="prose dark:prose-invert max-w-none">
91
132
$body$
92
133
</article>
93
134
</main>
94
-
<nav id="sitenav" class="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 ">
135
+
<nav id="sitenav" class="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
95
136
<div class="max-w-4xl mx-auto px-8 py-4">
96
137
<div class="flex justify-between gap-4">
97
138
<span class="flex-1">
+18
-3
flake.nix
+18
-3
flake.nix
···
76
76
};
77
77
buildGoApplication =
78
78
(self.callPackage "${gomod2nix}/builder" {
79
-
gomod2nix = gomod2nix.legacyPackages.${pkgs.system}.gomod2nix;
79
+
gomod2nix = gomod2nix.legacyPackages.${pkgs.stdenv.hostPlatform.system}.gomod2nix;
80
80
}).buildGoApplication;
81
81
modules = ./nix/gomod2nix.toml;
82
82
sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix {
···
94
94
spindle = self.callPackage ./nix/pkgs/spindle.nix {};
95
95
knot-unwrapped = self.callPackage ./nix/pkgs/knot-unwrapped.nix {};
96
96
knot = self.callPackage ./nix/pkgs/knot.nix {};
97
+
dolly = self.callPackage ./nix/pkgs/dolly.nix {};
97
98
});
98
99
in {
99
100
overlays.default = final: prev: {
100
-
inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs;
101
+
inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs dolly;
101
102
};
102
103
103
104
packages = forAllSystems (system: let
···
106
107
staticPackages = mkPackageSet pkgs.pkgsStatic;
107
108
crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic;
108
109
in {
109
-
inherit (packages) appview appview-static-files lexgen goat spindle knot knot-unwrapped sqlite-lib docs;
110
+
inherit
111
+
(packages)
112
+
appview
113
+
appview-static-files
114
+
lexgen
115
+
goat
116
+
spindle
117
+
knot
118
+
knot-unwrapped
119
+
sqlite-lib
120
+
docs
121
+
dolly
122
+
;
110
123
111
124
pkgsStatic-appview = staticPackages.appview;
112
125
pkgsStatic-knot = staticPackages.knot;
113
126
pkgsStatic-knot-unwrapped = staticPackages.knot-unwrapped;
114
127
pkgsStatic-spindle = staticPackages.spindle;
115
128
pkgsStatic-sqlite-lib = staticPackages.sqlite-lib;
129
+
pkgsStatic-dolly = staticPackages.dolly;
116
130
117
131
pkgsCross-gnu64-pkgsStatic-appview = crossPackages.appview;
118
132
pkgsCross-gnu64-pkgsStatic-knot = crossPackages.knot;
119
133
pkgsCross-gnu64-pkgsStatic-knot-unwrapped = crossPackages.knot-unwrapped;
120
134
pkgsCross-gnu64-pkgsStatic-spindle = crossPackages.spindle;
135
+
pkgsCross-gnu64-pkgsStatic-dolly = crossPackages.dolly;
121
136
122
137
treefmt-wrapper = pkgs.treefmt.withConfig {
123
138
settings.formatter = {
+88
ico/ico.go
+88
ico/ico.go
···
1
+
package ico
2
+
3
+
import (
4
+
"bytes"
5
+
"encoding/binary"
6
+
"fmt"
7
+
"image"
8
+
"image/png"
9
+
)
10
+
11
+
type IconDir struct {
12
+
Reserved uint16 // must be 0
13
+
Type uint16 // 1 for ICO, 2 for CUR
14
+
Count uint16 // number of images
15
+
}
16
+
17
+
type IconDirEntry struct {
18
+
Width uint8 // 0 means 256
19
+
Height uint8 // 0 means 256
20
+
ColorCount uint8
21
+
Reserved uint8 // must be 0
22
+
ColorPlanes uint16 // 0 or 1
23
+
BitsPerPixel uint16
24
+
SizeInBytes uint32
25
+
Offset uint32
26
+
}
27
+
28
+
func ImageToIco(img image.Image) ([]byte, error) {
29
+
// encode image as png
30
+
var pngBuf bytes.Buffer
31
+
if err := png.Encode(&pngBuf, img); err != nil {
32
+
return nil, fmt.Errorf("failed to encode PNG: %w", err)
33
+
}
34
+
pngData := pngBuf.Bytes()
35
+
36
+
// get image dimensions
37
+
bounds := img.Bounds()
38
+
width := bounds.Dx()
39
+
height := bounds.Dy()
40
+
41
+
// prepare output buffer
42
+
var icoBuf bytes.Buffer
43
+
44
+
iconDir := IconDir{
45
+
Reserved: 0,
46
+
Type: 1, // ICO format
47
+
Count: 1, // One image
48
+
}
49
+
50
+
w := uint8(width)
51
+
h := uint8(height)
52
+
53
+
// width/height of 256 should be stored as 0
54
+
if width == 256 {
55
+
w = 0
56
+
}
57
+
if height == 256 {
58
+
h = 0
59
+
}
60
+
61
+
iconDirEntry := IconDirEntry{
62
+
Width: w,
63
+
Height: h,
64
+
ColorCount: 0, // 0 for PNG (32-bit)
65
+
Reserved: 0,
66
+
ColorPlanes: 1,
67
+
BitsPerPixel: 32, // PNG with alpha
68
+
SizeInBytes: uint32(len(pngData)),
69
+
Offset: 6 + 16, // Size of ICONDIR + ICONDIRENTRY
70
+
}
71
+
72
+
// write IconDir
73
+
if err := binary.Write(&icoBuf, binary.LittleEndian, iconDir); err != nil {
74
+
return nil, fmt.Errorf("failed to write ICONDIR: %w", err)
75
+
}
76
+
77
+
// write IconDirEntry
78
+
if err := binary.Write(&icoBuf, binary.LittleEndian, iconDirEntry); err != nil {
79
+
return nil, fmt.Errorf("failed to write ICONDIRENTRY: %w", err)
80
+
}
81
+
82
+
// write PNG data directly
83
+
if _, err := icoBuf.Write(pngData); err != nil {
84
+
return nil, fmt.Errorf("failed to write PNG data: %w", err)
85
+
}
86
+
87
+
return icoBuf.Bytes(), nil
88
+
}
+14
input.css
+14
input.css
···
124
124
dark:text-gray-100 dark:before:bg-gray-800 dark:before:border-gray-700;
125
125
}
126
126
127
+
.btn-flat {
128
+
@apply relative z-10 inline-flex min-h-[30px] cursor-pointer items-center justify-center
129
+
bg-transparent px-2 pb-[0.2rem] text-sm text-gray-900
130
+
before:absolute before:inset-0 before:-z-10 before:block before:rounded
131
+
before:border before:border-gray-200 before:bg-white
132
+
before:content-[''] before:transition-all before:duration-150 before:ease-in-out
133
+
hover:before:bg-gray-50
134
+
dark:hover:before:bg-gray-700
135
+
focus:outline-none focus-visible:before:outline focus-visible:before:outline-2 focus-visible:before:outline-gray-400
136
+
disabled:cursor-not-allowed disabled:opacity-50
137
+
dark:text-gray-100 dark:before:bg-gray-800 dark:before:border-gray-700;
138
+
}
139
+
127
140
.btn-create {
128
141
@apply btn text-white
129
142
before:bg-green-600 hover:before:bg-green-700
···
255
268
@apply py-1 text-gray-900 dark:text-gray-100;
256
269
}
257
270
}
271
+
258
272
}
259
273
260
274
/* Background */
+10
-2
lexicons/pulls/pull.json
+10
-2
lexicons/pulls/pull.json
···
12
12
"required": [
13
13
"target",
14
14
"title",
15
-
"patch",
15
+
"patchBlob",
16
16
"createdAt"
17
17
],
18
18
"properties": {
···
27
27
"type": "string"
28
28
},
29
29
"patch": {
30
-
"type": "string"
30
+
"type": "string",
31
+
"description": "(deprecated) use patchBlob instead"
32
+
},
33
+
"patchBlob": {
34
+
"type": "blob",
35
+
"accept": [
36
+
"text/x-patch"
37
+
],
38
+
"description": "patch content"
31
39
},
32
40
"source": {
33
41
"type": "ref",
+6
-1
nix/pkgs/appview-static-files.nix
+6
-1
nix/pkgs/appview-static-files.nix
···
8
8
actor-typeahead-src,
9
9
sqlite-lib,
10
10
tailwindcss,
11
+
dolly,
11
12
src,
12
13
}:
13
14
runCommandLocal "appview-static-files" {
···
17
18
(allow file-read* (subpath "/System/Library/OpenSSL"))
18
19
'';
19
20
} ''
20
-
mkdir -p $out/{fonts,icons} && cd $out
21
+
mkdir -p $out/{fonts,icons,logos} && cd $out
21
22
cp -f ${htmx-src} htmx.min.js
22
23
cp -f ${htmx-ws-src} htmx-ext-ws.min.js
23
24
cp -rf ${lucide-src}/*.svg icons/
···
26
27
cp -f ${inter-fonts-src}/InterVariable*.ttf fonts/
27
28
cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono*.woff2 fonts/
28
29
cp -f ${actor-typeahead-src}/actor-typeahead.js .
30
+
31
+
${dolly}/bin/dolly -output logos/dolly.png -size 180x180
32
+
${dolly}/bin/dolly -output logos/dolly.ico -size 48x48
33
+
${dolly}/bin/dolly -output logos/dolly.svg -color currentColor
29
34
# tailwindcss -c $src/tailwind.config.js -i $src/input.css -o tw.css won't work
30
35
# for whatever reason (produces broken css), so we are doing this instead
31
36
cd ${src} && ${tailwindcss}/bin/tailwindcss -i input.css -o $out/tw.css
+17
-1
nix/pkgs/docs.nix
+17
-1
nix/pkgs/docs.nix
···
5
5
inter-fonts-src,
6
6
ibm-plex-mono-src,
7
7
lucide-src,
8
+
dolly,
8
9
src,
9
10
}:
10
11
runCommandLocal "docs" {} ''
···
18
19
# icons
19
20
cp -rf ${lucide-src}/*.svg working/
20
21
21
-
# content
22
+
# logo
23
+
${dolly}/bin/dolly -output working/dolly.svg -color currentColor
24
+
25
+
# content - chunked
22
26
${pandoc}/bin/pandoc ${src}/docs/DOCS.md \
23
27
-o $out/ \
24
28
-t chunkedhtml \
25
29
--variable toc \
30
+
--variable-json single-page=false \
26
31
--toc-depth=2 \
27
32
--css=stylesheet.css \
28
33
--chunk-template="%i.html" \
34
+
--highlight-style=working/highlight.theme \
35
+
--template=working/template.html
36
+
37
+
# content - single page
38
+
${pandoc}/bin/pandoc ${src}/docs/DOCS.md \
39
+
-o $out/single-page.html \
40
+
--toc \
41
+
--variable toc \
42
+
--variable single-page \
43
+
--toc-depth=2 \
44
+
--css=stylesheet.css \
29
45
--highlight-style=working/highlight.theme \
30
46
--template=working/template.html
31
47
+21
nix/pkgs/dolly.nix
+21
nix/pkgs/dolly.nix
···
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
+
}
+1
-1
nix/vm.nix
+1
-1
nix/vm.nix
···
8
8
var = builtins.getEnv name;
9
9
in
10
10
if var == ""
11
-
then throw "\$${name} must be defined, see docs/hacking.md for more details"
11
+
then throw "\$${name} must be defined, see https://docs.tangled.org/hacking-on-tangled.html#hacking-on-tangled for more details"
12
12
else var;
13
13
envVarOr = name: default: let
14
14
var = builtins.getEnv name;
+3
-3
readme.md
+3
-3
readme.md
···
10
10
11
11
## docs
12
12
13
-
* [knot hosting guide](/docs/knot-hosting.md)
14
-
* [contributing guide](/docs/contributing.md) **please read before opening a PR!**
15
-
* [hacking on tangled](/docs/hacking.md)
13
+
- [knot hosting guide](https://docs.tangled.org/knot-self-hosting-guide.html#knot-self-hosting-guide)
14
+
- [contributing guide](https://docs.tangled.org/contribution-guide.html#contribution-guide) **please read before opening a PR!**
15
+
- [hacking on tangled](https://docs.tangled.org/hacking-on-tangled.html#hacking-on-tangled)
16
16
17
17
## security
18
18
+2
-2
spindle/models/models.go
+2
-2
spindle/models/models.go
+1
-1
spindle/motd
+1
-1
spindle/motd
+31
-13
spindle/server.go
+31
-13
spindle/server.go
···
8
8
"log/slog"
9
9
"maps"
10
10
"net/http"
11
+
"sync"
11
12
12
13
"github.com/go-chi/chi/v5"
13
14
"tangled.org/core/api/tangled"
···
30
31
)
31
32
32
33
//go:embed motd
33
-
var motd []byte
34
+
var defaultMotd []byte
34
35
35
36
const (
36
37
rbacDomain = "thisserver"
37
38
)
38
39
39
40
type Spindle struct {
40
-
jc *jetstream.JetstreamClient
41
-
db *db.DB
42
-
e *rbac.Enforcer
43
-
l *slog.Logger
44
-
n *notifier.Notifier
45
-
engs map[string]models.Engine
46
-
jq *queue.Queue
47
-
cfg *config.Config
48
-
ks *eventconsumer.Consumer
49
-
res *idresolver.Resolver
50
-
vault secrets.Manager
41
+
jc *jetstream.JetstreamClient
42
+
db *db.DB
43
+
e *rbac.Enforcer
44
+
l *slog.Logger
45
+
n *notifier.Notifier
46
+
engs map[string]models.Engine
47
+
jq *queue.Queue
48
+
cfg *config.Config
49
+
ks *eventconsumer.Consumer
50
+
res *idresolver.Resolver
51
+
vault secrets.Manager
52
+
motd []byte
53
+
motdMu sync.RWMutex
51
54
}
52
55
53
56
// New creates a new Spindle server with the provided configuration and engines.
···
128
131
cfg: cfg,
129
132
res: resolver,
130
133
vault: vault,
134
+
motd: defaultMotd,
131
135
}
132
136
133
137
err = e.AddSpindle(rbacDomain)
···
201
205
return s.e
202
206
}
203
207
208
+
// SetMotdContent sets custom MOTD content, replacing the embedded default.
209
+
func (s *Spindle) SetMotdContent(content []byte) {
210
+
s.motdMu.Lock()
211
+
defer s.motdMu.Unlock()
212
+
s.motd = content
213
+
}
214
+
215
+
// GetMotdContent returns the current MOTD content.
216
+
func (s *Spindle) GetMotdContent() []byte {
217
+
s.motdMu.RLock()
218
+
defer s.motdMu.RUnlock()
219
+
return s.motd
220
+
}
221
+
204
222
// Start starts the Spindle server (blocking).
205
223
func (s *Spindle) Start(ctx context.Context) error {
206
224
// starts a job queue runner in the background
···
246
264
mux := chi.NewRouter()
247
265
248
266
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
249
-
w.Write(motd)
267
+
w.Write(s.GetMotdContent())
250
268
})
251
269
mux.HandleFunc("/events", s.Events)
252
270
mux.HandleFunc("/logs/{knot}/{rkey}/{name}", s.Logs)
+7
-2
types/diff.go
+7
-2
types/diff.go
···
27
27
}
28
28
29
29
type DiffStat struct {
30
-
Insertions int64
31
-
Deletions int64
30
+
Insertions int64
31
+
Deletions int64
32
+
FilesChanged int
32
33
}
33
34
34
35
func (d *Diff) Stats() DiffStat {
···
37
38
stats.Insertions += f.LinesAdded
38
39
stats.Deletions += f.LinesDeleted
39
40
}
41
+
stats.FilesChanged = len(d.TextFragments)
40
42
return stats
41
43
}
42
44
···
74
76
75
77
// used by html elements as a unique ID for hrefs
76
78
func (d *Diff) Id() string {
79
+
if d.IsDelete {
80
+
return d.Name.Old
81
+
}
77
82
return d.Name.New
78
83
}
79
84
+112
types/diff_test.go
+112
types/diff_test.go
···
1
+
package types
2
+
3
+
import "testing"
4
+
5
+
func TestDiffId(t *testing.T) {
6
+
tests := []struct {
7
+
name string
8
+
diff Diff
9
+
expected string
10
+
}{
11
+
{
12
+
name: "regular file uses new name",
13
+
diff: Diff{
14
+
Name: struct {
15
+
Old string `json:"old"`
16
+
New string `json:"new"`
17
+
}{Old: "", New: "src/main.go"},
18
+
},
19
+
expected: "src/main.go",
20
+
},
21
+
{
22
+
name: "new file uses new name",
23
+
diff: Diff{
24
+
Name: struct {
25
+
Old string `json:"old"`
26
+
New string `json:"new"`
27
+
}{Old: "", New: "src/new.go"},
28
+
IsNew: true,
29
+
},
30
+
expected: "src/new.go",
31
+
},
32
+
{
33
+
name: "deleted file uses old name",
34
+
diff: Diff{
35
+
Name: struct {
36
+
Old string `json:"old"`
37
+
New string `json:"new"`
38
+
}{Old: "src/deleted.go", New: ""},
39
+
IsDelete: true,
40
+
},
41
+
expected: "src/deleted.go",
42
+
},
43
+
{
44
+
name: "renamed file uses new name",
45
+
diff: Diff{
46
+
Name: struct {
47
+
Old string `json:"old"`
48
+
New string `json:"new"`
49
+
}{Old: "src/old.go", New: "src/renamed.go"},
50
+
IsRename: true,
51
+
},
52
+
expected: "src/renamed.go",
53
+
},
54
+
}
55
+
56
+
for _, tt := range tests {
57
+
t.Run(tt.name, func(t *testing.T) {
58
+
if got := tt.diff.Id(); got != tt.expected {
59
+
t.Errorf("Diff.Id() = %q, want %q", got, tt.expected)
60
+
}
61
+
})
62
+
}
63
+
}
64
+
65
+
func TestChangedFilesMatchesDiffId(t *testing.T) {
66
+
// ChangedFiles() must return values matching each Diff's Id()
67
+
// so that sidebar links point to the correct anchors.
68
+
// Tests existing, deleted, new, and renamed files.
69
+
nd := NiceDiff{
70
+
Diff: []Diff{
71
+
{
72
+
Name: struct {
73
+
Old string `json:"old"`
74
+
New string `json:"new"`
75
+
}{Old: "", New: "src/modified.go"},
76
+
},
77
+
{
78
+
Name: struct {
79
+
Old string `json:"old"`
80
+
New string `json:"new"`
81
+
}{Old: "src/deleted.go", New: ""},
82
+
IsDelete: true,
83
+
},
84
+
{
85
+
Name: struct {
86
+
Old string `json:"old"`
87
+
New string `json:"new"`
88
+
}{Old: "", New: "src/new.go"},
89
+
IsNew: true,
90
+
},
91
+
{
92
+
Name: struct {
93
+
Old string `json:"old"`
94
+
New string `json:"new"`
95
+
}{Old: "src/old.go", New: "src/renamed.go"},
96
+
IsRename: true,
97
+
},
98
+
},
99
+
}
100
+
101
+
changedFiles := nd.ChangedFiles()
102
+
103
+
if len(changedFiles) != len(nd.Diff) {
104
+
t.Fatalf("ChangedFiles() returned %d items, want %d", len(changedFiles), len(nd.Diff))
105
+
}
106
+
107
+
for i, diff := range nd.Diff {
108
+
if changedFiles[i] != diff.Id() {
109
+
t.Errorf("ChangedFiles()[%d] = %q, but Diff.Id() = %q", i, changedFiles[i], diff.Id())
110
+
}
111
+
}
112
+
}