Redesign the dashboard and sign-in page with the Lightwell design language
CI / test (push) Successful in 2m27s
CI / test (push) Successful in 2m27s
Warm neutrals, teal accent, 15px type, and Lightwell's component shapes. Status is a badge with text instead of a colored dot. Every destructive action asks first, naming the object and what is lost. Dialogs trap focus, close on Escape and return focus. The sidebar is a drawer on phones, there is a theme switch, loading shows skeleton rows, and empty states explain what belongs there. Emoji and dashes are gone, and server errors are capitalized and tell visitors what to do next. Adds scripts/lint-design.mjs and docs/design.md. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2b7cb5dd35
commit
d74e6c1e72
7 files changed
+536
-253
No files matched your search
+11
-11
@@ -108,7 +108,7 @@ func (s *Server) resourceInfo(w http.ResponseWriter, r *http.Request) {
|
||||
defer s.mu.Unlock()
|
||||
m := s.protectedMapping(r.PathValue("id"))
|
||||
if m == nil {
|
||||
fail(w, 404, errors.New("this resource doesn't need a login, or doesn't exist"))
|
||||
fail(w, 404, errors.New("This page does not need a sign-in, or it no longer exists. Check the address, or ask the person who shared it."))
|
||||
return
|
||||
}
|
||||
if _, err := checkReturn(m, r.URL.Query().Get("return")); err != nil {
|
||||
@@ -141,7 +141,7 @@ func (s *Server) grant(w http.ResponseWriter, r *http.Request, m *model.Mapping,
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if key == "" {
|
||||
fail(w, 500, errors.New("edge is not set up for logins"))
|
||||
fail(w, 500, errors.New("This server is not set up for sign-ins yet. Ask the administrator to finish setting it up."))
|
||||
return
|
||||
}
|
||||
tok := authtoken.Sign(key, authtoken.Token{Mapping: m.ID, Sub: sub, Kind: authtoken.KindCallback, Epoch: m.AuthEpoch, Exp: time.Now().Add(2 * time.Minute).Unix()})
|
||||
@@ -158,7 +158,7 @@ func (s *Server) resourceAuth(method string, fn func(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
if !s.allow("res|"+method+"|"+clientIP(r)+"|"+r.PathValue("id"), 12, 5*time.Minute) {
|
||||
fail(w, 429, errors.New("too many attempts, wait a few minutes"))
|
||||
fail(w, 429, errors.New("Too many attempts. Wait a few minutes and try again."))
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
@@ -169,7 +169,7 @@ func (s *Server) resourceAuth(method string, fn func(w http.ResponseWriter, r *h
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if pm == nil {
|
||||
fail(w, 404, errors.New("no such resource"))
|
||||
fail(w, 404, errors.New("That page does not exist. Check the address."))
|
||||
return
|
||||
}
|
||||
back, err := checkReturn(&m, body["return"])
|
||||
@@ -191,7 +191,7 @@ func (s *Server) resourcePassword(w http.ResponseWriter, r *http.Request, m *mod
|
||||
}
|
||||
if bcrypt.CompareHashAndPassword([]byte(hash), []byte(body["password"])) != nil || hash == string(dummyHash) {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
fail(w, 401, errors.New("wrong password"))
|
||||
fail(w, 401, errors.New("That password is not right. Check it and try again."))
|
||||
return
|
||||
}
|
||||
s.grant(w, r, m, back, "password")
|
||||
@@ -207,7 +207,7 @@ func (s *Server) resourcePin(w http.ResponseWriter, r *http.Request, m *model.Ma
|
||||
}
|
||||
if bcrypt.CompareHashAndPassword([]byte(hash), []byte(body["pin"])) != nil || hash == string(dummyHash) {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
fail(w, 401, errors.New("wrong PIN"))
|
||||
fail(w, 401, errors.New("That PIN is not right. Check it and try again."))
|
||||
return
|
||||
}
|
||||
s.grant(w, r, m, back, "pin")
|
||||
@@ -216,7 +216,7 @@ func (s *Server) resourcePin(w http.ResponseWriter, r *http.Request, m *model.Ma
|
||||
func (s *Server) resourceSSO(w http.ResponseWriter, r *http.Request, m *model.Mapping, back *url.URL, body map[string]string) {
|
||||
p := s.principalFrom(r)
|
||||
if p == nil || p.User.ID == "" || !m.Auth.SSO {
|
||||
fail(w, 401, errors.New("sign in first"))
|
||||
fail(w, 401, errors.New("Sign in first, then try again."))
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
@@ -224,7 +224,7 @@ func (s *Server) resourceSSO(w http.ResponseWriter, r *http.Request, m *model.Ma
|
||||
allowed := u != nil && s.canAccess(u, m)
|
||||
s.mu.Unlock()
|
||||
if !allowed {
|
||||
fail(w, 403, errors.New("your account doesn't have access to this resource"))
|
||||
fail(w, 403, errors.New("Your account does not have access to this page. Ask the person who shared it to add you."))
|
||||
return
|
||||
}
|
||||
s.grant(w, r, m, back, p.User.Email)
|
||||
@@ -243,7 +243,7 @@ func mailAllowed(list []string, email string) bool {
|
||||
func (s *Server) resourceEmailStart(w http.ResponseWriter, r *http.Request, m *model.Mapping, back *url.URL, body map[string]string) {
|
||||
email := strings.ToLower(strings.TrimSpace(body["email"]))
|
||||
if !m.Auth.Email || !s.mailConfigured() {
|
||||
fail(w, 400, errors.New("email codes are not available for this resource"))
|
||||
fail(w, 400, errors.New("Email codes are not turned on for this page. Use another sign-in method."))
|
||||
return
|
||||
}
|
||||
// always answer the same way so addresses can't be probed
|
||||
@@ -279,7 +279,7 @@ func (s *Server) resourceEmailVerify(w http.ResponseWriter, r *http.Request, m *
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if !good || !mailAllowed(m.Auth.AllowedMails, email) {
|
||||
fail(w, 401, errors.New("wrong or expired code"))
|
||||
fail(w, 401, errors.New("That code is wrong or has expired. Request a new one and try again."))
|
||||
return
|
||||
}
|
||||
s.grant(w, r, m, back, email)
|
||||
@@ -303,7 +303,7 @@ func (s *Server) resourceShare(w http.ResponseWriter, r *http.Request, m *model.
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if !valid {
|
||||
fail(w, 401, errors.New("this share link is invalid, expired or used up"))
|
||||
fail(w, 401, errors.New("This share link is invalid, expired or already used up. Ask for a new one."))
|
||||
return
|
||||
}
|
||||
if name == "" {
|
||||
|
||||
Reference in new issue
Block a user