Monorepo for Tangled tangled.org

Compare changes

Choose any two refs to compare.

+79 -20
api/tangled/cbor_gen.go
··· 7934 } 7935 7936 cw := cbg.NewCborWriter(w) 7937 - fieldCount := 9 7938 7939 if t.Body == nil { 7940 fieldCount-- 7941 } 7942 7943 if t.Mentions == nil { 7944 fieldCount-- 7945 } 7946 ··· 8008 } 8009 8010 // t.Patch (string) (string) 8011 - if len("patch") > 1000000 { 8012 - return xerrors.Errorf("Value in field \"patch\" was too long") 8013 - } 8014 8015 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patch"))); err != nil { 8016 - return err 8017 - } 8018 - if _, err := cw.WriteString(string("patch")); err != nil { 8019 - return err 8020 - } 8021 8022 - if len(t.Patch) > 1000000 { 8023 - return xerrors.Errorf("Value in field t.Patch was too long") 8024 - } 8025 8026 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Patch))); err != nil { 8027 - return err 8028 - } 8029 - if _, err := cw.WriteString(string(t.Patch)); err != nil { 8030 - return err 8031 } 8032 8033 // t.Title (string) (string) ··· 8147 return err 8148 } 8149 8150 // t.References ([]string) (slice) 8151 if t.References != nil { 8152 ··· 8262 case "patch": 8263 8264 { 8265 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 8266 if err != nil { 8267 return err 8268 } 8269 8270 - t.Patch = string(sval) 8271 } 8272 // t.Title (string) (string) 8273 case "title": ··· 8370 } 8371 8372 t.CreatedAt = string(sval) 8373 } 8374 // t.References ([]string) (slice) 8375 case "references":
··· 7934 } 7935 7936 cw := cbg.NewCborWriter(w) 7937 + fieldCount := 10 7938 7939 if t.Body == nil { 7940 fieldCount-- 7941 } 7942 7943 if t.Mentions == nil { 7944 + fieldCount-- 7945 + } 7946 + 7947 + if t.Patch == nil { 7948 fieldCount-- 7949 } 7950 ··· 8012 } 8013 8014 // t.Patch (string) (string) 8015 + if t.Patch != nil { 8016 8017 + if len("patch") > 1000000 { 8018 + return xerrors.Errorf("Value in field \"patch\" was too long") 8019 + } 8020 8021 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patch"))); err != nil { 8022 + return err 8023 + } 8024 + if _, err := cw.WriteString(string("patch")); err != nil { 8025 + return err 8026 + } 8027 + 8028 + if t.Patch == nil { 8029 + if _, err := cw.Write(cbg.CborNull); err != nil { 8030 + return err 8031 + } 8032 + } else { 8033 + if len(*t.Patch) > 1000000 { 8034 + return xerrors.Errorf("Value in field t.Patch was too long") 8035 + } 8036 8037 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Patch))); err != nil { 8038 + return err 8039 + } 8040 + if _, err := cw.WriteString(string(*t.Patch)); err != nil { 8041 + return err 8042 + } 8043 + } 8044 } 8045 8046 // t.Title (string) (string) ··· 8160 return err 8161 } 8162 8163 + // t.PatchBlob (util.LexBlob) (struct) 8164 + if len("patchBlob") > 1000000 { 8165 + return xerrors.Errorf("Value in field \"patchBlob\" was too long") 8166 + } 8167 + 8168 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patchBlob"))); err != nil { 8169 + return err 8170 + } 8171 + if _, err := cw.WriteString(string("patchBlob")); err != nil { 8172 + return err 8173 + } 8174 + 8175 + if err := t.PatchBlob.MarshalCBOR(cw); err != nil { 8176 + return err 8177 + } 8178 + 8179 // t.References ([]string) (slice) 8180 if t.References != nil { 8181 ··· 8291 case "patch": 8292 8293 { 8294 + b, err := cr.ReadByte() 8295 if err != nil { 8296 return err 8297 } 8298 + if b != cbg.CborNull[0] { 8299 + if err := cr.UnreadByte(); err != nil { 8300 + return err 8301 + } 8302 8303 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 8304 + if err != nil { 8305 + return err 8306 + } 8307 + 8308 + t.Patch = (*string)(&sval) 8309 + } 8310 } 8311 // t.Title (string) (string) 8312 case "title": ··· 8409 } 8410 8411 t.CreatedAt = string(sval) 8412 + } 8413 + // t.PatchBlob (util.LexBlob) (struct) 8414 + case "patchBlob": 8415 + 8416 + { 8417 + 8418 + b, err := cr.ReadByte() 8419 + if err != nil { 8420 + return err 8421 + } 8422 + if b != cbg.CborNull[0] { 8423 + if err := cr.UnreadByte(); err != nil { 8424 + return err 8425 + } 8426 + t.PatchBlob = new(util.LexBlob) 8427 + if err := t.PatchBlob.UnmarshalCBOR(cr); err != nil { 8428 + return xerrors.Errorf("unmarshaling t.PatchBlob pointer: %w", err) 8429 + } 8430 + } 8431 + 8432 } 8433 // t.References ([]string) (slice) 8434 case "references":
+12 -9
api/tangled/repopull.go
··· 17 } // 18 // RECORDTYPE: RepoPull 19 type RepoPull struct { 20 - LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"` 21 - Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 - CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 - Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 24 - Patch string `json:"patch" cborgen:"patch"` 25 - References []string `json:"references,omitempty" cborgen:"references,omitempty"` 26 - Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"` 27 - Target *RepoPull_Target `json:"target" cborgen:"target"` 28 - Title string `json:"title" cborgen:"title"` 29 } 30 31 // RepoPull_Source is a "source" in the sh.tangled.repo.pull schema.
··· 17 } // 18 // RECORDTYPE: RepoPull 19 type RepoPull struct { 20 + LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"` 21 + Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 + Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 24 + // patch: (deprecated) use patchBlob instead 25 + Patch *string `json:"patch,omitempty" cborgen:"patch,omitempty"` 26 + // patchBlob: patch content 27 + PatchBlob *util.LexBlob `json:"patchBlob" cborgen:"patchBlob"` 28 + References []string `json:"references,omitempty" cborgen:"references,omitempty"` 29 + Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"` 30 + Target *RepoPull_Target `json:"target" cborgen:"target"` 31 + Title string `json:"title" cborgen:"title"` 32 } 33 34 // RepoPull_Source is a "source" in the sh.tangled.repo.pull schema.
+18 -11
appview/db/profile.go
··· 20 timeline := models.ProfileTimeline{ 21 ByMonth: make([]models.ByMonth, TimeframeMonths), 22 } 23 - currentMonth := time.Now().Month() 24 timeframe := fmt.Sprintf("-%d months", TimeframeMonths) 25 26 pulls, err := GetPullsByOwnerDid(e, forDid, timeframe) ··· 30 31 // group pulls by month 32 for _, pull := range pulls { 33 - pullMonth := pull.Created.Month() 34 35 - if currentMonth-pullMonth >= TimeframeMonths { 36 // shouldn't happen; but times are weird 37 continue 38 } 39 40 - idx := currentMonth - pullMonth 41 items := &timeline.ByMonth[idx].PullEvents.Items 42 43 *items = append(*items, &pull) ··· 53 } 54 55 for _, issue := range issues { 56 - issueMonth := issue.Created.Month() 57 58 - if currentMonth-issueMonth >= TimeframeMonths { 59 // shouldn't happen; but times are weird 60 continue 61 } 62 63 - idx := currentMonth - issueMonth 64 items := &timeline.ByMonth[idx].IssueEvents.Items 65 66 *items = append(*items, &issue) ··· 77 if repo.Source != "" { 78 sourceRepo, err = GetRepoByAtUri(e, repo.Source) 79 if err != nil { 80 - return nil, err 81 } 82 } 83 84 - repoMonth := repo.Created.Month() 85 86 - if currentMonth-repoMonth >= TimeframeMonths { 87 // shouldn't happen; but times are weird 88 continue 89 } 90 91 - idx := currentMonth - repoMonth 92 93 items := &timeline.ByMonth[idx].RepoEvents 94 *items = append(*items, models.RepoEvent{ ··· 98 } 99 100 return &timeline, nil 101 } 102 103 func UpsertProfile(tx *sql.Tx, profile *models.Profile) error {
··· 20 timeline := models.ProfileTimeline{ 21 ByMonth: make([]models.ByMonth, TimeframeMonths), 22 } 23 + now := time.Now() 24 timeframe := fmt.Sprintf("-%d months", TimeframeMonths) 25 26 pulls, err := GetPullsByOwnerDid(e, forDid, timeframe) ··· 30 31 // group pulls by month 32 for _, pull := range pulls { 33 + monthsAgo := monthsBetween(pull.Created, now) 34 35 + if monthsAgo >= TimeframeMonths { 36 // shouldn't happen; but times are weird 37 continue 38 } 39 40 + idx := monthsAgo 41 items := &timeline.ByMonth[idx].PullEvents.Items 42 43 *items = append(*items, &pull) ··· 53 } 54 55 for _, issue := range issues { 56 + monthsAgo := monthsBetween(issue.Created, now) 57 58 + if monthsAgo >= TimeframeMonths { 59 // shouldn't happen; but times are weird 60 continue 61 } 62 63 + idx := monthsAgo 64 items := &timeline.ByMonth[idx].IssueEvents.Items 65 66 *items = append(*items, &issue) ··· 77 if repo.Source != "" { 78 sourceRepo, err = GetRepoByAtUri(e, repo.Source) 79 if err != nil { 80 + // the source repo was not found, skip this bit 81 + log.Println("profile", "err", err) 82 } 83 } 84 85 + monthsAgo := monthsBetween(repo.Created, now) 86 87 + if monthsAgo >= TimeframeMonths { 88 // shouldn't happen; but times are weird 89 continue 90 } 91 92 + idx := monthsAgo 93 94 items := &timeline.ByMonth[idx].RepoEvents 95 *items = append(*items, models.RepoEvent{ ··· 99 } 100 101 return &timeline, nil 102 + } 103 + 104 + func monthsBetween(from, to time.Time) int { 105 + years := to.Year() - from.Year() 106 + months := int(to.Month() - from.Month()) 107 + return years*12 + months 108 } 109 110 func UpsertProfile(tx *sql.Tx, profile *models.Profile) error {
+1 -1
appview/db/punchcard.go
··· 78 punch.Count = int(count.Int64) 79 } 80 81 - punchcard.Punches[punch.Date.YearDay()] = punch 82 punchcard.Total += punch.Count 83 } 84
··· 78 punch.Count = int(count.Int64) 79 } 80 81 + punchcard.Punches[punch.Date.YearDay()-1] = punch 82 punchcard.Total += punch.Count 83 } 84
+2 -2
appview/issues/opengraph.go
··· 193 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 194 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 195 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 196 - err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor) 197 if err != nil { 198 - log.Printf("dolly silhouette not available (this is ok): %v", err) 199 } 200 201 // Draw "opened by @author" and date at the bottom with more spacing
··· 193 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 194 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 195 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 196 + err = dollyArea.DrawDolly(dollyX, dollyY, dollySize, dollyColor) 197 if err != nil { 198 + log.Printf("dolly not available (this is ok): %v", err) 199 } 200 201 // Draw "opened by @author" and date at the bottom with more spacing
-5
appview/knots/knots.go
··· 666 k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 667 return 668 } 669 - if memberId.Handle.IsInvalidHandle() { 670 - l.Error("failed to resolve member identity to handle") 671 - k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 672 - return 673 - } 674 675 // remove from enforcer 676 err = k.Enforcer.RemoveKnotMember(domain, memberId.DID.String())
··· 666 k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 667 return 668 } 669 670 // remove from enforcer 671 err = k.Enforcer.RemoveKnotMember(domain, memberId.DID.String())
+4
appview/middleware/middleware.go
··· 223 ) 224 if err != nil { 225 log.Println("failed to resolve repo", "err", err) 226 mw.pages.ErrorKnot404(w) 227 return 228 } ··· 240 f, err := mw.repoResolver.Resolve(r) 241 if err != nil { 242 log.Println("failed to fully resolve repo", err) 243 mw.pages.ErrorKnot404(w) 244 return 245 } ··· 288 f, err := mw.repoResolver.Resolve(r) 289 if err != nil { 290 log.Println("failed to fully resolve repo", err) 291 mw.pages.ErrorKnot404(w) 292 return 293 } ··· 324 f, err := mw.repoResolver.Resolve(r) 325 if err != nil { 326 log.Println("failed to fully resolve repo", err) 327 mw.pages.ErrorKnot404(w) 328 return 329 }
··· 223 ) 224 if err != nil { 225 log.Println("failed to resolve repo", "err", err) 226 + w.WriteHeader(http.StatusNotFound) 227 mw.pages.ErrorKnot404(w) 228 return 229 } ··· 241 f, err := mw.repoResolver.Resolve(r) 242 if err != nil { 243 log.Println("failed to fully resolve repo", err) 244 + w.WriteHeader(http.StatusNotFound) 245 mw.pages.ErrorKnot404(w) 246 return 247 } ··· 290 f, err := mw.repoResolver.Resolve(r) 291 if err != nil { 292 log.Println("failed to fully resolve repo", err) 293 + w.WriteHeader(http.StatusNotFound) 294 mw.pages.ErrorKnot404(w) 295 return 296 } ··· 327 f, err := mw.repoResolver.Resolve(r) 328 if err != nil { 329 log.Println("failed to fully resolve repo", err) 330 + w.WriteHeader(http.StatusNotFound) 331 mw.pages.ErrorKnot404(w) 332 return 333 }
+1 -1
appview/models/pull.go
··· 83 Repo *Repo 84 } 85 86 func (p Pull) AsRecord() tangled.RepoPull { 87 var source *tangled.RepoPull_Source 88 if p.PullSource != nil { ··· 113 Repo: p.RepoAt.String(), 114 Branch: p.TargetBranch, 115 }, 116 - Patch: p.LatestPatch(), 117 Source: source, 118 } 119 return record
··· 83 Repo *Repo 84 } 85 86 + // NOTE: This method does not include patch blob in returned atproto record 87 func (p Pull) AsRecord() tangled.RepoPull { 88 var source *tangled.RepoPull_Source 89 if p.PullSource != nil { ··· 114 Repo: p.RepoAt.String(), 115 Branch: p.TargetBranch, 116 }, 117 Source: source, 118 } 119 return record
+9 -9
appview/ogcard/card.go
··· 334 return nil 335 } 336 337 - func (c *Card) DrawDollySilhouette(x, y, size int, iconColor color.Color) error { 338 tpl, err := template.New("dolly"). 339 - ParseFS(pages.Files, "templates/fragments/dolly/silhouette.html") 340 if err != nil { 341 - return fmt.Errorf("failed to read dolly silhouette template: %w", err) 342 } 343 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) 347 } 348 349 icon, err := BuildSVGIconFromData(svgData.Bytes(), iconColor) ··· 453 454 // Handle SVG separately 455 if contentType == "image/svg+xml" || strings.HasSuffix(url, ".svg") { 456 - return c.convertSVGToPNG(bodyBytes) 457 } 458 459 // Support content types are in-sync with the allowed custom avatar file types ··· 493 } 494 495 // convertSVGToPNG converts SVG data to a PNG image 496 - func (c *Card) convertSVGToPNG(svgData []byte) (image.Image, bool) { 497 // Parse the SVG 498 icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData)) 499 if err != nil { ··· 547 draw.CatmullRom.Scale(scaledImg, scaledImg.Bounds(), img, srcBounds, draw.Src, nil) 548 549 // Draw the image with circular clipping 550 - for cy := 0; cy < size; cy++ { 551 - for cx := 0; cx < size; cx++ { 552 // Calculate distance from center 553 dx := float64(cx - center) 554 dy := float64(cy - center)
··· 334 return nil 335 } 336 337 + func (c *Card) DrawDolly(x, y, size int, iconColor color.Color) error { 338 tpl, err := template.New("dolly"). 339 + ParseFS(pages.Files, "templates/fragments/dolly/logo.html") 340 if err != nil { 341 + return fmt.Errorf("failed to read dolly template: %w", err) 342 } 343 344 var svgData bytes.Buffer 345 + if err = tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", nil); err != nil { 346 + return fmt.Errorf("failed to execute dolly template: %w", err) 347 } 348 349 icon, err := BuildSVGIconFromData(svgData.Bytes(), iconColor) ··· 453 454 // Handle SVG separately 455 if contentType == "image/svg+xml" || strings.HasSuffix(url, ".svg") { 456 + return convertSVGToPNG(bodyBytes) 457 } 458 459 // Support content types are in-sync with the allowed custom avatar file types ··· 493 } 494 495 // convertSVGToPNG converts SVG data to a PNG image 496 + func convertSVGToPNG(svgData []byte) (image.Image, bool) { 497 // Parse the SVG 498 icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData)) 499 if err != nil { ··· 547 draw.CatmullRom.Scale(scaledImg, scaledImg.Bounds(), img, srcBounds, draw.Src, nil) 548 549 // Draw the image with circular clipping 550 + for cy := range size { 551 + for cx := range size { 552 // Calculate distance from center 553 dx := float64(cx - center) 554 dy := float64(cy - center)
+12 -1
appview/pages/pages.go
··· 210 return tpl.ExecuteTemplate(w, "layouts/base", params) 211 } 212 213 func (p *Pages) Favicon(w io.Writer) error { 214 - return p.executePlain("fragments/dolly/silhouette", w, nil) 215 } 216 217 type LoginParams struct {
··· 210 return tpl.ExecuteTemplate(w, "layouts/base", params) 211 } 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 + 222 func (p *Pages) Favicon(w io.Writer) error { 223 + return p.Dolly(w, DollyParams{ 224 + Classes: "text-black dark:text-white", 225 + }) 226 } 227 228 type LoginParams struct {
+1 -1
appview/pages/templates/banner.html
··· 30 <div class="mx-6"> 31 These services may not be fully accessible until upgraded. 32 <a class="underline text-red-800 dark:text-red-200" 33 - href="https://tangled.org/@tangled.org/core/tree/master/docs/migrations.md"> 34 Click to read the upgrade guide</a>. 35 </div> 36 </details>
··· 30 <div class="mx-6"> 31 These services may not be fully accessible until upgraded. 32 <a class="underline text-red-800 dark:text-red-200" 33 + href="https://docs.tangled.org/migrating-knots-spindles.html#migrating-knots-spindles"> 34 Click to read the upgrade guide</a>. 35 </div> 36 </details>
+9 -29
appview/pages/templates/brand/brand.html
··· 4 <div class="grid grid-cols-10"> 5 <header class="col-span-full md:col-span-10 px-6 py-2 mb-4"> 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"> 8 Assets and guidelines for using Tangled's logo and brand elements. 9 </p> 10 </header> ··· 14 15 <!-- Introduction Section --> 16 <section> 17 - <p class="text-gray-600 dark:text-gray-400 mb-2"> 18 Tangled's logo and mascot is <strong>Dolly</strong>, the first ever <em>cloned</em> mammal. Please 19 follow the below guidelines when using Dolly and the logotype. 20 </p> 21 - <p class="text-gray-600 dark:text-gray-400 mb-2"> 22 All assets are served as SVGs, and can be downloaded by right-clicking and clicking "Save image as". 23 </p> 24 </section> ··· 34 </div> 35 <div class="order-1 lg:order-2"> 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> 38 <p class="text-gray-700 dark:text-gray-300"> 39 This is the preferred version of the logotype, featuring dark text and elements, ideal for light 40 backgrounds and designs. ··· 53 </div> 54 <div class="order-1 lg:order-2"> 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> 57 <p class="text-gray-700 dark:text-gray-300"> 58 This version features white text and elements, ideal for dark backgrounds 59 and inverted designs. ··· 81 </div> 82 <div class="order-1 lg:order-2"> 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"> 85 When a smaller 1:1 logo or icon is needed, Dolly's face may be used on its own. 86 </p> 87 <p class="text-gray-700 dark:text-gray-300 mb-4"> ··· 123 </div> 124 <div class="order-1 lg:order-2"> 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"> 127 White logo mark on colored backgrounds. 128 </p> 129 <p class="text-gray-700 dark:text-gray-300 mb-4"> ··· 165 </div> 166 <div class="order-1 lg:order-2"> 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"> 169 Dark logo mark on lighter, pastel backgrounds. 170 </p> 171 <p class="text-gray-700 dark:text-gray-300 mb-4"> ··· 186 </div> 187 <div class="order-1 lg:order-2"> 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"> 190 Custom coloring of the logotype is permitted. 191 </p> 192 <p class="text-gray-700 dark:text-gray-300 mb-4"> ··· 194 </p> 195 <p class="text-gray-700 dark:text-gray-300 text-sm"> 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 </p> 218 </div> 219 </section>
··· 4 <div class="grid grid-cols-10"> 5 <header class="col-span-full md:col-span-10 px-6 py-2 mb-4"> 6 <h1 class="text-2xl font-bold dark:text-white mb-1">Brand</h1> 7 + <p class="text-gray-500 dark:text-gray-300 mb-1"> 8 Assets and guidelines for using Tangled's logo and brand elements. 9 </p> 10 </header> ··· 14 15 <!-- Introduction Section --> 16 <section> 17 + <p class="text-gray-500 dark:text-gray-300 mb-2"> 18 Tangled's logo and mascot is <strong>Dolly</strong>, the first ever <em>cloned</em> mammal. Please 19 follow the below guidelines when using Dolly and the logotype. 20 </p> 21 + <p class="text-gray-500 dark:text-gray-300 mb-2"> 22 All assets are served as SVGs, and can be downloaded by right-clicking and clicking "Save image as". 23 </p> 24 </section> ··· 34 </div> 35 <div class="order-1 lg:order-2"> 36 <h2 class="text-xl font-semibold dark:text-white mb-3">Black logotype</h2> 37 + <p class="text-gray-500 dark:text-gray-300 mb-4">For use on light-colored backgrounds.</p> 38 <p class="text-gray-700 dark:text-gray-300"> 39 This is the preferred version of the logotype, featuring dark text and elements, ideal for light 40 backgrounds and designs. ··· 53 </div> 54 <div class="order-1 lg:order-2"> 55 <h2 class="text-xl font-semibold dark:text-white mb-3">White logotype</h2> 56 + <p class="text-gray-500 dark:text-gray-300 mb-4">For use on dark-colored backgrounds.</p> 57 <p class="text-gray-700 dark:text-gray-300"> 58 This version features white text and elements, ideal for dark backgrounds 59 and inverted designs. ··· 81 </div> 82 <div class="order-1 lg:order-2"> 83 <h2 class="text-xl font-semibold dark:text-white mb-3">Mark only</h2> 84 + <p class="text-gray-500 dark:text-gray-300 mb-4"> 85 When a smaller 1:1 logo or icon is needed, Dolly's face may be used on its own. 86 </p> 87 <p class="text-gray-700 dark:text-gray-300 mb-4"> ··· 123 </div> 124 <div class="order-1 lg:order-2"> 125 <h2 class="text-xl font-semibold dark:text-white mb-3">Colored backgrounds</h2> 126 + <p class="text-gray-500 dark:text-gray-300 mb-4"> 127 White logo mark on colored backgrounds. 128 </p> 129 <p class="text-gray-700 dark:text-gray-300 mb-4"> ··· 165 </div> 166 <div class="order-1 lg:order-2"> 167 <h2 class="text-xl font-semibold dark:text-white mb-3">Lighter backgrounds</h2> 168 + <p class="text-gray-500 dark:text-gray-300 mb-4"> 169 Dark logo mark on lighter, pastel backgrounds. 170 </p> 171 <p class="text-gray-700 dark:text-gray-300 mb-4"> ··· 186 </div> 187 <div class="order-1 lg:order-2"> 188 <h2 class="text-xl font-semibold dark:text-white mb-3">Recoloring</h2> 189 + <p class="text-gray-500 dark:text-gray-300 mb-4"> 190 Custom coloring of the logotype is permitted. 191 </p> 192 <p class="text-gray-700 dark:text-gray-300 mb-4"> ··· 194 </p> 195 <p class="text-gray-700 dark:text-gray-300 text-sm"> 196 <strong>Example:</strong> Gray/sand colored logotype on a light yellow/tan background. 197 </p> 198 </div> 199 </section>
+14 -2
appview/pages/templates/fragments/dolly/logo.html
··· 2 <svg 3 version="1.1" 4 id="svg1" 5 - class="{{ . }}" 6 width="25" 7 height="25" 8 viewBox="0 0 25 25" ··· 17 xmlns:svg="http://www.w3.org/2000/svg" 18 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 19 xmlns:cc="http://creativecommons.org/ns#"> 20 <sodipodi:namedview 21 id="namedview1" 22 pagecolor="#ffffff" ··· 51 id="g1" 52 transform="translate(-0.42924038,-0.87777209)"> 53 <path 54 - fill="currentColor" 55 style="stroke-width:0.111183;" 56 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 id="path4"
··· 2 <svg 3 version="1.1" 4 id="svg1" 5 + class="{{ .Classes }}" 6 width="25" 7 height="25" 8 viewBox="0 0 25 25" ··· 17 xmlns:svg="http://www.w3.org/2000/svg" 18 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 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> 31 <sodipodi:namedview 32 id="namedview1" 33 pagecolor="#ffffff" ··· 62 id="g1" 63 transform="translate(-0.42924038,-0.87777209)"> 64 <path 65 + class="dolly" 66 + fill="{{ or .FillColor "currentColor" }}" 67 style="stroke-width:0.111183;" 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" 69 id="path4"
-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 {{ define "fragments/logotype" }} 2 <span class="flex items-center gap-2"> 3 - {{ template "fragments/dolly/logo" "size-16 text-black dark:text-white" }} 4 <span class="font-bold text-4xl not-italic">tangled</span> 5 <span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1"> 6 alpha
··· 1 {{ define "fragments/logotype" }} 2 <span class="flex items-center gap-2"> 3 + {{ template "fragments/dolly/logo" (dict "Classes" "size-16 text-black dark:text-white") }} 4 <span class="font-bold text-4xl not-italic">tangled</span> 5 <span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1"> 6 alpha
+1 -1
appview/pages/templates/fragments/logotypeSmall.html
··· 1 {{ define "fragments/logotypeSmall" }} 2 <span class="flex items-center gap-2"> 3 - {{ template "fragments/dolly/logo" "size-8 text-black dark:text-white" }} 4 <span class="font-bold text-xl not-italic">tangled</span> 5 <span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1"> 6 alpha
··· 1 {{ define "fragments/logotypeSmall" }} 2 <span class="flex items-center gap-2"> 3 + {{ template "fragments/dolly/logo" (dict "Classes" "size-8 text-black dark:text-white")}} 4 <span class="font-bold text-xl not-italic">tangled</span> 5 <span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1"> 6 alpha
+1 -1
appview/pages/templates/knots/index.html
··· 105 {{ define "docsButton" }} 106 <a 107 class="btn flex items-center gap-2" 108 - href="https://tangled.org/@tangled.org/core/blob/master/docs/knot-hosting.md"> 109 {{ i "book" "size-4" }} 110 docs 111 </a>
··· 105 {{ define "docsButton" }} 106 <a 107 class="btn flex items-center gap-2" 108 + href="https://docs.tangled.org/knot-self-hosting-guide.html#knot-self-hosting-guide"> 109 {{ i "book" "size-4" }} 110 docs 111 </a>
+4
appview/pages/templates/layouts/base.html
··· 11 <script defer src="/static/htmx-ext-ws.min.js"></script> 12 <script defer src="/static/actor-typeahead.js" type="module"></script> 13 14 <!-- preconnect to image cdn --> 15 <link rel="preconnect" href="https://avatar.tangled.sh" /> 16 <link rel="preconnect" href="https://camo.tangled.sh" />
··· 11 <script defer src="/static/htmx-ext-ws.min.js"></script> 12 <script defer src="/static/actor-typeahead.js" type="module"></script> 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 + 18 <!-- preconnect to image cdn --> 19 <link rel="preconnect" href="https://avatar.tangled.sh" /> 20 <link rel="preconnect" href="https://camo.tangled.sh" />
+2 -2
appview/pages/templates/layouts/fragments/footer.html
··· 26 <div class="flex flex-col gap-1"> 27 <div class="{{ $headerStyle }}">resources</div> 28 <a href="https://blog.tangled.org" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "book-open" $iconStyle }} blog</a> 29 - <a href="https://tangled.org/@tangled.org/core/tree/master/docs" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a> 30 <a href="https://tangled.org/@tangled.org/core" class="{{ $linkStyle }}">{{ i "code" $iconStyle }} source</a> 31 <a href="https://tangled.org/brand" class="{{ $linkStyle }}">{{ i "paintbrush" $iconStyle }} brand</a> 32 </div> ··· 73 <div class="flex flex-col gap-1"> 74 <div class="{{ $headerStyle }}">resources</div> 75 <a href="https://blog.tangled.org" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "book-open" $iconStyle }} blog</a> 76 - <a href="https://tangled.org/@tangled.org/core/tree/master/docs" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a> 77 <a href="https://tangled.org/@tangled.org/core" class="{{ $linkStyle }}">{{ i "code" $iconStyle }} source</a> 78 <a href="https://tangled.org/brand" class="{{ $linkStyle }}">{{ i "paintbrush" $iconStyle }} brand</a> 79 </div>
··· 26 <div class="flex flex-col gap-1"> 27 <div class="{{ $headerStyle }}">resources</div> 28 <a href="https://blog.tangled.org" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "book-open" $iconStyle }} blog</a> 29 + <a href="https://docs.tangled.org" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a> 30 <a href="https://tangled.org/@tangled.org/core" class="{{ $linkStyle }}">{{ i "code" $iconStyle }} source</a> 31 <a href="https://tangled.org/brand" class="{{ $linkStyle }}">{{ i "paintbrush" $iconStyle }} brand</a> 32 </div> ··· 73 <div class="flex flex-col gap-1"> 74 <div class="{{ $headerStyle }}">resources</div> 75 <a href="https://blog.tangled.org" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "book-open" $iconStyle }} blog</a> 76 + <a href="https://docs.tangled.org" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a> 77 <a href="https://tangled.org/@tangled.org/core" class="{{ $linkStyle }}">{{ i "code" $iconStyle }} source</a> 78 <a href="https://tangled.org/brand" class="{{ $linkStyle }}">{{ i "paintbrush" $iconStyle }} brand</a> 79 </div>
+1 -5
appview/pages/templates/layouts/fragments/topbar.html
··· 3 <div class="flex justify-between p-0 items-center"> 4 <div id="left-items"> 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> 11 </a> 12 </div> 13
··· 3 <div class="flex justify-between p-0 items-center"> 4 <div id="left-items"> 5 <a href="/" hx-boost="true" class="text-2xl no-underline hover:no-underline flex items-center gap-2"> 6 + {{ template "fragments/logotypeSmall" }} 7 </a> 8 </div> 9
+1 -1
appview/pages/templates/repo/fragments/diff.html
··· 17 {{ else }} 18 {{ range $idx, $hunk := $diff }} 19 {{ with $hunk }} 20 - <details open id="file-{{ .Name.New }}" class="group border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm" tabindex="{{ add $idx 1 }}"> 21 <summary class="list-none cursor-pointer sticky top-0"> 22 <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 23 <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto">
··· 17 {{ else }} 18 {{ range $idx, $hunk := $diff }} 19 {{ with $hunk }} 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"> 22 <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 23 <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto">
+35 -35
appview/pages/templates/repo/fragments/splitDiff.html
··· 3 {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800" -}} 4 {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 5 {{- $lineNrSepStyle := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 6 - {{- $containerStyle := "flex min-w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} 7 {{- $emptyStyle := "bg-gray-200/30 dark:bg-gray-700/30" -}} 8 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400" -}} 9 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} 10 {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}} 11 {{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}} 12 <div class="grid grid-cols-2 divide-x divide-gray-200 dark:divide-gray-700"> 13 - <pre class="overflow-x-auto col-span-1"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</div> 14 {{- range .LeftLines -}} 15 {{- if .IsEmpty -}} 16 - <div class="{{ $emptyStyle }} {{ $containerStyle }}"> 17 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></div> 18 - <div class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></div> 19 - <div class="px-2 invisible" aria-hidden="true">{{ .Content }}</div> 20 - </div> 21 {{- else if eq .Op.String "-" -}} 22 - <div class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}"> 23 - <div class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></div> 24 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 25 - <div class="px-2">{{ .Content }}</div> 26 - </div> 27 {{- else if eq .Op.String " " -}} 28 - <div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}"> 29 - <div class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></div> 30 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 31 - <div class="px-2">{{ .Content }}</div> 32 - </div> 33 {{- end -}} 34 {{- end -}} 35 - {{- end -}}</div></div></pre> 36 37 - <pre class="overflow-x-auto col-span-1"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</div> 38 {{- range .RightLines -}} 39 {{- if .IsEmpty -}} 40 - <div class="{{ $emptyStyle }} {{ $containerStyle }}"> 41 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></div> 42 - <div class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></div> 43 - <div class="px-2 invisible" aria-hidden="true">{{ .Content }}</div> 44 - </div> 45 {{- else if eq .Op.String "+" -}} 46 - <div class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}"> 47 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></div> 48 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 49 - <div class="px-2" >{{ .Content }}</div> 50 - </div> 51 {{- else if eq .Op.String " " -}} 52 - <div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}"> 53 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></div> 54 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 55 - <div class="px-2">{{ .Content }}</div> 56 - </div> 57 {{- end -}} 58 {{- end -}} 59 - {{- end -}}</div></div></pre> 60 </div> 61 {{ end }}
··· 3 {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800" -}} 4 {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 5 {{- $lineNrSepStyle := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 6 + {{- $containerStyle := "inline-flex w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} 7 {{- $emptyStyle := "bg-gray-200/30 dark:bg-gray-700/30" -}} 8 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400" -}} 9 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} 10 {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}} 11 {{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}} 12 <div class="grid grid-cols-2 divide-x divide-gray-200 dark:divide-gray-700"> 13 + <div class="overflow-x-auto col-span-1 font-mono leading-normal"><div class="overflow-x-auto"><div class="inline-flex flex-col min-w-full">{{- range .TextFragments -}}<span class="block bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</span> 14 {{- range .LeftLines -}} 15 {{- if .IsEmpty -}} 16 + <span class="{{ $emptyStyle }} {{ $containerStyle }}"> 17 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></span> 18 + <span class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></span> 19 + <span class="px-2 invisible" aria-hidden="true">{{ .Content }}</span> 20 + </span> 21 {{- else if eq .Op.String "-" -}} 22 + <span class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}"> 23 + <span class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></span> 24 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 25 + <span class="px-2 whitespace-pre">{{ .Content }}</span> 26 + </span> 27 {{- else if eq .Op.String " " -}} 28 + <span class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}"> 29 + <span class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></span> 30 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 31 + <span class="px-2 whitespace-pre">{{ .Content }}</span> 32 + </span> 33 {{- end -}} 34 {{- end -}} 35 + {{- end -}}</div></div></div> 36 37 + <div class="overflow-x-auto col-span-1 font-mono leading-normal"><div class="overflow-x-auto"><div class="inline-flex flex-col min-w-full">{{- range .TextFragments -}}<span class="block bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</span> 38 {{- range .RightLines -}} 39 {{- if .IsEmpty -}} 40 + <span class="{{ $emptyStyle }} {{ $containerStyle }}"> 41 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></span> 42 + <span class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></span> 43 + <span class="px-2 invisible" aria-hidden="true">{{ .Content }}</span> 44 + </span> 45 {{- else if eq .Op.String "+" -}} 46 + <span class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}"> 47 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></span> 48 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 49 + <span class="px-2 whitespace-pre">{{ .Content }}</span> 50 + </span> 51 {{- else if eq .Op.String " " -}} 52 + <span class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}"> 53 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a> </span> 54 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 55 + <span class="px-2 whitespace-pre">{{ .Content }}</span> 56 + </span> 57 {{- end -}} 58 {{- end -}} 59 + {{- end -}}</div></div></div> 60 </div> 61 {{ end }}
+21 -22
appview/pages/templates/repo/fragments/unifiedDiff.html
··· 1 {{ define "repo/fragments/unifiedDiff" }} 2 {{ $name := .Id }} 3 - <pre class="overflow-x-auto"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</div> 4 {{- $oldStart := .OldPosition -}} 5 {{- $newStart := .NewPosition -}} 6 {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 target:bg-yellow-200 target:dark:bg-yellow-600" -}} 7 {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 8 {{- $lineNrSepStyle1 := "" -}} 9 {{- $lineNrSepStyle2 := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 10 - {{- $containerStyle := "flex min-w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} 11 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 " -}} 12 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} 13 {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}} 14 {{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}} 15 {{- range .Lines -}} 16 {{- if eq .Op.String "+" -}} 17 - <div class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{$newStart}}"> 18 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><span aria-hidden="true" class="invisible">{{$newStart}}</span></div> 19 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></div> 20 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 21 - <div class="px-2">{{ .Line }}</div> 22 - </div> 23 {{- $newStart = add64 $newStart 1 -}} 24 {{- end -}} 25 {{- if eq .Op.String "-" -}} 26 - <div class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}"> 27 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></div> 28 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><span aria-hidden="true" class="invisible">{{$oldStart}}</span></div> 29 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 30 - <div class="px-2">{{ .Line }}</div> 31 - </div> 32 {{- $oldStart = add64 $oldStart 1 -}} 33 {{- end -}} 34 {{- if eq .Op.String " " -}} 35 - <div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}-N{{$newStart}}"> 36 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $oldStart }}</a></div> 37 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $newStart }}</a></div> 38 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 39 - <div class="px-2">{{ .Line }}</div> 40 - </div> 41 {{- $newStart = add64 $newStart 1 -}} 42 {{- $oldStart = add64 $oldStart 1 -}} 43 {{- end -}} 44 {{- end -}} 45 - {{- end -}}</div></div></pre> 46 {{ end }} 47 -
··· 1 {{ define "repo/fragments/unifiedDiff" }} 2 {{ $name := .Id }} 3 + <div class="overflow-x-auto font-mono leading-normal"><div class="overflow-x-auto"><div class="inline-flex flex-col min-w-full">{{- range .TextFragments -}}<span class="block bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</span> 4 {{- $oldStart := .OldPosition -}} 5 {{- $newStart := .NewPosition -}} 6 {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 target:bg-yellow-200 target:dark:bg-yellow-600" -}} 7 {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 8 {{- $lineNrSepStyle1 := "" -}} 9 {{- $lineNrSepStyle2 := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 10 + {{- $containerStyle := "inline-flex w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} 11 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 " -}} 12 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} 13 {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}} 14 {{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}} 15 {{- range .Lines -}} 16 {{- if eq .Op.String "+" -}} 17 + <span class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{$newStart}}"> 18 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><span aria-hidden="true" class="invisible">{{$newStart}}</span></span> 19 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></span> 20 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 21 + <span class="px-2 whitespace-pre">{{ .Line }}</span> 22 + </span> 23 {{- $newStart = add64 $newStart 1 -}} 24 {{- end -}} 25 {{- if eq .Op.String "-" -}} 26 + <span class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}"> 27 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></span> 28 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><span aria-hidden="true" class="invisible">{{$oldStart}}</span></span> 29 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 30 + <span class="px-2 whitespace-pre">{{ .Line }}</span> 31 + </span> 32 {{- $oldStart = add64 $oldStart 1 -}} 33 {{- end -}} 34 {{- if eq .Op.String " " -}} 35 + <span class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}-N{{$newStart}}"> 36 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $oldStart }}</a></span> 37 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $newStart }}</a></span> 38 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 39 + <span class="px-2 whitespace-pre">{{ .Line }}</span> 40 + </span> 41 {{- $newStart = add64 $newStart 1 -}} 42 {{- $oldStart = add64 $oldStart 1 -}} 43 {{- end -}} 44 {{- end -}} 45 + {{- end -}}</div></div></div> 46 {{ end }}
+1 -1
appview/pages/templates/repo/pipelines/pipelines.html
··· 23 </p> 24 <p> 25 <span class="{{ $bullet }}">2</span>Configure your CI/CD 26 - <a href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/pipeline.md" class="underline">pipeline</a>. 27 </p> 28 <p><span class="{{ $bullet }}">3</span>Trigger a workflow with a push or a pull-request!</p> 29 </div>
··· 23 </p> 24 <p> 25 <span class="{{ $bullet }}">2</span>Configure your CI/CD 26 + <a href="https://docs.tangled.org/spindles.html#pipelines" class="underline">pipeline</a>. 27 </p> 28 <p><span class="{{ $bullet }}">3</span>Trigger a workflow with a push or a pull-request!</p> 29 </div>
+1 -1
appview/pages/templates/repo/settings/pipelines.html
··· 22 <p class="text-gray-500 dark:text-gray-400"> 23 Choose a spindle to execute your workflows on. Only repository owners 24 can configure spindles. Spindles can be selfhosted, 25 - <a class="text-gray-500 dark:text-gray-400 underline" href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md"> 26 click to learn more. 27 </a> 28 </p>
··· 22 <p class="text-gray-500 dark:text-gray-400"> 23 Choose a spindle to execute your workflows on. Only repository owners 24 can configure spindles. Spindles can be selfhosted, 25 + <a class="text-gray-500 dark:text-gray-400 underline" href="https://docs.tangled.org/spindles.html#self-hosting-guide"> 26 click to learn more. 27 </a> 28 </p>
+1 -1
appview/pages/templates/spindles/index.html
··· 102 {{ define "docsButton" }} 103 <a 104 class="btn flex items-center gap-2" 105 - href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md"> 106 {{ i "book" "size-4" }} 107 docs 108 </a>
··· 102 {{ define "docsButton" }} 103 <a 104 class="btn flex items-center gap-2" 105 + href="https://docs.tangled.org/spindles.html#self-hosting-guide"> 106 {{ i "book" "size-4" }} 107 docs 108 </a>
+1 -1
appview/pulls/opengraph.go
··· 242 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 243 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 244 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 245 - err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor) 246 if err != nil { 247 log.Printf("dolly silhouette not available (this is ok): %v", err) 248 }
··· 242 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 243 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 244 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 245 + err = dollyArea.DrawDolly(dollyX, dollyY, dollySize, dollyColor) 246 if err != nil { 247 log.Printf("dolly silhouette not available (this is ok): %v", err) 248 }
+48 -36
appview/pulls/pulls.go
··· 1241 return 1242 } 1243 1244 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1245 Collection: tangled.RepoPullNSID, 1246 Repo: user.Did, ··· 1252 Repo: string(repo.RepoAt()), 1253 Branch: targetBranch, 1254 }, 1255 - Patch: patch, 1256 Source: recordPullSource, 1257 CreatedAt: time.Now().Format(time.RFC3339), 1258 }, ··· 1328 // apply all record creations at once 1329 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1330 for _, p := range stack { 1331 record := p.AsRecord() 1332 - write := comatproto.RepoApplyWrites_Input_Writes_Elem{ 1333 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 1334 Collection: tangled.RepoPullNSID, 1335 Rkey: &p.Rkey, ··· 1337 Val: &record, 1338 }, 1339 }, 1340 - } 1341 - writes = append(writes, &write) 1342 } 1343 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 1344 Repo: user.Did, ··· 1871 return 1872 } 1873 1874 - var recordPullSource *tangled.RepoPull_Source 1875 - if pull.IsBranchBased() { 1876 - recordPullSource = &tangled.RepoPull_Source{ 1877 - Branch: pull.PullSource.Branch, 1878 - Sha: sourceRev, 1879 - } 1880 } 1881 - if pull.IsForkBased() { 1882 - repoAt := pull.PullSource.RepoAt.String() 1883 - recordPullSource = &tangled.RepoPull_Source{ 1884 - Branch: pull.PullSource.Branch, 1885 - Repo: &repoAt, 1886 - Sha: sourceRev, 1887 - } 1888 - } 1889 1890 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1891 Collection: tangled.RepoPullNSID, ··· 1893 Rkey: pull.Rkey, 1894 SwapRecord: ex.Cid, 1895 Record: &lexutil.LexiconTypeDecoder{ 1896 - Val: &tangled.RepoPull{ 1897 - Title: pull.Title, 1898 - Target: &tangled.RepoPull_Target{ 1899 - Repo: string(repo.RepoAt()), 1900 - Branch: pull.TargetBranch, 1901 - }, 1902 - Patch: patch, // new patch 1903 - Source: recordPullSource, 1904 - CreatedAt: time.Now().Format(time.RFC3339), 1905 - }, 1906 }, 1907 }) 1908 if err != nil { ··· 1988 } 1989 defer tx.Rollback() 1990 1991 // pds updates to make 1992 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1993 ··· 2021 return 2022 } 2023 2024 record := p.AsRecord() 2025 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 2026 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 2027 Collection: tangled.RepoPullNSID, ··· 2056 return 2057 } 2058 2059 record := np.AsRecord() 2060 - 2061 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 2062 RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 2063 Collection: tangled.RepoPullNSID, ··· 2091 if err != nil { 2092 log.Println("failed to resubmit pull", err) 2093 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 2094 - return 2095 - } 2096 - 2097 - client, err := s.oauth.AuthorizedClient(r) 2098 - if err != nil { 2099 - log.Println("failed to authorize client") 2100 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 2101 return 2102 } 2103
··· 1241 return 1242 } 1243 1244 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 1245 + if err != nil { 1246 + log.Println("failed to upload patch", err) 1247 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1248 + return 1249 + } 1250 + 1251 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1252 Collection: tangled.RepoPullNSID, 1253 Repo: user.Did, ··· 1259 Repo: string(repo.RepoAt()), 1260 Branch: targetBranch, 1261 }, 1262 + PatchBlob: blob.Blob, 1263 Source: recordPullSource, 1264 CreatedAt: time.Now().Format(time.RFC3339), 1265 }, ··· 1335 // apply all record creations at once 1336 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1337 for _, p := range stack { 1338 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(p.LatestPatch())) 1339 + if err != nil { 1340 + log.Println("failed to upload patch blob", err) 1341 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1342 + return 1343 + } 1344 + 1345 record := p.AsRecord() 1346 + record.PatchBlob = blob.Blob 1347 + writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1348 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 1349 Collection: tangled.RepoPullNSID, 1350 Rkey: &p.Rkey, ··· 1352 Val: &record, 1353 }, 1354 }, 1355 + }) 1356 } 1357 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 1358 Repo: user.Did, ··· 1885 return 1886 } 1887 1888 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 1889 + if err != nil { 1890 + log.Println("failed to upload patch blob", err) 1891 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1892 + return 1893 } 1894 + record := pull.AsRecord() 1895 + record.PatchBlob = blob.Blob 1896 + record.CreatedAt = time.Now().Format(time.RFC3339) 1897 1898 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1899 Collection: tangled.RepoPullNSID, ··· 1901 Rkey: pull.Rkey, 1902 SwapRecord: ex.Cid, 1903 Record: &lexutil.LexiconTypeDecoder{ 1904 + Val: &record, 1905 }, 1906 }) 1907 if err != nil { ··· 1987 } 1988 defer tx.Rollback() 1989 1990 + client, err := s.oauth.AuthorizedClient(r) 1991 + if err != nil { 1992 + log.Println("failed to authorize client") 1993 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1994 + return 1995 + } 1996 + 1997 // pds updates to make 1998 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1999 ··· 2027 return 2028 } 2029 2030 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 2031 + if err != nil { 2032 + log.Println("failed to upload patch blob", err) 2033 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 2034 + return 2035 + } 2036 record := p.AsRecord() 2037 + record.PatchBlob = blob.Blob 2038 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 2039 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 2040 Collection: tangled.RepoPullNSID, ··· 2069 return 2070 } 2071 2072 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 2073 + if err != nil { 2074 + log.Println("failed to upload patch blob", err) 2075 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 2076 + return 2077 + } 2078 record := np.AsRecord() 2079 + record.PatchBlob = blob.Blob 2080 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 2081 RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 2082 Collection: tangled.RepoPullNSID, ··· 2110 if err != nil { 2111 log.Println("failed to resubmit pull", err) 2112 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 2113 return 2114 } 2115
+1
appview/repo/archive.go
··· 18 l := rp.logger.With("handler", "DownloadArchive") 19 ref := chi.URLParam(r, "ref") 20 ref, _ = url.PathUnescape(ref) 21 f, err := rp.repoResolver.Resolve(r) 22 if err != nil { 23 l.Error("failed to get repo and knot", "err", err)
··· 18 l := rp.logger.With("handler", "DownloadArchive") 19 ref := chi.URLParam(r, "ref") 20 ref, _ = url.PathUnescape(ref) 21 + ref = strings.TrimSuffix(ref, ".tar.gz") 22 f, err := rp.repoResolver.Resolve(r) 23 if err != nil { 24 l.Error("failed to get repo and knot", "err", err)
+1 -1
appview/repo/opengraph.go
··· 237 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 238 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 239 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 240 - err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor) 241 if err != nil { 242 log.Printf("dolly silhouette not available (this is ok): %v", err) 243 }
··· 237 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 238 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 239 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 240 + err = dollyArea.DrawDolly(dollyX, dollyY, dollySize, dollyColor) 241 if err != nil { 242 log.Printf("dolly silhouette not available (this is ok): %v", err) 243 }
+26 -1
appview/reporesolver/resolver.go
··· 63 } 64 65 // get dir/ref 66 - currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath())) 67 ref := chi.URLParam(r, "ref") 68 69 repoAt := repo.RepoAt() ··· 130 } 131 132 return repoInfo 133 } 134 135 // extractPathAfterRef gets the actual repository path
··· 63 } 64 65 // get dir/ref 66 + currentDir := extractCurrentDir(r.URL.EscapedPath()) 67 ref := chi.URLParam(r, "ref") 68 69 repoAt := repo.RepoAt() ··· 130 } 131 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 "." 158 } 159 160 // extractPathAfterRef gets the actual repository path
+22
appview/reporesolver/resolver_test.go
···
··· 1 + package reporesolver 2 + 3 + import "testing" 4 + 5 + func TestExtractCurrentDir(t *testing.T) { 6 + tests := []struct { 7 + path string 8 + want string 9 + }{ 10 + {"/@user/repo/blob/main/docs/README.md", "docs"}, 11 + {"/@user/repo/blob/main/README.md", "."}, 12 + {"/@user/repo/tree/main/docs", "docs"}, 13 + {"/@user/repo/tree/main/docs/", "docs"}, 14 + {"/@user/repo/tree/main", "."}, 15 + } 16 + 17 + for _, tt := range tests { 18 + if got := extractCurrentDir(tt.path); got != tt.want { 19 + t.Errorf("extractCurrentDir(%q) = %q, want %q", tt.path, got, tt.want) 20 + } 21 + } 22 + }
-5
appview/spindles/spindles.go
··· 653 s.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 654 return 655 } 656 - if memberId.Handle.IsInvalidHandle() { 657 - l.Error("failed to resolve member identity to handle") 658 - s.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 659 - return 660 - } 661 662 tx, err := s.Db.Begin() 663 if err != nil {
··· 653 s.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 654 return 655 } 656 657 tx, err := s.Db.Begin() 658 if err != nil {
+29
appview/state/manifest.go
···
··· 1 + package state 2 + 3 + import ( 4 + "encoding/json" 5 + "net/http" 6 + ) 7 + 8 + // https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest 9 + // https://www.w3.org/TR/appmanifest/ 10 + var manifestData = map[string]any{ 11 + "name": "tangled", 12 + "description": "tightly-knit social coding.", 13 + "icons": []map[string]string{ 14 + { 15 + "src": "/static/logos/dolly.svg", 16 + "sizes": "144x144", 17 + }, 18 + }, 19 + "start_url": "/", 20 + "id": "https://tangled.org", 21 + "display": "standalone", 22 + "background_color": "#111827", 23 + "theme_color": "#111827", 24 + } 25 + 26 + func (p *State) WebAppManifest(w http.ResponseWriter, r *http.Request) { 27 + w.Header().Set("Content-Type", "application/manifest+json") 28 + json.NewEncoder(w).Encode(manifestData) 29 + }
+6 -4
appview/state/profile.go
··· 163 } 164 165 // populate commit counts in the timeline, using the punchcard 166 - currentMonth := time.Now().Month() 167 for _, p := range profile.Punchcard.Punches { 168 - idx := currentMonth - p.Date.Month() 169 - if int(idx) < len(timeline.ByMonth) { 170 - timeline.ByMonth[idx].Commits += p.Count 171 } 172 } 173
··· 163 } 164 165 // populate commit counts in the timeline, using the punchcard 166 + now := time.Now() 167 for _, p := range profile.Punchcard.Punches { 168 + years := now.Year() - p.Date.Year() 169 + months := int(now.Month() - p.Date.Month()) 170 + monthsAgo := years*12 + months 171 + if monthsAgo >= 0 && monthsAgo < len(timeline.ByMonth) { 172 + timeline.ByMonth[monthsAgo].Commits += p.Count 173 } 174 } 175
+3 -3
appview/state/router.go
··· 32 s.pages, 33 ) 34 35 - router.Get("/favicon.svg", s.Favicon) 36 - router.Get("/favicon.ico", s.Favicon) 37 - router.Get("/pwa-manifest.json", s.PWAManifest) 38 router.Get("/robots.txt", s.RobotsTxt) 39 40 userRouter := s.UserRouter(&middleware) ··· 109 }) 110 111 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 112 s.pages.Error404(w) 113 }) 114 ··· 182 r.Get("/brand", s.Brand) 183 184 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 185 s.pages.Error404(w) 186 }) 187 return r
··· 32 s.pages, 33 ) 34 35 + router.Get("/pwa-manifest.json", s.WebAppManifest) 36 router.Get("/robots.txt", s.RobotsTxt) 37 38 userRouter := s.UserRouter(&middleware) ··· 107 }) 108 109 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 110 + w.WriteHeader(http.StatusNotFound) 111 s.pages.Error404(w) 112 }) 113 ··· 181 r.Get("/brand", s.Brand) 182 183 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 184 + w.WriteHeader(http.StatusNotFound) 185 s.pages.Error404(w) 186 }) 187 return r
-36
appview/state/state.go
··· 202 return s.db.Close() 203 } 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 func (s *State) RobotsTxt(w http.ResponseWriter, r *http.Request) { 219 w.Header().Set("Content-Type", "text/plain") 220 w.Header().Set("Cache-Control", "public, max-age=86400") // one day ··· 223 Allow: / 224 ` 225 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 } 250 251 func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) {
··· 202 return s.db.Close() 203 } 204 205 func (s *State) RobotsTxt(w http.ResponseWriter, r *http.Request) { 206 w.Header().Set("Content-Type", "text/plain") 207 w.Header().Set("Cache-Control", "public, max-age=86400") // one day ··· 210 Allow: / 211 ` 212 w.Write([]byte(robotsTxt)) 213 } 214 215 func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) {
+182
cmd/dolly/main.go
···
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "flag" 6 + "fmt" 7 + "image" 8 + "image/color" 9 + "image/png" 10 + "os" 11 + "path/filepath" 12 + "strconv" 13 + "strings" 14 + "text/template" 15 + 16 + "github.com/srwiley/oksvg" 17 + "github.com/srwiley/rasterx" 18 + "golang.org/x/image/draw" 19 + "tangled.org/core/appview/pages" 20 + "tangled.org/core/ico" 21 + ) 22 + 23 + func main() { 24 + var ( 25 + size string 26 + fillColor string 27 + output string 28 + ) 29 + 30 + flag.StringVar(&size, "size", "512x512", "Output size in format WIDTHxHEIGHT (e.g., 512x512)") 31 + flag.StringVar(&fillColor, "color", "#000000", "Fill color in hex format (e.g., #FF5733)") 32 + flag.StringVar(&output, "output", "dolly.svg", "Output file path (format detected from extension: .svg, .png, or .ico)") 33 + flag.Parse() 34 + 35 + width, height, err := parseSize(size) 36 + if err != nil { 37 + fmt.Fprintf(os.Stderr, "Error parsing size: %v\n", err) 38 + os.Exit(1) 39 + } 40 + 41 + // Detect format from file extension 42 + ext := strings.ToLower(filepath.Ext(output)) 43 + format := strings.TrimPrefix(ext, ".") 44 + 45 + if format != "svg" && format != "png" && format != "ico" { 46 + fmt.Fprintf(os.Stderr, "Invalid file extension: %s. Must be .svg, .png, or .ico\n", ext) 47 + os.Exit(1) 48 + } 49 + 50 + if fillColor != "currentColor" && !isValidHexColor(fillColor) { 51 + fmt.Fprintf(os.Stderr, "Invalid color format: %s. Use hex format like #FF5733\n", fillColor) 52 + os.Exit(1) 53 + } 54 + 55 + svgData, err := dolly(fillColor) 56 + if err != nil { 57 + fmt.Fprintf(os.Stderr, "Error generating SVG: %v\n", err) 58 + os.Exit(1) 59 + } 60 + 61 + // Create output directory if it doesn't exist 62 + dir := filepath.Dir(output) 63 + if dir != "" && dir != "." { 64 + if err := os.MkdirAll(dir, 0755); err != nil { 65 + fmt.Fprintf(os.Stderr, "Error creating output directory: %v\n", err) 66 + os.Exit(1) 67 + } 68 + } 69 + 70 + switch format { 71 + case "svg": 72 + err = saveSVG(svgData, output, width, height) 73 + case "png": 74 + err = savePNG(svgData, output, width, height) 75 + case "ico": 76 + err = saveICO(svgData, output, width, height) 77 + } 78 + 79 + if err != nil { 80 + fmt.Fprintf(os.Stderr, "Error saving file: %v\n", err) 81 + os.Exit(1) 82 + } 83 + 84 + fmt.Printf("Successfully generated %s (%dx%d)\n", output, width, height) 85 + } 86 + 87 + func dolly(hexColor string) ([]byte, error) { 88 + tpl, err := template.New("dolly"). 89 + ParseFS(pages.Files, "templates/fragments/dolly/logo.html") 90 + if err != nil { 91 + return nil, err 92 + } 93 + 94 + var svgData bytes.Buffer 95 + if err := tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", pages.DollyParams{ 96 + FillColor: hexColor, 97 + }); err != nil { 98 + return nil, err 99 + } 100 + 101 + return svgData.Bytes(), nil 102 + } 103 + 104 + func svgToImage(svgData []byte, w, h int) (image.Image, error) { 105 + icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData)) 106 + if err != nil { 107 + return nil, fmt.Errorf("error parsing SVG: %v", err) 108 + } 109 + 110 + icon.SetTarget(0, 0, float64(w), float64(h)) 111 + rgba := image.NewRGBA(image.Rect(0, 0, w, h)) 112 + draw.Draw(rgba, rgba.Bounds(), &image.Uniform{color.Transparent}, image.Point{}, draw.Src) 113 + scanner := rasterx.NewScannerGV(w, h, rgba, rgba.Bounds()) 114 + raster := rasterx.NewDasher(w, h, scanner) 115 + icon.Draw(raster, 1.0) 116 + 117 + return rgba, nil 118 + } 119 + 120 + func parseSize(size string) (int, int, error) { 121 + parts := strings.Split(size, "x") 122 + if len(parts) != 2 { 123 + return 0, 0, fmt.Errorf("invalid size format, use WIDTHxHEIGHT") 124 + } 125 + 126 + width, err := strconv.Atoi(parts[0]) 127 + if err != nil { 128 + return 0, 0, fmt.Errorf("invalid width: %v", err) 129 + } 130 + 131 + height, err := strconv.Atoi(parts[1]) 132 + if err != nil { 133 + return 0, 0, fmt.Errorf("invalid height: %v", err) 134 + } 135 + 136 + if width <= 0 || height <= 0 { 137 + return 0, 0, fmt.Errorf("width and height must be positive") 138 + } 139 + 140 + return width, height, nil 141 + } 142 + 143 + func isValidHexColor(hex string) bool { 144 + if len(hex) != 7 || hex[0] != '#' { 145 + return false 146 + } 147 + _, err := strconv.ParseUint(hex[1:], 16, 32) 148 + return err == nil 149 + } 150 + 151 + func saveSVG(svgData []byte, filepath string, _, _ int) error { 152 + return os.WriteFile(filepath, svgData, 0644) 153 + } 154 + 155 + func savePNG(svgData []byte, filepath string, width, height int) error { 156 + img, err := svgToImage(svgData, width, height) 157 + if err != nil { 158 + return err 159 + } 160 + 161 + f, err := os.Create(filepath) 162 + if err != nil { 163 + return err 164 + } 165 + defer f.Close() 166 + 167 + return png.Encode(f, img) 168 + } 169 + 170 + func saveICO(svgData []byte, filepath string, width, height int) error { 171 + img, err := svgToImage(svgData, width, height) 172 + if err != nil { 173 + return err 174 + } 175 + 176 + icoData, err := ico.ImageToIco(img) 177 + if err != nil { 178 + return err 179 + } 180 + 181 + return os.WriteFile(filepath, icoData, 0644) 182 + }
+86 -89
docs/DOCS.md
··· 1 --- 2 - title: Tangled Documentation 3 author: The Tangled Contributors 4 date: 21 Sun, Dec 2025 5 - --- 6 - 7 - # Introduction 8 - 9 - Tangled is a decentralized code hosting and collaboration 10 - platform. Every component of Tangled is open-source and 11 - selfhostable. [tangled.org](https://tangled.org) also 12 - provides hosting and CI services that are free to use. 13 14 - There are several models for decentralized code 15 - collaboration platforms, ranging from ActivityPubโ€™s 16 - (Forgejo) federated model, to Radicleโ€™s entirely P2P model. 17 - Our approach attempts to be the best of both worlds by 18 - adopting atprotoโ€”a protocol for building decentralized 19 - social applications with a central identity 20 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. 28 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. 33 34 - # Quick Start Guide 35 36 - ## Login or Sign up 37 38 - You can [login](https://tangled.org) by using your AT 39 account. If you are unclear on what that means, simply head 40 to the [signup](https://tangled.org/signup) page and create 41 an account. By doing so, you will be choosing Tangled as 42 your account provider (you will be granted a handle of the 43 form `user.tngl.sh`). 44 45 - In the AT network, users are free to choose their account 46 provider (known as a "Personal Data Service", or PDS), and 47 login to applications that support AT accounts. 48 49 - You can think of it as "one account for all of the 50 - atmosphere"! 51 52 If you already have an AT account (you may have one if you 53 signed up to Bluesky, for example), you can login with the 54 same handle on Tangled (so just use `user.bsky.social` on 55 the login page). 56 57 - ## Add an SSH Key 58 59 Once you are logged in, you can start creating repositories 60 and pushing code. Tangled supports pushing git repositories ··· 87 paste your public key, give it a descriptive name, and hit 88 save. 89 90 - ## Create a Repository 91 92 Once your SSH key is added, create your first repository: 93 ··· 98 4. Choose a knotserver to host this repository on 99 5. Hit create 100 101 - "Knots" are selfhostable, lightweight git servers that can 102 host your repository. Unlike traditional code forges, your 103 code can live on any server. Read the [Knots](TODO) section 104 for more. ··· 125 are hosted by tangled.org. If you use a custom knot, refer 126 to the [Knots](TODO) section. 127 128 - ## Push Your First Repository 129 130 - Initialize a new git repository: 131 132 ```bash 133 mkdir my-project ··· 165 cd /path/to/your/existing/repo 166 ``` 167 168 - You can inspect your existing git remote like so: 169 170 ```bash 171 git remote -v ··· 197 origin git@tangled.org:user.tngl.sh/my-project (push) 198 ``` 199 200 - Push all your branches and tags to tangled: 201 202 ```bash 203 git push -u origin --all ··· 232 ``` 233 234 You also need to re-add the original URL as a push 235 - destination (git replaces the push URL when you use `--add` 236 the first time): 237 238 ```bash ··· 249 ``` 250 251 Notice that there's one fetch URL (the primary remote) and 252 - two push URLs. Now, whenever you push, git will 253 automatically push to both remotes: 254 255 ```bash ··· 301 ## Docker 302 303 Refer to 304 - [@tangled.org/knot-docker](https://tangled.sh/@tangled.sh/knot-docker). 305 Note that this is community maintained. 306 307 ## Manual setup ··· 372 ``` 373 KNOT_REPO_SCAN_PATH=/home/git 374 KNOT_SERVER_HOSTNAME=knot.example.com 375 - APPVIEW_ENDPOINT=https://tangled.sh 376 KNOT_SERVER_OWNER=did:plc:foobar 377 KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:5444 378 KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555 ··· 603 - `nixery`: This uses an instance of 604 [Nixery](https://nixery.dev) to run steps, which allows 605 you to add [dependencies](#dependencies) from 606 - [Nixpkgs](https://github.com/NixOS/nixpkgs). You can 607 search for packages on https://search.nixos.org, and 608 there's a pretty good chance the package(s) you're looking 609 for will be there. ··· 630 default, the depth is set to 1, meaning only the most 631 recent commit will be fetched, which is the commit that 632 triggered the workflow. 633 - - `submodules`: If you use [git 634 - submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) 635 in your repository, setting this field to `true` will 636 recursively fetch all submodules. This is `false` by 637 default. ··· 657 Say you want to fetch Node.js and Go from `nixpkgs`, and a 658 package called `my_pkg` you've made from your own registry 659 at your repository at 660 - `https://tangled.sh/@example.com/my_pkg`. You can define 661 those dependencies like so: 662 663 ```yaml ··· 779 780 If you want another example of a workflow, you can look at 781 the one [Tangled uses to build the 782 - project](https://tangled.sh/@tangled.sh/core/blob/master/.tangled/workflows/build.yml). 783 784 ## Self-hosting guide 785 ··· 836 837 ## Architecture 838 839 - Spindle is a small CI runner service. Here's a high level overview of how it operates: 840 841 - * listens for [`sh.tangled.spindle.member`](/lexicons/spindle/member.json) and 842 [`sh.tangled.repo`](/lexicons/repo.json) records on the Jetstream. 843 - * when a new repo record comes through (typically when you add a spindle to a 844 repo from the settings), spindle then resolves the underlying knot and 845 subscribes to repo events (see: 846 [`sh.tangled.pipeline`](/lexicons/pipeline.json)). 847 - * the spindle engine then handles execution of the pipeline, with results and 848 - logs beamed on the spindle event stream over wss 849 850 ### The engine 851 852 At present, the only supported backend is Docker (and Podman, if Docker 853 - compatibility is enabled, so that `/run/docker.sock` is created). Spindle 854 executes each step in the pipeline in a fresh container, with state persisted 855 across steps within the `/tangled/workspace` directory. 856 ··· 858 [Nixery](https://nixery.dev), which is handy for caching layers for frequently 859 used packages. 860 861 - The pipeline manifest is [specified here](/docs/spindle/pipeline.md). 862 863 ## Secrets with openbao 864 865 - This document covers setting up Spindle to use OpenBao for secrets 866 management via OpenBao Proxy instead of the default SQLite backend. 867 868 ### Overview 869 870 Spindle now uses OpenBao Proxy for secrets management. The proxy handles 871 - authentication automatically using AppRole credentials, while Spindle 872 connects to the local proxy instead of directly to the OpenBao server. 873 874 This approach provides better security, automatic token renewal, and ··· 876 877 ### Installation 878 879 - Install OpenBao from nixpkgs: 880 881 ```bash 882 nix shell nixpkgs#openbao # for a local server ··· 1029 } 1030 } 1031 1032 - # Proxy listener for Spindle 1033 listener "tcp" { 1034 address = "127.0.0.1:8201" 1035 tls_disable = true ··· 1062 1063 #### Configure spindle 1064 1065 - Set these environment variables for Spindle: 1066 1067 ```bash 1068 export SPINDLE_SERVER_SECRETS_PROVIDER=openbao ··· 1070 export SPINDLE_SERVER_SECRETS_OPENBAO_MOUNT=spindle 1071 ``` 1072 1073 - On startup, the spindle will now connect to the local proxy, 1074 which handles all authentication automatically. 1075 1076 ### Production setup for proxy ··· 1099 # List all secrets 1100 bao kv list spindle/ 1101 1102 - # Add a test secret via Spindle API, then check it exists 1103 bao kv list spindle/repos/ 1104 1105 # Get a specific secret ··· 1112 port 8200 or 8201) 1113 - The proxy authenticates with OpenBao using AppRole 1114 credentials 1115 - - All Spindle requests go through the proxy, which injects 1116 authentication tokens 1117 - Secrets are stored at 1118 `spindle/repos/{sanitized_repo_path}/{secret_key}` ··· 1131 and the policy has the necessary permissions. 1132 1133 **404 route errors**: The spindle KV mount probably doesn't 1134 - exist - run the mount creation step again. 1135 1136 **Proxy authentication failures**: Check the proxy logs and 1137 verify the role-id and secret-id files are readable and ··· 1159 secret_id="$(cat /tmp/openbao/secret-id)" 1160 ``` 1161 1162 - # Migrating knots & spindles 1163 1164 Sometimes, non-backwards compatible changes are made to the 1165 knot/spindle XRPC APIs. If you host a knot or a spindle, you ··· 1172 1173 ## Upgrading from v1.8.x 1174 1175 - After v1.8.2, the HTTP API for knot and spindles have been 1176 deprecated and replaced with XRPC. Repositories on outdated 1177 knots will not be viewable from the appview. Upgrading is 1178 straightforward however. 1179 1180 For knots: 1181 1182 - - Upgrade to latest tag (v1.9.0 or above) 1183 - Head to the [knot dashboard](https://tangled.org/settings/knots) and 1184 hit the "retry" button to verify your knot 1185 1186 For spindles: 1187 1188 - - Upgrade to latest tag (v1.9.0 or above) 1189 - Head to the [spindle 1190 dashboard](https://tangled.org/settings/spindles) and hit the 1191 "retry" button to verify your spindle ··· 1227 # Hacking on Tangled 1228 1229 We highly recommend [installing 1230 - nix](https://nixos.org/download/) (the package manager) 1231 - before working on the codebase. The nix flake provides a lot 1232 of helpers to get started and most importantly, builds and 1233 dev shells are entirely deterministic. 1234 ··· 1238 nix develop 1239 ``` 1240 1241 - Non-nix users can look at the `devShell` attribute in the 1242 `flake.nix` file to determine necessary dependencies. 1243 1244 ## Running the appview 1245 1246 - The nix flake also exposes a few `app` attributes (run `nix 1247 flake show` to see a full list of what the flake provides), 1248 one of the apps runs the appview with the `air` 1249 live-reloader: ··· 1258 nix run .#watch-tailwind 1259 ``` 1260 1261 - To authenticate with the appview, you will need redis and 1262 - OAUTH JWKs to be setup: 1263 1264 ``` 1265 - # oauth jwks should already be setup by the nix devshell: 1266 echo $TANGLED_OAUTH_CLIENT_SECRET 1267 z42ty4RT1ovnTopY8B8ekz9NuziF2CuMkZ7rbRFpAR9jBqMc 1268 ··· 1280 # the secret key from above 1281 export TANGLED_OAUTH_CLIENT_SECRET="z42tuP..." 1282 1283 - # run redis in at a new shell to store oauth sessions 1284 redis-server 1285 ``` 1286 1287 ## Running knots and spindles 1288 1289 An end-to-end knot setup requires setting up a machine with 1290 - `sshd`, `AuthorizedKeysCommand`, and git user, which is 1291 - quite cumbersome. So the nix flake provides a 1292 `nixosConfiguration` to do so. 1293 1294 <details> 1295 - <summary><strong>MacOS users will have to setup a Nix Builder first</strong></summary> 1296 1297 In order to build Tangled's dev VM on macOS, you will 1298 first need to set up a Linux Nix builder. The recommended ··· 1303 you are using Apple Silicon). 1304 1305 > IMPORTANT: You must build `darwin.linux-builder` somewhere other than inside 1306 - > the tangled repo so that it doesn't conflict with the other VM. For example, 1307 > you can do 1308 > 1309 > ```shell ··· 1316 > avoid subtle problems. 1317 1318 Alternatively, you can use any other method to set up a 1319 - Linux machine with `nix` installed that you can `sudo ssh` 1320 into (in other words, root user on your Mac has to be able 1321 to ssh into the Linux machine without entering a password) 1322 and that has the same architecture as your Mac. See ··· 1347 with `ssh` exposed on port 2222. 1348 1349 Once the services are running, head to 1350 - http://localhost:3000/settings/knots and hit verify. It should 1351 verify the ownership of the services instantly if everything 1352 went smoothly. 1353 ··· 1371 1372 The above VM should already be running a spindle on 1373 `localhost:6555`. Head to http://localhost:3000/settings/spindles and 1374 - hit verify. You can then configure each repository to use 1375 this spindle and run CI jobs. 1376 1377 Of interest when debugging spindles: 1378 1379 ``` 1380 - # service logs from journald: 1381 journalctl -xeu spindle 1382 1383 # CI job logs from disk: 1384 ls /var/log/spindle 1385 1386 - # debugging spindle db: 1387 sqlite3 /var/lib/spindle/spindle.db 1388 1389 # litecli has a nicer REPL interface: ··· 1432 1433 ### General notes 1434 1435 - - PRs get merged "as-is" (fast-forward) -- like applying a patch-series 1436 - using `git am`. At present, there is no squashing -- so please author 1437 your commits as they would appear on `master`, following the above 1438 guidelines. 1439 - If there is a lot of nesting, for example "appview: ··· 1454 ## Code formatting 1455 1456 We use a variety of tools to format our code, and multiplex them with 1457 - [`treefmt`](https://treefmt.com): all you need to do to format your changes 1458 is run `nix run .#fmt` (or just `treefmt` if you're in the devshell). 1459 1460 ## Proposals for bigger changes ··· 1482 We'll use the issue thread to discuss and refine the idea before moving 1483 forward. 1484 1485 - ## Developer certificate of origin (DCO) 1486 1487 We require all contributors to certify that they have the right to 1488 submit the code they're contributing. To do this, we follow the
··· 1 --- 2 + title: Tangled docs 3 author: The Tangled Contributors 4 date: 21 Sun, Dec 2025 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. 10 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 17 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. 25 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 + --- 31 32 + # Quick start guide 33 34 + ## Login or sign up 35 36 + You can [login](https://tangled.org) by using your AT Protocol 37 account. If you are unclear on what that means, simply head 38 to the [signup](https://tangled.org/signup) page and create 39 an account. By doing so, you will be choosing Tangled as 40 your account provider (you will be granted a handle of the 41 form `user.tngl.sh`). 42 43 + In the AT Protocol network, users are free to choose their account 44 provider (known as a "Personal Data Service", or PDS), and 45 login to applications that support AT accounts. 46 47 + You can think of it as "one account for all of the atmosphere"! 48 49 If you already have an AT account (you may have one if you 50 signed up to Bluesky, for example), you can login with the 51 same handle on Tangled (so just use `user.bsky.social` on 52 the login page). 53 54 + ## Add an SSH key 55 56 Once you are logged in, you can start creating repositories 57 and pushing code. Tangled supports pushing git repositories ··· 84 paste your public key, give it a descriptive name, and hit 85 save. 86 87 + ## Create a repository 88 89 Once your SSH key is added, create your first repository: 90 ··· 95 4. Choose a knotserver to host this repository on 96 5. Hit create 97 98 + Knots are self-hostable, lightweight Git servers that can 99 host your repository. Unlike traditional code forges, your 100 code can live on any server. Read the [Knots](TODO) section 101 for more. ··· 122 are hosted by tangled.org. If you use a custom knot, refer 123 to the [Knots](TODO) section. 124 125 + ## Push your first repository 126 127 + Initialize a new Git repository: 128 129 ```bash 130 mkdir my-project ··· 162 cd /path/to/your/existing/repo 163 ``` 164 165 + You can inspect your existing Git remote like so: 166 167 ```bash 168 git remote -v ··· 194 origin git@tangled.org:user.tngl.sh/my-project (push) 195 ``` 196 197 + Push all your branches and tags to Tangled: 198 199 ```bash 200 git push -u origin --all ··· 229 ``` 230 231 You also need to re-add the original URL as a push 232 + destination (Git replaces the push URL when you use `--add` 233 the first time): 234 235 ```bash ··· 246 ``` 247 248 Notice that there's one fetch URL (the primary remote) and 249 + two push URLs. Now, whenever you push, Git will 250 automatically push to both remotes: 251 252 ```bash ··· 298 ## Docker 299 300 Refer to 301 + [@tangled.org/knot-docker](https://tangled.org/@tangled.org/knot-docker). 302 Note that this is community maintained. 303 304 ## Manual setup ··· 369 ``` 370 KNOT_REPO_SCAN_PATH=/home/git 371 KNOT_SERVER_HOSTNAME=knot.example.com 372 + APPVIEW_ENDPOINT=https://tangled.org 373 KNOT_SERVER_OWNER=did:plc:foobar 374 KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:5444 375 KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555 ··· 600 - `nixery`: This uses an instance of 601 [Nixery](https://nixery.dev) to run steps, which allows 602 you to add [dependencies](#dependencies) from 603 + Nixpkgs (https://github.com/NixOS/nixpkgs). You can 604 search for packages on https://search.nixos.org, and 605 there's a pretty good chance the package(s) you're looking 606 for will be there. ··· 627 default, the depth is set to 1, meaning only the most 628 recent commit will be fetched, which is the commit that 629 triggered the workflow. 630 + - `submodules`: If you use Git submodules 631 + (https://git-scm.com/book/en/v2/Git-Tools-Submodules) 632 in your repository, setting this field to `true` will 633 recursively fetch all submodules. This is `false` by 634 default. ··· 654 Say you want to fetch Node.js and Go from `nixpkgs`, and a 655 package called `my_pkg` you've made from your own registry 656 at your repository at 657 + `https://tangled.org/@example.com/my_pkg`. You can define 658 those dependencies like so: 659 660 ```yaml ··· 776 777 If you want another example of a workflow, you can look at 778 the one [Tangled uses to build the 779 + project](https://tangled.org/@tangled.org/core/blob/master/.tangled/workflows/build.yml). 780 781 ## Self-hosting guide 782 ··· 833 834 ## Architecture 835 836 + Spindle is a small CI runner service. Here's a high-level overview of how it operates: 837 838 + * Listens for [`sh.tangled.spindle.member`](/lexicons/spindle/member.json) and 839 [`sh.tangled.repo`](/lexicons/repo.json) records on the Jetstream. 840 + * When a new repo record comes through (typically when you add a spindle to a 841 repo from the settings), spindle then resolves the underlying knot and 842 subscribes to repo events (see: 843 [`sh.tangled.pipeline`](/lexicons/pipeline.json)). 844 + * The spindle engine then handles execution of the pipeline, with results and 845 + logs beamed on the spindle event stream over WebSocket 846 847 ### The engine 848 849 At present, the only supported backend is Docker (and Podman, if Docker 850 + compatibility is enabled, so that `/run/docker.sock` is created). spindle 851 executes each step in the pipeline in a fresh container, with state persisted 852 across steps within the `/tangled/workspace` directory. 853 ··· 855 [Nixery](https://nixery.dev), which is handy for caching layers for frequently 856 used packages. 857 858 + The pipeline manifest is [specified here](https://docs.tangled.org/spindles.html#pipelines). 859 860 ## Secrets with openbao 861 862 + This document covers setting up spindle to use OpenBao for secrets 863 management via OpenBao Proxy instead of the default SQLite backend. 864 865 ### Overview 866 867 Spindle now uses OpenBao Proxy for secrets management. The proxy handles 868 + authentication automatically using AppRole credentials, while spindle 869 connects to the local proxy instead of directly to the OpenBao server. 870 871 This approach provides better security, automatic token renewal, and ··· 873 874 ### Installation 875 876 + Install OpenBao from Nixpkgs: 877 878 ```bash 879 nix shell nixpkgs#openbao # for a local server ··· 1026 } 1027 } 1028 1029 + # Proxy listener for spindle 1030 listener "tcp" { 1031 address = "127.0.0.1:8201" 1032 tls_disable = true ··· 1059 1060 #### Configure spindle 1061 1062 + Set these environment variables for spindle: 1063 1064 ```bash 1065 export SPINDLE_SERVER_SECRETS_PROVIDER=openbao ··· 1067 export SPINDLE_SERVER_SECRETS_OPENBAO_MOUNT=spindle 1068 ``` 1069 1070 + On startup, spindle will now connect to the local proxy, 1071 which handles all authentication automatically. 1072 1073 ### Production setup for proxy ··· 1096 # List all secrets 1097 bao kv list spindle/ 1098 1099 + # Add a test secret via the spindle API, then check it exists 1100 bao kv list spindle/repos/ 1101 1102 # Get a specific secret ··· 1109 port 8200 or 8201) 1110 - The proxy authenticates with OpenBao using AppRole 1111 credentials 1112 + - All spindle requests go through the proxy, which injects 1113 authentication tokens 1114 - Secrets are stored at 1115 `spindle/repos/{sanitized_repo_path}/{secret_key}` ··· 1128 and the policy has the necessary permissions. 1129 1130 **404 route errors**: The spindle KV mount probably doesn't 1131 + existโ€”run the mount creation step again. 1132 1133 **Proxy authentication failures**: Check the proxy logs and 1134 verify the role-id and secret-id files are readable and ··· 1156 secret_id="$(cat /tmp/openbao/secret-id)" 1157 ``` 1158 1159 + # Migrating knots and spindles 1160 1161 Sometimes, non-backwards compatible changes are made to the 1162 knot/spindle XRPC APIs. If you host a knot or a spindle, you ··· 1169 1170 ## Upgrading from v1.8.x 1171 1172 + After v1.8.2, the HTTP API for knots and spindles has been 1173 deprecated and replaced with XRPC. Repositories on outdated 1174 knots will not be viewable from the appview. Upgrading is 1175 straightforward however. 1176 1177 For knots: 1178 1179 + - Upgrade to the latest tag (v1.9.0 or above) 1180 - Head to the [knot dashboard](https://tangled.org/settings/knots) and 1181 hit the "retry" button to verify your knot 1182 1183 For spindles: 1184 1185 + - Upgrade to the latest tag (v1.9.0 or above) 1186 - Head to the [spindle 1187 dashboard](https://tangled.org/settings/spindles) and hit the 1188 "retry" button to verify your spindle ··· 1224 # Hacking on Tangled 1225 1226 We highly recommend [installing 1227 + Nix](https://nixos.org/download/) (the package manager) 1228 + before working on the codebase. The Nix flake provides a lot 1229 of helpers to get started and most importantly, builds and 1230 dev shells are entirely deterministic. 1231 ··· 1235 nix develop 1236 ``` 1237 1238 + Non-Nix users can look at the `devShell` attribute in the 1239 `flake.nix` file to determine necessary dependencies. 1240 1241 ## Running the appview 1242 1243 + The Nix flake also exposes a few `app` attributes (run `nix 1244 flake show` to see a full list of what the flake provides), 1245 one of the apps runs the appview with the `air` 1246 live-reloader: ··· 1255 nix run .#watch-tailwind 1256 ``` 1257 1258 + To authenticate with the appview, you will need Redis and 1259 + OAuth JWKs to be set up: 1260 1261 ``` 1262 + # OAuth JWKs should already be set up by the Nix devshell: 1263 echo $TANGLED_OAUTH_CLIENT_SECRET 1264 z42ty4RT1ovnTopY8B8ekz9NuziF2CuMkZ7rbRFpAR9jBqMc 1265 ··· 1277 # the secret key from above 1278 export TANGLED_OAUTH_CLIENT_SECRET="z42tuP..." 1279 1280 + # Run Redis in a new shell to store OAuth sessions 1281 redis-server 1282 ``` 1283 1284 ## Running knots and spindles 1285 1286 An end-to-end knot setup requires setting up a machine with 1287 + `sshd`, `AuthorizedKeysCommand`, and a Git user, which is 1288 + quite cumbersome. So the Nix flake provides a 1289 `nixosConfiguration` to do so. 1290 1291 <details> 1292 + <summary><strong>macOS users will have to set up a Nix Builder first</strong></summary> 1293 1294 In order to build Tangled's dev VM on macOS, you will 1295 first need to set up a Linux Nix builder. The recommended ··· 1300 you are using Apple Silicon). 1301 1302 > IMPORTANT: You must build `darwin.linux-builder` somewhere other than inside 1303 + > the Tangled repo so that it doesn't conflict with the other VM. For example, 1304 > you can do 1305 > 1306 > ```shell ··· 1313 > avoid subtle problems. 1314 1315 Alternatively, you can use any other method to set up a 1316 + Linux machine with Nix installed that you can `sudo ssh` 1317 into (in other words, root user on your Mac has to be able 1318 to ssh into the Linux machine without entering a password) 1319 and that has the same architecture as your Mac. See ··· 1344 with `ssh` exposed on port 2222. 1345 1346 Once the services are running, head to 1347 + http://localhost:3000/settings/knots and hit "Verify". It should 1348 verify the ownership of the services instantly if everything 1349 went smoothly. 1350 ··· 1368 1369 The above VM should already be running a spindle on 1370 `localhost:6555`. Head to http://localhost:3000/settings/spindles and 1371 + hit "Verify". You can then configure each repository to use 1372 this spindle and run CI jobs. 1373 1374 Of interest when debugging spindles: 1375 1376 ``` 1377 + # Service logs from journald: 1378 journalctl -xeu spindle 1379 1380 # CI job logs from disk: 1381 ls /var/log/spindle 1382 1383 + # Debugging spindle database: 1384 sqlite3 /var/lib/spindle/spindle.db 1385 1386 # litecli has a nicer REPL interface: ··· 1429 1430 ### General notes 1431 1432 + - PRs get merged "as-is" (fast-forward)โ€”like applying a patch-series 1433 + using `git am`. At present, there is no squashingโ€”so please author 1434 your commits as they would appear on `master`, following the above 1435 guidelines. 1436 - If there is a lot of nesting, for example "appview: ··· 1451 ## Code formatting 1452 1453 We use a variety of tools to format our code, and multiplex them with 1454 + [`treefmt`](https://treefmt.com). All you need to do to format your changes 1455 is run `nix run .#fmt` (or just `treefmt` if you're in the devshell). 1456 1457 ## Proposals for bigger changes ··· 1479 We'll use the issue thread to discuss and refine the idea before moving 1480 forward. 1481 1482 + ## Developer Certificate of Origin (DCO) 1483 1484 We require all contributors to certify that they have the right to 1485 submit the code they're contributing. To do this, we follow the
+6
docs/logo.html
···
··· 1 + <div class="flex items-center gap-2 w-fit mx-auto"> 2 + <span class="w-16 h-16 [&>svg]:w-full [&>svg]:h-full text-black dark:text-white"> 3 + ${ dolly.svg() } 4 + </span> 5 + <span class="font-bold text-4xl not-italic text-black dark:text-white">tangled</span> 6 + </div>
+3
docs/mode.html
···
··· 1 + <a class="px-4 py-2 mt-8 block text-center w-full rounded-sm shadow-sm border border-gray-200 dark:border-gray-700 no-underline hover:no-underline" href="$if(single-page)$/$else$/single-page.html$endif$"> 2 + $if(single-page)$View as multi-page$else$View as single-page$endif$ 3 + </a>
+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>
+79 -39
docs/template.html
··· 20 <meta name="description" content="$description-meta$" /> 21 $endif$ 22 23 - <title>$pagetitle$ - Tangled docs</title> 24 25 <style> 26 $styles.css()$ ··· 37 <link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin /> 38 39 </head> 40 - <body class="bg-white dark:bg-gray-900 min-h-screen"> 41 $for(include-before)$ 42 $include-before$ 43 $endfor$ 44 $if(toc)$ 45 - <!-- mobile topbar toc --> 46 - <details id="mobile-$idprefix$TOC" role="doc-toc" class="md:hidden bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 z-50 space-y-4 group px-6 py-4"> 47 - <summary class="cursor-pointer list-none text-sm font-semibold select-none flex gap-2 justify-between items-center dark:text-white"> 48 - $if(toc-title)$$toc-title$$else$Table of Contents$endif$ 49 - <span class="group-open:hidden inline">${ menu.svg() }</span> 50 - <span class="hidden group-open:inline">${ x.svg() }</span> 51 - </summary> 52 - ${ table-of-contents:toc.html() } 53 - </details> 54 55 - <!-- desktop sidebar toc --> 56 - <nav id="$idprefix$TOC" role="doc-toc" class="hidden md:block fixed left-0 top-0 w-80 h-screen bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 overflow-y-auto p-4 z-50"> 57 - $if(toc-title)$ 58 - <h2 id="$idprefix$toc-title" class="text-lg font-semibold mb-4 text-gray-900">$toc-title$</h2> 59 - $endif$ 60 - ${ table-of-contents:toc.html() } 61 - </nav> 62 $endif$ 63 64 - <div class="min-h-screen flex-1 flex flex-col $if(toc)$md:ml-80$endif$"> 65 - <main class="flex-1 max-w-4xl w-full mx-auto p-6"> 66 $if(top)$ 67 - $-- only print title block if this is NOT the top page 68 $else$ 69 $if(title)$ 70 - <header id="title-block-header" class="mb-8 pb-8 border-b border-gray-200 dark:border-gray-700"> 71 - <h1 class="text-4xl font-bold mb-2 text-black dark:text-white">$title$</h1> 72 - $if(subtitle)$ 73 - <p class="text-xl text-gray-500 dark:text-gray-400 mb-2">$subtitle$</p> 74 - $endif$ 75 - $for(author)$ 76 - <p class="text-sm text-gray-500 dark:text-gray-400">$author$</p> 77 - $endfor$ 78 - $if(date)$ 79 - <p class="text-sm text-gray-500 dark:text-gray-400">$date$</p> 80 - $endif$ 81 $if(abstract)$ 82 - <div class="mt-6 p-4 bg-gray-50 rounded-lg"> 83 - <div class="text-sm font-semibold text-gray-700 uppercase mb-2">$abstract-title$</div> 84 - <div class="text-gray-700">$abstract$</div> 85 - </div> 86 - $endif$ 87 $endif$ 88 - </header> 89 $endif$ 90 <article class="prose dark:prose-invert max-w-none"> 91 $body$ 92 </article> 93 </main> 94 - <nav id="sitenav" class="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 "> 95 <div class="max-w-4xl mx-auto px-8 py-4"> 96 <div class="flex justify-between gap-4"> 97 <span class="flex-1">
··· 20 <meta name="description" content="$description-meta$" /> 21 $endif$ 22 23 + <title>$pagetitle$</title> 24 25 <style> 26 $styles.css()$ ··· 37 <link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin /> 38 39 </head> 40 + <body class="bg-white dark:bg-gray-900 flex flex-col min-h-svh"> 41 $for(include-before)$ 42 $include-before$ 43 $endfor$ 44 + 45 $if(toc)$ 46 + <!-- mobile TOC trigger --> 47 + <div class="md:hidden px-6 py-4 border-b border-gray-200 dark:border-gray-700"> 48 + <button 49 + type="button" 50 + popovertarget="mobile-toc-popover" 51 + popovertargetaction="toggle" 52 + class="w-full flex gap-2 items-center text-sm font-semibold dark:text-white" 53 + > 54 + ${ menu.svg() } 55 + $if(toc-title)$$toc-title$$else$Table of Contents$endif$ 56 + </button> 57 + </div> 58 + 59 + <div 60 + id="mobile-toc-popover" 61 + popover 62 + class="mobile-toc-popover 63 + bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 64 + h-full overflow-y-auto shadow-sm 65 + px-6 py-4 fixed inset-x-0 top-0 w-fit max-w-4/5 m-0" 66 + > 67 + <div class="flex flex-col min-h-full"> 68 + <div class="flex-1 space-y-4"> 69 + <button 70 + type="button" 71 + popovertarget="mobile-toc-popover" 72 + popovertargetaction="toggle" 73 + class="w-full flex gap-2 items-center text-sm font-semibold dark:text-white mb-4"> 74 + ${ x.svg() } 75 + $if(toc-title)$$toc-title$$else$Table of Contents$endif$ 76 + </button> 77 + ${ logo.html() } 78 + ${ search.html() } 79 + ${ table-of-contents:toc.html() } 80 + </div> 81 + ${ single-page:mode.html() } 82 + </div> 83 + </div> 84 85 + <!-- desktop sidebar toc --> 86 + <nav 87 + id="$idprefix$TOC" 88 + role="doc-toc" 89 + class="hidden md:flex md:flex-col gap-4 fixed left-0 top-0 w-80 h-screen 90 + bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 91 + p-4 z-50 overflow-y-auto"> 92 + ${ logo.html() } 93 + ${ search.html() } 94 + <div class="flex-1"> 95 + $if(toc-title)$ 96 + <h2 id="$idprefix$toc-title" class="text-lg font-semibold mb-4 text-gray-900">$toc-title$</h2> 97 + $endif$ 98 + ${ table-of-contents:toc.html() } 99 + </div> 100 + ${ single-page:mode.html() } 101 + </nav> 102 $endif$ 103 104 + <div class="$if(toc)$md:ml-80$endif$ flex-1 flex flex-col"> 105 + <main class="max-w-4xl w-full mx-auto p-6 flex-1"> 106 $if(top)$ 107 + $-- only print title block if this is NOT the top page 108 $else$ 109 $if(title)$ 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 $if(abstract)$ 124 + <article class="prose dark:prose-invert max-w-none"> 125 + $abstract$ 126 + </article> 127 $endif$ 128 $endif$ 129 + 130 <article class="prose dark:prose-invert max-w-none"> 131 $body$ 132 </article> 133 </main> 134 + <nav id="sitenav" class="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"> 135 <div class="max-w-4xl mx-auto px-8 py-4"> 136 <div class="flex justify-between gap-4"> 137 <span class="flex-1">
+18 -3
flake.nix
··· 76 }; 77 buildGoApplication = 78 (self.callPackage "${gomod2nix}/builder" { 79 - gomod2nix = gomod2nix.legacyPackages.${pkgs.system}.gomod2nix; 80 }).buildGoApplication; 81 modules = ./nix/gomod2nix.toml; 82 sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix { ··· 94 spindle = self.callPackage ./nix/pkgs/spindle.nix {}; 95 knot-unwrapped = self.callPackage ./nix/pkgs/knot-unwrapped.nix {}; 96 knot = self.callPackage ./nix/pkgs/knot.nix {}; 97 }); 98 in { 99 overlays.default = final: prev: { 100 - inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs; 101 }; 102 103 packages = forAllSystems (system: let ··· 106 staticPackages = mkPackageSet pkgs.pkgsStatic; 107 crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic; 108 in { 109 - inherit (packages) appview appview-static-files lexgen goat spindle knot knot-unwrapped sqlite-lib docs; 110 111 pkgsStatic-appview = staticPackages.appview; 112 pkgsStatic-knot = staticPackages.knot; 113 pkgsStatic-knot-unwrapped = staticPackages.knot-unwrapped; 114 pkgsStatic-spindle = staticPackages.spindle; 115 pkgsStatic-sqlite-lib = staticPackages.sqlite-lib; 116 117 pkgsCross-gnu64-pkgsStatic-appview = crossPackages.appview; 118 pkgsCross-gnu64-pkgsStatic-knot = crossPackages.knot; 119 pkgsCross-gnu64-pkgsStatic-knot-unwrapped = crossPackages.knot-unwrapped; 120 pkgsCross-gnu64-pkgsStatic-spindle = crossPackages.spindle; 121 122 treefmt-wrapper = pkgs.treefmt.withConfig { 123 settings.formatter = {
··· 76 }; 77 buildGoApplication = 78 (self.callPackage "${gomod2nix}/builder" { 79 + gomod2nix = gomod2nix.legacyPackages.${pkgs.stdenv.hostPlatform.system}.gomod2nix; 80 }).buildGoApplication; 81 modules = ./nix/gomod2nix.toml; 82 sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix { ··· 94 spindle = self.callPackage ./nix/pkgs/spindle.nix {}; 95 knot-unwrapped = self.callPackage ./nix/pkgs/knot-unwrapped.nix {}; 96 knot = self.callPackage ./nix/pkgs/knot.nix {}; 97 + dolly = self.callPackage ./nix/pkgs/dolly.nix {}; 98 }); 99 in { 100 overlays.default = final: prev: { 101 + inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs dolly; 102 }; 103 104 packages = forAllSystems (system: let ··· 107 staticPackages = mkPackageSet pkgs.pkgsStatic; 108 crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic; 109 in { 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 + ; 123 124 pkgsStatic-appview = staticPackages.appview; 125 pkgsStatic-knot = staticPackages.knot; 126 pkgsStatic-knot-unwrapped = staticPackages.knot-unwrapped; 127 pkgsStatic-spindle = staticPackages.spindle; 128 pkgsStatic-sqlite-lib = staticPackages.sqlite-lib; 129 + pkgsStatic-dolly = staticPackages.dolly; 130 131 pkgsCross-gnu64-pkgsStatic-appview = crossPackages.appview; 132 pkgsCross-gnu64-pkgsStatic-knot = crossPackages.knot; 133 pkgsCross-gnu64-pkgsStatic-knot-unwrapped = crossPackages.knot-unwrapped; 134 pkgsCross-gnu64-pkgsStatic-spindle = crossPackages.spindle; 135 + pkgsCross-gnu64-pkgsStatic-dolly = crossPackages.dolly; 136 137 treefmt-wrapper = pkgs.treefmt.withConfig { 138 settings.formatter = {
+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 + }
+1
input.css
··· 255 @apply py-1 text-gray-900 dark:text-gray-100; 256 } 257 } 258 } 259 260 /* Background */
··· 255 @apply py-1 text-gray-900 dark:text-gray-100; 256 } 257 } 258 + 259 } 260 261 /* Background */
+10 -2
lexicons/pulls/pull.json
··· 12 "required": [ 13 "target", 14 "title", 15 - "patch", 16 "createdAt" 17 ], 18 "properties": { ··· 27 "type": "string" 28 }, 29 "patch": { 30 - "type": "string" 31 }, 32 "source": { 33 "type": "ref",
··· 12 "required": [ 13 "target", 14 "title", 15 + "patchBlob", 16 "createdAt" 17 ], 18 "properties": { ··· 27 "type": "string" 28 }, 29 "patch": { 30 + "type": "string", 31 + "description": "(deprecated) use patchBlob instead" 32 + }, 33 + "patchBlob": { 34 + "type": "blob", 35 + "accept": [ 36 + "text/x-patch" 37 + ], 38 + "description": "patch content" 39 }, 40 "source": { 41 "type": "ref",
+6 -1
nix/pkgs/appview-static-files.nix
··· 8 actor-typeahead-src, 9 sqlite-lib, 10 tailwindcss, 11 src, 12 }: 13 runCommandLocal "appview-static-files" { ··· 17 (allow file-read* (subpath "/System/Library/OpenSSL")) 18 ''; 19 } '' 20 - mkdir -p $out/{fonts,icons} && cd $out 21 cp -f ${htmx-src} htmx.min.js 22 cp -f ${htmx-ws-src} htmx-ext-ws.min.js 23 cp -rf ${lucide-src}/*.svg icons/ ··· 26 cp -f ${inter-fonts-src}/InterVariable*.ttf fonts/ 27 cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono*.woff2 fonts/ 28 cp -f ${actor-typeahead-src}/actor-typeahead.js . 29 # tailwindcss -c $src/tailwind.config.js -i $src/input.css -o tw.css won't work 30 # for whatever reason (produces broken css), so we are doing this instead 31 cd ${src} && ${tailwindcss}/bin/tailwindcss -i input.css -o $out/tw.css
··· 8 actor-typeahead-src, 9 sqlite-lib, 10 tailwindcss, 11 + dolly, 12 src, 13 }: 14 runCommandLocal "appview-static-files" { ··· 18 (allow file-read* (subpath "/System/Library/OpenSSL")) 19 ''; 20 } '' 21 + mkdir -p $out/{fonts,icons,logos} && cd $out 22 cp -f ${htmx-src} htmx.min.js 23 cp -f ${htmx-ws-src} htmx-ext-ws.min.js 24 cp -rf ${lucide-src}/*.svg icons/ ··· 27 cp -f ${inter-fonts-src}/InterVariable*.ttf fonts/ 28 cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono*.woff2 fonts/ 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 34 # tailwindcss -c $src/tailwind.config.js -i $src/input.css -o tw.css won't work 35 # for whatever reason (produces broken css), so we are doing this instead 36 cd ${src} && ${tailwindcss}/bin/tailwindcss -i input.css -o $out/tw.css
+17 -1
nix/pkgs/docs.nix
··· 5 inter-fonts-src, 6 ibm-plex-mono-src, 7 lucide-src, 8 src, 9 }: 10 runCommandLocal "docs" {} '' ··· 18 # icons 19 cp -rf ${lucide-src}/*.svg working/ 20 21 - # content 22 ${pandoc}/bin/pandoc ${src}/docs/DOCS.md \ 23 -o $out/ \ 24 -t chunkedhtml \ 25 --variable toc \ 26 --toc-depth=2 \ 27 --css=stylesheet.css \ 28 --chunk-template="%i.html" \ 29 --highlight-style=working/highlight.theme \ 30 --template=working/template.html 31
··· 5 inter-fonts-src, 6 ibm-plex-mono-src, 7 lucide-src, 8 + dolly, 9 src, 10 }: 11 runCommandLocal "docs" {} '' ··· 19 # icons 20 cp -rf ${lucide-src}/*.svg working/ 21 22 + # logo 23 + ${dolly}/bin/dolly -output working/dolly.svg -color currentColor 24 + 25 + # content - chunked 26 ${pandoc}/bin/pandoc ${src}/docs/DOCS.md \ 27 -o $out/ \ 28 -t chunkedhtml \ 29 --variable toc \ 30 + --variable-json single-page=false \ 31 --toc-depth=2 \ 32 --css=stylesheet.css \ 33 --chunk-template="%i.html" \ 34 + --highlight-style=working/highlight.theme \ 35 + --template=working/template.html 36 + 37 + # content - single page 38 + ${pandoc}/bin/pandoc ${src}/docs/DOCS.md \ 39 + -o $out/single-page.html \ 40 + --toc \ 41 + --variable toc \ 42 + --variable single-page \ 43 + --toc-depth=2 \ 44 + --css=stylesheet.css \ 45 --highlight-style=working/highlight.theme \ 46 --template=working/template.html 47
+21
nix/pkgs/dolly.nix
···
··· 1 + { 2 + buildGoApplication, 3 + modules, 4 + src, 5 + }: 6 + buildGoApplication { 7 + pname = "dolly"; 8 + version = "0.1.0"; 9 + inherit src modules; 10 + 11 + # patch the static dir 12 + postUnpack = '' 13 + pushd source 14 + mkdir -p appview/pages/static 15 + touch appview/pages/static/x 16 + popd 17 + ''; 18 + 19 + doCheck = false; 20 + subPackages = ["cmd/dolly"]; 21 + }
+1 -1
nix/vm.nix
··· 8 var = builtins.getEnv name; 9 in 10 if var == "" 11 - then throw "\$${name} must be defined, see docs/hacking.md for more details" 12 else var; 13 envVarOr = name: default: let 14 var = builtins.getEnv name;
··· 8 var = builtins.getEnv name; 9 in 10 if var == "" 11 + then throw "\$${name} must be defined, see https://docs.tangled.org/hacking-on-tangled.html#hacking-on-tangled for more details" 12 else var; 13 envVarOr = name: default: let 14 var = builtins.getEnv name;
+3 -3
readme.md
··· 10 11 ## docs 12 13 - * [knot hosting guide](/docs/knot-hosting.md) 14 - * [contributing guide](/docs/contributing.md) **please read before opening a PR!** 15 - * [hacking on tangled](/docs/hacking.md) 16 17 ## security 18
··· 10 11 ## docs 12 13 + - [knot hosting guide](https://docs.tangled.org/knot-self-hosting-guide.html#knot-self-hosting-guide) 14 + - [contributing guide](https://docs.tangled.org/contribution-guide.html#contribution-guide) **please read before opening a PR!** 15 + - [hacking on tangled](https://docs.tangled.org/hacking-on-tangled.html#hacking-on-tangled) 16 17 ## security 18
+1 -1
spindle/motd
··· 20 ** 21 ******** 22 23 - This is a spindle server. More info at https://tangled.sh/@tangled.sh/core/tree/master/docs/spindle 24 25 Most API routes are under /xrpc/
··· 20 ** 21 ******** 22 23 + This is a spindle server. More info at https://docs.tangled.org/spindles.html#spindles 24 25 Most API routes are under /xrpc/
+31 -13
spindle/server.go
··· 8 "log/slog" 9 "maps" 10 "net/http" 11 12 "github.com/go-chi/chi/v5" 13 "tangled.org/core/api/tangled" ··· 30 ) 31 32 //go:embed motd 33 - var motd []byte 34 35 const ( 36 rbacDomain = "thisserver" 37 ) 38 39 type Spindle struct { 40 - jc *jetstream.JetstreamClient 41 - db *db.DB 42 - e *rbac.Enforcer 43 - l *slog.Logger 44 - n *notifier.Notifier 45 - engs map[string]models.Engine 46 - jq *queue.Queue 47 - cfg *config.Config 48 - ks *eventconsumer.Consumer 49 - res *idresolver.Resolver 50 - vault secrets.Manager 51 } 52 53 // New creates a new Spindle server with the provided configuration and engines. ··· 128 cfg: cfg, 129 res: resolver, 130 vault: vault, 131 } 132 133 err = e.AddSpindle(rbacDomain) ··· 201 return s.e 202 } 203 204 // Start starts the Spindle server (blocking). 205 func (s *Spindle) Start(ctx context.Context) error { 206 // starts a job queue runner in the background ··· 246 mux := chi.NewRouter() 247 248 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 249 - w.Write(motd) 250 }) 251 mux.HandleFunc("/events", s.Events) 252 mux.HandleFunc("/logs/{knot}/{rkey}/{name}", s.Logs)
··· 8 "log/slog" 9 "maps" 10 "net/http" 11 + "sync" 12 13 "github.com/go-chi/chi/v5" 14 "tangled.org/core/api/tangled" ··· 31 ) 32 33 //go:embed motd 34 + var defaultMotd []byte 35 36 const ( 37 rbacDomain = "thisserver" 38 ) 39 40 type Spindle struct { 41 + jc *jetstream.JetstreamClient 42 + db *db.DB 43 + e *rbac.Enforcer 44 + l *slog.Logger 45 + n *notifier.Notifier 46 + engs map[string]models.Engine 47 + jq *queue.Queue 48 + cfg *config.Config 49 + ks *eventconsumer.Consumer 50 + res *idresolver.Resolver 51 + vault secrets.Manager 52 + motd []byte 53 + motdMu sync.RWMutex 54 } 55 56 // New creates a new Spindle server with the provided configuration and engines. ··· 131 cfg: cfg, 132 res: resolver, 133 vault: vault, 134 + motd: defaultMotd, 135 } 136 137 err = e.AddSpindle(rbacDomain) ··· 205 return s.e 206 } 207 208 + // SetMotdContent sets custom MOTD content, replacing the embedded default. 209 + func (s *Spindle) SetMotdContent(content []byte) { 210 + s.motdMu.Lock() 211 + defer s.motdMu.Unlock() 212 + s.motd = content 213 + } 214 + 215 + // GetMotdContent returns the current MOTD content. 216 + func (s *Spindle) GetMotdContent() []byte { 217 + s.motdMu.RLock() 218 + defer s.motdMu.RUnlock() 219 + return s.motd 220 + } 221 + 222 // Start starts the Spindle server (blocking). 223 func (s *Spindle) Start(ctx context.Context) error { 224 // starts a job queue runner in the background ··· 264 mux := chi.NewRouter() 265 266 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 267 + w.Write(s.GetMotdContent()) 268 }) 269 mux.HandleFunc("/events", s.Events) 270 mux.HandleFunc("/logs/{knot}/{rkey}/{name}", s.Logs)
+3
types/diff.go
··· 74 75 // used by html elements as a unique ID for hrefs 76 func (d *Diff) Id() string { 77 return d.Name.New 78 } 79
··· 74 75 // used by html elements as a unique ID for hrefs 76 func (d *Diff) Id() string { 77 + if d.IsDelete { 78 + return d.Name.Old 79 + } 80 return d.Name.New 81 } 82
+112
types/diff_test.go
···
··· 1 + package types 2 + 3 + import "testing" 4 + 5 + func TestDiffId(t *testing.T) { 6 + tests := []struct { 7 + name string 8 + diff Diff 9 + expected string 10 + }{ 11 + { 12 + name: "regular file uses new name", 13 + diff: Diff{ 14 + Name: struct { 15 + Old string `json:"old"` 16 + New string `json:"new"` 17 + }{Old: "", New: "src/main.go"}, 18 + }, 19 + expected: "src/main.go", 20 + }, 21 + { 22 + name: "new file uses new name", 23 + diff: Diff{ 24 + Name: struct { 25 + Old string `json:"old"` 26 + New string `json:"new"` 27 + }{Old: "", New: "src/new.go"}, 28 + IsNew: true, 29 + }, 30 + expected: "src/new.go", 31 + }, 32 + { 33 + name: "deleted file uses old name", 34 + diff: Diff{ 35 + Name: struct { 36 + Old string `json:"old"` 37 + New string `json:"new"` 38 + }{Old: "src/deleted.go", New: ""}, 39 + IsDelete: true, 40 + }, 41 + expected: "src/deleted.go", 42 + }, 43 + { 44 + name: "renamed file uses new name", 45 + diff: Diff{ 46 + Name: struct { 47 + Old string `json:"old"` 48 + New string `json:"new"` 49 + }{Old: "src/old.go", New: "src/renamed.go"}, 50 + IsRename: true, 51 + }, 52 + expected: "src/renamed.go", 53 + }, 54 + } 55 + 56 + for _, tt := range tests { 57 + t.Run(tt.name, func(t *testing.T) { 58 + if got := tt.diff.Id(); got != tt.expected { 59 + t.Errorf("Diff.Id() = %q, want %q", got, tt.expected) 60 + } 61 + }) 62 + } 63 + } 64 + 65 + func TestChangedFilesMatchesDiffId(t *testing.T) { 66 + // ChangedFiles() must return values matching each Diff's Id() 67 + // so that sidebar links point to the correct anchors. 68 + // Tests existing, deleted, new, and renamed files. 69 + nd := NiceDiff{ 70 + Diff: []Diff{ 71 + { 72 + Name: struct { 73 + Old string `json:"old"` 74 + New string `json:"new"` 75 + }{Old: "", New: "src/modified.go"}, 76 + }, 77 + { 78 + Name: struct { 79 + Old string `json:"old"` 80 + New string `json:"new"` 81 + }{Old: "src/deleted.go", New: ""}, 82 + IsDelete: true, 83 + }, 84 + { 85 + Name: struct { 86 + Old string `json:"old"` 87 + New string `json:"new"` 88 + }{Old: "", New: "src/new.go"}, 89 + IsNew: true, 90 + }, 91 + { 92 + Name: struct { 93 + Old string `json:"old"` 94 + New string `json:"new"` 95 + }{Old: "src/old.go", New: "src/renamed.go"}, 96 + IsRename: true, 97 + }, 98 + }, 99 + } 100 + 101 + changedFiles := nd.ChangedFiles() 102 + 103 + if len(changedFiles) != len(nd.Diff) { 104 + t.Fatalf("ChangedFiles() returned %d items, want %d", len(changedFiles), len(nd.Diff)) 105 + } 106 + 107 + for i, diff := range nd.Diff { 108 + if changedFiles[i] != diff.Id() { 109 + t.Errorf("ChangedFiles()[%d] = %q, but Diff.Id() = %q", i, changedFiles[i], diff.Id()) 110 + } 111 + } 112 + }