arche / commit

commit af02ba0a9e08a8111d36295a54915f9d691017bb0e5062cf2e49be68bc75a64a
change qkeryphf
author dewn <dewn5228@proton.me>
committer dewn <dewn5228@proton.me>
date 2026-03-11 17:35:43
phase public
parents 154431fd
signature Unsigned
fix log DAG walk from bookmarks; render README on repo home
internal/archesrv/handlers_repo.go [M]
--- a/internal/archesrv/handlers_repo.go
+++ b/internal/archesrv/handlers_repo.go
@@ -1,779 +1,894 @@
 package archesrv
 
 import (
 	"bytes"
 	"encoding/hex"
 	"fmt"
 	"html/template"
 	"net/http"
 	"sort"
 	"strings"
 
 	"arche/internal/diff"
 	"arche/internal/
+markdown"
+	"arche/internal/
 object"
 	"arche/internal/repo"
 	"arche/internal/revset"
 	"arche/internal/syncpkg"
 
 	"github.com/alecthomas/chroma/v2"
 	chrhtml "github.com/alecthomas/chroma/v2/formatters/html"
 	"github.com/alecthomas/chroma/v2/lexers"
 	"github.com/alecthomas/chroma/v2/styles"
 	"golang.org/x/crypto/ssh"
 )
 
 func (s *forgeServer) requireRepoAccess(w http.ResponseWriter, r *http.Request) (*repo.Repo, *RepoRecord, bool) {
 	repoName := r.PathValue("repo")
 	rec, err := s.db.GetRepo(repoName)
 	if err != nil || rec == nil {
 		http.NotFound(w, r)
 		return nil, nil, false
 	}
 
 	user := s.db.currentUser(r)
 	if !s.db.CanRead(rec, user) {
 		http.Error(w, "Unauthorized", http.StatusUnauthorized)
 		return nil, nil, false
 	}
 
 	repoObj, err := openRepo(s.dataDir(), repoName)
 	if err != nil {
 		http.Error(w, "open repo: "+err.Error(), http.StatusInternalServerError)
 		return nil, nil, false
 	}
 
 	return repoObj, rec, true
 }
 
 func (s *forgeServer) handleSyncProxy(w http.ResponseWriter, r *http.Request) {
 	repoName := r.PathValue("repo")
 	rec, err := s.db.GetRepo(repoName)
 	if err != nil || rec == nil {
 		http.Error(w, "repo not found", http.StatusNotFound)
 		return
 	}
 
 	user := s.db.currentUser(r)
 
 	if r.Method != http.MethodGet && !s.db.CanWrite(rec, user) {
 		user := s.db.currentUser(r)
 		username := "anonymous"
 		if user != nil {
 			username = user.Username
 		}
 		s.log.Warn("sync write denied", "repo", repoName, "user", username)
 		http.Error(w, "Unauthorized", http.StatusUnauthorized)
 		return
 	}
 	if r.Method == http.MethodGet && !s.db.CanRead(rec, user) {
 		s.log.Warn("sync read denied", "repo", repoName)
 		http.Error(w, "Unauthorized", http.StatusUnauthorized)
 		return
 	}
 
 	repoObj, err := openRepo(s.dataDir(), repoName)
 	if err != nil {
 		http.Error(w, "open repo: "+err.Error(), http.StatusInternalServerError)
 		return
 	}
 	defer repoObj.Close()
 
 	action := strings.TrimPrefix(r.URL.Path, "/"+repoName)
 	r2 := r.Clone(r.Context())
 	r2.URL.Path = action
 
 	user = s.db.currentUser(r)
 	pusher := "anonymous"
 	if user != nil {
 		pusher = user.Username
 	}
 
 	srv := syncpkg.NewServer(repoObj, "")
 
 	repoKey := repoName
 	repoCfg := s.cfg.Repo[repoKey]
 	srv.PreUpdateHook = func(bm, oldHex, newHex string) error {
 		if s.cfg.Hooks.PreReceive != "" || s.cfg.Hooks.Update != "" {
 			if err := runPreReceiveHook(s.cfg.Hooks.PreReceive, bm, oldHex, newHex, s.cfg.Hooks.TimeoutSec); err != nil {
 				return err
 			}
 			if err := runPreReceiveHook(s.cfg.Hooks.Update, bm, oldHex, newHex, s.cfg.Hooks.TimeoutSec); err != nil {
 				return err
 			}
 		}
 		if repoCfg.RequireSignedCommits && user != nil {
 			for _, id := range collectNewCommitIDs(repoObj, oldHex, newHex) {
 				c, err := repoObj.ReadCommit(id)
 				if err != nil {
 					continue
 				}
 				if len(c.CommitSig) == 0 {
 					return fmt.Errorf("commit %s (ch:%s) is unsigned; this repository requires signed commits",
 						hex.EncodeToString(id[:8]), c.ChangeID)
 				}
 				body := object.CommitBodyForSigning(c)
 				keys, _ := s.db.ListSSHKeys(user.ID)
 				verified := false
 				for _, k := range keys {
 					pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k.PublicKey))
 					if err != nil {
 						continue
 					}
 					if object.VerifyCommitSig(body, c.CommitSig, pub) == nil {
 						verified = true
 						break
 					}
 				}
 				if !verified {
 					return fmt.Errorf("commit %s (ch:%s) has an unverifiable signature; this repository requires commits signed by a registered key",
 						hex.EncodeToString(id[:8]), c.ChangeID)
 				}
 			}
 		}
 		return nil
 	}
 
 	srv.OnBookmarkUpdated = func(bm, oldHex, newHex string) {
 		s.db.FirePushWebhooks(repoName, pusher, bm, oldHex, newHex, collectPushCommits(repoObj, oldHex, newHex))
 		runPostReceiveHook(s.cfg.Hooks.PostReceive, bm, oldHex, newHex, s.cfg.Hooks.TimeoutSec)
 
 		if user != nil {
 			for _, id := range collectNewCommitIDs(repoObj, oldHex, newHex) {
 				c, err := repoObj.ReadCommit(id)
 				if err != nil {
 					continue
 				}
 				_ = s.db.RecordCommitSignature(repoObj, id, c, user.ID)
 			}
 		}
 
 		if allowed, script, _ := s.db.GetRepoHookConfig(rec.ID); allowed && script != "" {
 			if !s.db.hasWriteCollaborator(rec.ID) {
 				runPostReceiveHook(script, bm, oldHex, newHex, s.cfg.Hooks.TimeoutSec)
 			}
 		}
 	}
 	srv.Handler().ServeHTTP(w, r2)
 }
 
 func collectPushCommits(r *repo.Repo, oldHex, newHex string) []CommitRef {
 	if len(newHex) != 64 {
 		return []CommitRef{}
 	}
 	newBytes, err := hex.DecodeString(newHex)
 	if err != nil || len(newBytes) != 32 {
 		return []CommitRef{}
 	}
 	var newID [32]byte
 	copy(newID[:], newBytes)
 
 	var oldID [32]byte
 	if len(oldHex) == 64 {
 		if oldBytes, err2 := hex.DecodeString(oldHex); err2 == nil && len(oldBytes) == 32 {
 			copy(oldID[:], oldBytes)
 		}
 	}
 
 	seen := make(map[[32]byte]bool)
 	queue := [][32]byte{newID}
 	var results []CommitRef
 	const maxCommits = 50
 
 	for len(queue) > 0 && len(results) < maxCommits {
 		id := queue[0]
 		queue = queue[1:]
 		if seen[id] || id == oldID {
 			continue
 		}
 		seen[id] = true
 		c, err := r.ReadCommit(id)
 		if err != nil {
 			break
 		}
 		author := c.Author.Name
 		if c.Author.Email != "" {
 			author += " <" + c.Author.Email + ">"
 		}
 		results = append(results, CommitRef{
 			ID:       hex.EncodeToString(id[:]),
 			ChangeID: "ch:" + c.ChangeID,
 			Message:  c.Message,
 			Author:   author,
 		})
 		for _, p := range c.Parents {
 			if !seen[p] && p != oldID {
 				queue = append(queue, p)
 			}
 		}
 	}
 	return results
 }
 
+type srvHomeData struct {
+	Repo       string
+	User       *User
+	CommitHex  string
+	ShortHex   string
+	ReadmeName string
+	ReadmeHTML template.HTML
+	Entries    []srvTreeEntry
+}
+
-func (s *forgeServer) handleRepoHome(w http.ResponseWriter, r *http.Request) {
-	
+func (s *forgeServer) handleRepoHome(w http.ResponseWriter, r *http.Request) {
+	
+repoObj, rec, ok := s.requireRepoAccess(w, r)
+	if !ok {
+		return
+	}
+	defer repoObj.Close()
+
+	commitID := resolveDefaultCommit(repoObj)
+	if commitID == ([32]byte{}) {
+		_, id, err := repoObj.HeadCommit()
+		if err != nil {
+			
-http.Redirect(w, r, "/"+r
+http.Redirect(w, r, "/"+r
+ec
-.
+.
-P
+N
-a
+a
+me+"/log", h
-t
+t
+tp.StatusFound)
+			return
+		}
+		commitID = id
+	}
+
+	c, err := repoObj.ReadCommit(commitID)
+	if err != nil {
+		
-h
+h
-V
+ttp.Redirect(w, r, "/"+rec.N
-a
+a
+me+"/
-l
+l
+og", http.Stat
-u
+u
+sFound)
+		return
+	}
+	tree, err := repoObj.ReadTre
-e(
+e(
+c.TreeID)
+	if err != nil {
+		http.Redirect(w, r, "/
-"
+"
++
-re
+re
+c.Name+"/log", htt
-p
+p
+.StatusF
-o
+o
+und)
+		return
+	}
+
+	commitHex := fullHex(commitID)
+
+	var entries []srvTreeEntry
+	for _, e := range tree.Entries {
+		isDir := e.Mode == object.ModeDir
+		var link string
+		if isDir {
+			link = fmt.Sprintf(
-"
+"
+/%s/tree?id=%s&path=%s", rec.Name, commitHex, e.Name
-)
+)
+
+		} else {
+			link = fmt.Sprintf("/%s/file?id=%s&path=%s", rec.Name, commitHex, e.Name)
+		}
+		entries = append(entries, srvTreeEntry{
+			Name:  e.Name,
+			IsDir: isDir,
+			Mode:  modeStr(e.Mode),
+			Link:  link,
+		})
+	}
+
+	var readmeName string
+	var readmeHTML template.HTML
+	for _, candidate := range []string{"README.md", "readme.md", "README", "readme"} {
+		for _, e := range tree.Entries {
+			if e.Name == candidate && e.Mode != object.ModeDir {
+				content, err := repoObj.ReadBlob(e.ObjectID)
+				if err != nil || isBinaryContent(content) {
+					break
+				}
+				readmeName = e.Name
+				if strings.HasSuffix(strings.ToLower(e.Name), ".md") {
+					readmeHTML = markdown.Render(string(content))
+				} else {
+					readmeHTML = template.HTML("<pre>" + template.HTMLEscapeString(string(content)) + "</pre>")
+				}
+				break
+			}
+		}
+		if readmeName != "" {
+			break
+		}
+	}
+
+	if readmeName == "" && len(entries) == 0 {
+		http.Redirect(w, r, "/"+rec.Name
-+"/log", http.StatusFound
++"/log", http.StatusFound
+)
+		return
+	}
+
+	s.render(w, "srv_repo_home.html", srvHomeData{
+		Repo:       rec.Name,
+		User:       s.db.currentUser(r),
+		CommitHex:  commitHex,
+		ShortHex:   shortHex(commitID),
+		ReadmeName: readmeName,
+		ReadmeHTML: readmeHTML,
+		Entries:    entries,
+	}
 )
 }
 
 type srvCommitRow struct {
 	HexID      string
 	ShortHex   string
 	ChangeID   string
 	Author     string
 	Date       string
 	Phase      string
 	PhaseClass string
 	Message    string
 	Bookmarks  []string
 	IsHead     bool
 }
 
 type srvLogData struct {
 	Repo           string
 	User           *User
 	Commits        []srvCommitRow
 	WhereExpr      string
 	WhereErr       string
 	BookmarkFilter string
 	AllBookmarks   []string
 }
 
 func (s *forgeServer) handleRepoLog(w http.ResponseWriter, r *http.Request) {
 	repoObj, rec, ok := s.requireRepoAccess(w, r)
 	if !ok {
 		return
 	}
 	defer repoObj.Close()
 
 	const maxCommits = 200
 	where := r.URL.Query().Get("where")
 	bookmarkFilter := r.URL.Query().Get("bookmark")
 
 	var whereFilter revset.Func
 	var whereErr string
 	if where != "" {
 		var err error
 		whereFilter, err = revset.Parse(where)
 		if err != nil {
 			whereErr = err.Error()
 		}
 	}
 
 	headCID, _ := repoObj.HeadChangeID()
 	bmMap := bookmarkMap(repoObj)
 
 	allBms, _ := repoObj.Store.ListBookmarks()
 	allBmNames := make([]string, 0, len(allBms))
 	for _, bm := range allBms {
 		allBmNames = append(allBmNames, bm.Name)
 	}
 
 	var candidateIDs [][32]byte
 	if bookmarkFilter != "" {
 		bm, err := repoObj.Store.GetBookmark(bookmarkFilter)
 		if err == nil && bm != nil {
 			visited := map[[32]byte]bool{}
 			queue := [][32]byte{bm.CommitID}
 			for len(queue) > 0 && len(candidateIDs) < maxCommits*2 {
 				id := queue[0]
 				queue = queue[1:]
 				if visited[id] {
 					continue
 				}
 				visited[id] = true
 				candidateIDs = append(candidateIDs, id)
 				c, err := repoObj.ReadCommit(id)
 				if err != nil {
 					continue
 				}
 				for _, p := range c.Parents {
 					if !visited[p] {
 						queue = append(queue, p)
 					}
 				}
 			}
 		}
 	} else {
 		v
+isited := map[[32]byte]bool{}
+		v
-ar 
+ar 
+qu
-e
+e
-rr
+ue
- 
+ 
+[][32]byt
-e
+e
-rror
-
-		
+
+		
-c
-a
+a
-ndidateID
+llBm
-s
+s
+2
-, 
+, 
-err
+_
- 
+ 
+:
-= repoObj.Store.List
+= repoObj.Store.List
-Pu
+Bookmarks()
+		for _, 
-b
+b
+m := range a
 l
+lBms2 {
+			
 i
-c
+f !visited[bm.
 CommitID
-s
+] {
+				queue = append
 (
+queue, bm.CommitID
 )
 		
+		visited[bm.CommitID] = true
+			}
+		}
+		
 if 
+_, headID, 
 err 
-!
+:= repoObj.HeadCommit(); err =
 = nil 
+&& !visited[headID] 
 {
 			
+queue = append(queue, 
 h
+eadID)
+			visi
 t
+ed[headID] = 
 t
-p.E
 r
-r
+ue
+		}
+		f
 or
+ len
 (
-w,
+queue)
  
-"
+> 0 && 
 l
+en(cand
 i
+dateID
 s
-t
+)
  
-c
+< maxC
 ommits
+*2 {
+			id 
 :
+=
  
-"+
+queue[0]
+			queue = queue[1:]
+			candidateIDs = append(candidateIDs, id)
+			c, 
 err
+ := repoObj
 .
-E
+ReadCommit(id)
+			if e
 rr
+ != nil {
+				continue
+			}
+			f
 or
-()
+ _
 , 
-htt
 p
-.St
+ := r
 a
-tusI
 n
-t
+g
 e
+ c.Pa
 r

internal/archesrv/templates/srv_repo_home.html [A]
--- /dev/null
+++ b/internal/archesrv/templates/srv_repo_home.html
@@ -1,0 +1,150 @@
+{{ define "title" }}{{.Repo}}{{ end }} {{ define "navextra" }}<a href="/{{.Repo}}">{{.Repo}}</a>
+<a href="/{{.Repo}}/log">log</a> <a href="/{{.Repo}}/tree">tree</a>
+<a href="/{{.Repo}}/issues">issues</a> <a href="/{{.Repo}}/stacks">stacks</a>
+<a href="/{{.Repo}}/wiki">wiki</a>{{ end }} {{ define "srv_repo_home.html" }}{{
+template "head" . }}
+<style>
+  .entry-dir a .folder-icon-open {
+    display: none;
+  }
+  .entry-dir a:hover .folder-icon {
+    display: none;
+  }
+  .entry-dir a:hover .folder-icon-open {
+    display: inline;
+  }
+  .readme-box {
+    border: 1px solid #ddd;
+    border-radius: 4px;
+    margin-top: 24px;
+  }
+  .readme-header {
+    background: #f4f4f4;
+    padding: 6px 12px;
+    font-size: 12px;
+    color: #666;
+    border-bottom: 1px solid #ddd;
+    border-radius: 4px 4px 0 0;
+  }
+  .readme-body {
+    padding: 16px 20px;
+    line-height: 1.6;
+  }
+  .readme-body h1,
+  .readme-body h2,
+  .readme-body h3 {
+    margin: 1em 0 0.4em;
+    font-size: 1.1em;
+  }
+  .readme-body h1 {
+    font-size: 1.4em;
+    border-bottom: 1px solid #eee;
+    padding-bottom: 4px;
+  }
+  .readme-body h2 {
+    font-size: 1.2em;
+    border-bottom: 1px solid #eee;
+    padding-bottom: 2px;
+  }
+  .readme-body p {
+    margin: 0.6em 0;
+  }
+  .readme-body pre {
+    background: #f8f8f8;
+    border: 1px solid #ddd;
+    border-radius: 3px;
+    padding: 10px 12px;
+    overflow-x: auto;
+    font-size: 13px;
+  }
+  .readme-body code {
+    background: #f0f0f0;
+    padding: 1px 4px;
+    border-radius: 2px;
+    font-size: 12px;
+  }
+  .readme-body pre code {
+    background: none;
+    padding: 0;
+  }
+  .readme-body ul,
+  .readme-body ol {
+    padding-left: 1.5em;
+    margin: 0.5em 0;
+  }
+  .readme-body blockquote {
+    border-left: 3px solid #ddd;
+    padding-left: 12px;
+    color: #666;
+    margin: 0.5em 0;
+  }
+  .readme-body a {
+    color: #0645ad;
+  }
+  .readme-body table {
+    border-collapse: collapse;
+  }
+  .readme-body th,
+  .readme-body td {
+    border: 1px solid #ddd;
+    padding: 4px 8px;
+  }
+  .readme-body th {
+    background: #f4f4f4;
+  }
+</style>
+<div class="container">
+    <h1 class="repo-name">{{.Repo}}</h1>
+    <div class="repo-meta">
+        at
+        <a href="/{{.Repo}}/commit?id={{.CommitHex}}"><code>{{.ShortHex}}</code></a>
+        &nbsp;·&nbsp; <a href="/{{.Repo}}/log">log</a> &nbsp;·&nbsp;
+        <a href="/{{.Repo}}/tree?id={{.CommitHex}}">browse tree</a>
+    </div>
+    {{ if .Entries }}
+    <table style="margin-top: 12px">
+        <thead>
+            <tr>
+                <th>Name</th>
+                <th>Type</th>
+            </tr>
+        </thead>
+        <tbody>
+            {{ range .Entries }}
+        <tr class="{{ if .IsDir }}entry-dir{{ else }}entry-file{{ end }}">
+            <td>
+                <a href="{{.Link}}">{{ if .IsDir }}
+                    <svg class="folder-icon"
+                         xmlns="http://www.w3.org/2000/svg"
+                         width="14"
+                         height="14"
+                         viewBox="0 0 24 24"
+                         style="vertical-align: -2px;
+                                margin-right: 4px">
+                        <path fill="currentColor" d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h6l2 2h8q.825 0 1.413.588T22 8v10q0 .825-.587 1.413T20 20zm0-2h16V8h-8.825l-2-2H4zm0 0V6z" />
+                    </svg>
+                    <svg class="folder-icon-open"
+                         xmlns="http://www.w3.org/2000/svg"
+                         width="14"
+                         height="14"
+                         viewBox="0 0 24 24"
+                         style="vertical-align: -2px;
+                                margin-right: 4px;
+                                display: none">
+                        <path fill="currentColor" d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h6l2 2h8q.825 0 1.413.588T22 8H11.175l-2-2H4v12l2.4-8h17.1l-2.575 8.575q-.2.65-.737 1.038T19 20zm2.1-2H19l1.8-6H7.9zm0 0l1.8-6zM4 8V6z" />
+                    </svg>
+                {{ end }}{{.Name}}</a>
+            </td>
+            <td style="color: #888; font-size: 12px">{{ if .IsDir }}dir{{ else }}{{.Mode}}{{ end }}</td>
+        </tr>
+        {{ end }}
+    </tbody>
+</table>
+{{ end }} {{ if .ReadmeName }}
+<div class="readme-box">
+    <div class="readme-header">{{.ReadmeName}}</div>
+    <div class="readme-body">{{.ReadmeHTML}}</div>
+</div>
+{{ end }}
+</div>
+{{ template "foot" . }} {{ end }}

internal/archesrv/templates/srv_repo_log.html [M]
--- a/internal/archesrv/templates/srv_repo_log.html
+++ b/internal/archesrv/templates/srv_repo_log.html
@@ -1,79 +1,111 @@
 {{ define "title" }}{{.Repo}} — log{{ end }}
 {{ define "navextra" }}<a href="/{{.Repo}}">{{.Repo}}</a> <a href="/{{.Repo}}/log">log</a> <a href="/{{.Repo}}/tree">tree</a> <a href="/{{.Repo}}/issues">issues</a> <a href="/{{.Repo}}/stacks">stacks</a> <a href="/{{.Repo}}/wiki">wiki</a>{{ end }}
 {{ define "srv_repo_log.html" }}{{ template "head" . }}
 <div class="container">
     <h1 class="repo-name">{{.Repo}} / log</h1>
     <form method="get"
           style="display:flex;
                  gap:8px;
                  margin-bottom:16px;
                  align-items:center">
         <input name="where"
                value="{{.WhereExpr}}"
                placeholder='filter: author(alice) or draft()'
                style="flex:1;
                       padding:6px 10px;
                       font-family:monospace;
                       font-size:13px;
                       background:#1e1e1e;
                       color:#ccc;
                       border:1px solid #444;
                       border-radius:4px;
                       outline:none">
         <button type="submit"
                 style="padding:6px 14px;
                        background:#2a6;
                        color:#fff;
                        border:none;
                        border-radius:4px;
                        cursor:pointer;
                        font-size:13px">Filter</button>
         {{ if .WhereExpr }}<a href="?where=" style="color:#888; font-size:12px; white-space:nowrap">✕ clear</a>{{ end }}
     </form>
     {{ if .WhereErr }}
     <p style="color:#e55;
               font-family:monospace;
               font-size:12px;
               margin:-8px 0 12px">parse error: {{.WhereErr}}</p>
     {{ end }}
     {{ if .AllBookmarks }}
     <div style="display:flex;
+
+               
- flex-wrap:wrap;
+ flex-wrap:wrap;
+
+               
- gap:6px;
+ gap:6px;
+
+               
  margin-bottom:14px;
+
+               
  align-items:center">
         <span style="font-size:12px; color:#888">branch:</span>
         {{ if .BookmarkFilter }}
         <a href="?where={{.WhereExpr}}"
+
+          
  style="font-size:11px;
+
+                 
- padding:2px 8px;
+ padding:2px 8px;
+
+                 
  border-radius:3px;
+
+                 
  border:1px solid #aaa;
+
+                 
- color:#555;
+ color:#555;
+
+                 
  background:transparent;
+
+                 
  text-decoration:none">all</a>
-    
     {{ else }}
         <a href="?where={{.WhereExpr}}"
+
+          
  style="font-size:11px;
+
+                 
- padding:2px 8px;
+ padding:2px 8px;
+
+                 
  border-radius:3px;
+
+                 
  border:1px solid #aaa;
+
+                 
  color:#fff;
+
+                 
  background:#444;
+
+                 
  text-decoration:none">all</a>
         {{ end }}
         {{ range .AllBookmarks }}
         {{ if eq . $.BookmarkFilter }}
         <a href="?bookmark={{.}}&where={{$.WhereExpr}}"
+
+          
  style="font-size:11px;
+
+                 
  padding:2px 8px;
+
+                 
  border-radius:3px;
+
+                 
  border:1px solid #b0c0ff;
+
+                 
  color:#fff;
+
+                 
  background:#0645ad;
+
+                 
  text-decoration:none">{{.}}</a>
-    
     {{ else }}
         <a href="?bookmark={{.}}&where={{$.WhereExpr}}"
+
+          
  style="font-size:11px;
+
+                 
  padding:2px 8px;
+
+                 
  border-radius:3px;
+
+                 
  border:1px solid #b0c0ff;
+
+