+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
+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 {
+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
+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">
+1
-1
appview/pages/templates/repo/fragments/diff.html
+1
-1
appview/pages/templates/repo/fragments/diff.html
···
18
18
{{ range $idx, $hunk := $diff }}
19
19
{{ with $hunk }}
20
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-0">
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
-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>
+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/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
}
+11
-2
appview/pulls/pulls.go
+11
-2
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 {
+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
+
}
+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
+
}
+1
-3
appview/state/router.go
+1
-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)
-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
+
}
+23
-25
docs/DOCS.md
+23
-25
docs/DOCS.md
···
2
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
-
self-hostable. [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 the AT Protocolโ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
32
# Quick start guide
35
33
+6
docs/logo.html
+6
docs/logo.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>
+31
-27
docs/template.html
+31
-27
docs/template.html
···
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$
···
60
60
id="mobile-toc-popover"
61
61
popover
62
62
class="mobile-toc-popover
63
-
bg-white dark:bg-gray-800
64
-
border-r border-gray-200 dark:border-gray-700
65
-
h-full overflow-y-auto
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
66
65
px-6 py-4 fixed inset-x-0 top-0 w-fit max-w-4/5 m-0"
67
66
>
68
67
<div class="flex flex-col min-h-full">
69
-
<div class="flex-1">
68
+
<div class="flex-1 space-y-4">
70
69
<button
71
70
type="button"
72
71
popovertarget="mobile-toc-popover"
···
75
74
${ x.svg() }
76
75
$if(toc-title)$$toc-title$$else$Table of Contents$endif$
77
76
</button>
77
+
${ logo.html() }
78
+
${ search.html() }
78
79
${ table-of-contents:toc.html() }
79
80
</div>
80
81
${ single-page:mode.html() }
···
85
86
<nav
86
87
id="$idprefix$TOC"
87
88
role="doc-toc"
88
-
class="hidden md:flex md:flex-col fixed left-0 top-0 w-80 h-screen
89
+
class="hidden md:flex md:flex-col gap-4 fixed left-0 top-0 w-80 h-screen
89
90
bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700
90
91
p-4 z-50 overflow-y-auto">
92
+
${ logo.html() }
93
+
${ search.html() }
91
94
<div class="flex-1">
92
95
$if(toc-title)$
93
-
<h2 id="$idprefix$toc-title" class="text-lg font-semibold mb-4 text-gray-900">$toc-title$</h2>
96
+
<h2 id="$idprefix$toc-title" class="text-lg font-semibold mb-4 text-gray-900">$toc-title$</h2>
94
97
$endif$
95
98
${ table-of-contents:toc.html() }
96
99
</div>
···
101
104
<div class="$if(toc)$md:ml-80$endif$ flex-1 flex flex-col">
102
105
<main class="max-w-4xl w-full mx-auto p-6 flex-1">
103
106
$if(top)$
104
-
$-- only print title block if this is NOT the top page
107
+
$-- only print title block if this is NOT the top page
105
108
$else$
106
109
$if(title)$
107
-
<header id="title-block-header" class="mb-8 pb-8 border-b border-gray-200 dark:border-gray-700">
108
-
<h1 class="text-4xl font-bold mb-2 text-black dark:text-white">$title$</h1>
109
-
$if(subtitle)$
110
-
<p class="text-xl text-gray-500 dark:text-gray-400 mb-2">$subtitle$</p>
111
-
$endif$
112
-
$for(author)$
113
-
<p class="text-sm text-gray-500 dark:text-gray-400">$author$</p>
114
-
$endfor$
115
-
$if(date)$
116
-
<p class="text-sm text-gray-500 dark:text-gray-400">Updated on $date$</p>
117
-
$endif$
118
-
$if(abstract)$
119
-
<div class="mt-6 p-4 bg-gray-50 rounded-lg">
120
-
<div class="text-sm font-semibold text-gray-700 uppercase mb-2">$abstract-title$</div>
121
-
<div class="text-gray-700">$abstract$</div>
122
-
</div>
123
-
$endif$
124
-
$endif$
125
-
</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>
123
+
$endif$
124
+
125
+
$if(abstract)$
126
+
<article class="prose dark:prose-invert max-w-none">
127
+
$abstract$
128
+
</article>
126
129
$endif$
130
+
127
131
<article class="prose dark:prose-invert max-w-none">
128
132
$body$
129
133
</article>
+17
-2
flake.nix
+17
-2
flake.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
+
}
+13
input.css
+13
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
+3
-1
lexicons/pulls/pull.json
+3
-1
lexicons/pulls/pull.json
+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
+4
nix/pkgs/docs.nix
+4
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" {} ''
···
17
18
18
19
# icons
19
20
cp -rf ${lucide-src}/*.svg working/
21
+
22
+
# logo
23
+
${dolly}/bin/dolly -output working/dolly.svg -color currentColor
20
24
21
25
# content - chunked
22
26
${pandoc}/bin/pandoc ${src}/docs/DOCS.md \
+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
+
}
+2
-2
spindle/models/models.go
+2
-2
spindle/models/models.go
+4
-2
types/diff.go
+4
-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