feat(web/panettone): Add initial styles

Take an initial crack at styling most of the Panettone application,
taking inspiration from the styles from todo.tvl.fyi and tvl.fyi itself.
This uses the LASS CSS library, after a brief attempt at using css-lite
which I ended up not going with because I don't like the library's
design very much, and also it's not compatible with sbcl's (safety
3) (some macroexpansions SETQ undeclared variables).

Change-Id: I054402e4c68ae1e99884d5164e6e2fc39d2779ff
Reviewed-on: https://cl.tvl.fyi/c/depot/+/1350
Tested-by: BuildkiteCI
Reviewed-by: eta <eta@theta.eu.org>
This commit is contained in:
Griffin Smith 2020-07-22 18:16:58 -04:00 committed by glittershark
parent d3b7de0783
commit d445136140
5 changed files with 234 additions and 60 deletions

View file

@ -9,6 +9,7 @@ depot.nix.buildLisp.program {
defclass-std defclass-std
easy-routes easy-routes
hunchentoot hunchentoot
lass
local-time local-time
trivial-ldap trivial-ldap
@ -16,6 +17,9 @@ depot.nix.buildLisp.program {
]; ];
srcs = [ srcs = [
./panettone.asd
./src/packages.lisp
./src/css.lisp
./src/panettone.lisp ./src/panettone.lisp
]; ];
} }

View file

@ -0,0 +1,6 @@
(asdf:defsystem "panettone"
:description "A simple issue tracker"
:serial t
:components ((:file "packages")
(:file "css")
(:file "pannetone")))

120
web/panettone/src/css.lisp Normal file
View file

@ -0,0 +1,120 @@
(in-package :panettone.css)
(declaim (optimize (safety 3)))
(defparameter color/black
"rgb(24, 24, 24)")
(defparameter color/gray
"#8D8D8D")
(defparameter color/primary
"rgb(106, 154, 255)")
(defparameter color/primary-light
"rgb(150, 166, 200)")
(defparameter color/success
"rgb(168, 249, 166)")
(defparameter color/success-2
"rgb(168, 249, 166)")
(defun button (selector)
`((,selector
:background-color ,color/success
:padding "0.5rem"
:text-decoration "none"
:transition "box-shadow" "0.15s" "ease-in-out")
((:and ,selector :hover)
:box-shadow "0.25rem" "0.25rem" "0" "0" "rgba(0,0,0,0.08)")
((:and ,selector (:or :active :focus))
:box-shadow "0.1rem" "0.1rem" "0" "0" "rgba(0,0,0,0.05)"
:outline "none"
:border "none"
:background-color ,color/success-2)))
(defparameter issue-list-styles
`((.issue-list
:list-style-type "none"
:padding-left 0
(.issue-subject
:font-weight "bold")
(li
:padding-bottom "1rem")
((li + li)
:border-top "1px" "solid" ,color/gray)
(a
:text-decoration "none"
:display "block")
((:and a :hover)
:outline "none"
(.issue-subject
:color ,color/primary)))))
(defparameter form-styles
`(((:or (:and input (:or (:= type "text")
(:= type "password")))
textarea)
:width "100%"
:padding "0.5rem"
:outline "none"
:border-top "none"
:border-left "none"
:border-right "none"
:border-bottom "1px" "solid" ,color/gray
:margin-bottom "1rem")
(textarea
:resize "vertical")
((:and input (:= type "submit"))
:-webkit-appearance "none"
:border "none"
:cursor "pointer")
,@(button '(:and input (:= type "submit")))))
(defparameter styles
`(,@form-styles
,@issue-list-styles
(body
:font-family "sans-serif"
:color ,color/black)
(a :color "inherit")
(.content
:width "800px"
:margin "0 auto")
(header
:display "flex"
:align-items "center"
:border-bottom "1px" "solid" ,color/black
:margin-bottom "1rem"
(h1
:padding 0
:flex 1)
(.issue-number
:color ,color/gray
:font-size "1.5rem"))
,@(button '.new-issue)
(.login-form
:width "300px"
:margin "0 auto")
(.created-by-at
:color ,color/gray)))

View file

@ -0,0 +1,10 @@
(defpackage panettone.css
(:use :cl :lass)
(:export :styles))
(defpackage panettone
(:use :cl :klatre :easy-routes)
(:import-from :cl-prevalence :get-id)
(:import-from :defclass-std :defclass/std)
(:import-from :alexandria :if-let :when-let)
(:export :start-pannetone :config :main))

View file

@ -1,11 +1,4 @@
(defpackage panettone
(:use :cl :klatre :easy-routes)
(:import-from :defclass-std :defclass/std)
(:import-from :alexandria :if-let)
(:shadowing-import-from :alexandria :when-let)
(:export :start-panettone :main))
(in-package :panettone) (in-package :panettone)
(declaim (optimize (safety 3))) (declaim (optimize (safety 3)))
;;; ;;;
@ -170,67 +163,100 @@ updated issue"
(defmacro render (&body body) (defmacro render (&body body)
`(who:with-html-output-to-string (*standard-output* nil :prologue t) `(who:with-html-output-to-string (*standard-output* nil :prologue t)
(:head (:head
(:title (who:esc *title*))) (:title (who:esc *title*))
(:body ,@body))) (:link :rel "stylesheet" :type "text/css" :href "/main.css"))
(:body
(:div :class "content"
,@body))))
(defun render/login (&optional message) (defun render/login (&optional message)
(render (render
(:h1 "Login") (:div
(when message :class "login-form"
(who:htm (:div.alert (who:esc message)))) (:header
(:form (:h1 "Login"))
:method :post :action "/login" (:main
(:div :class "login-form"
(:label :for "username" (when message
"Username") (who:htm (:div :class "alert" (who:esc message))))
(:input :type "text" (:form
:name "username" :method :post :action "/login"
:id "username" (:div
:placeholder "username")) (:label :for "username"
(:div "Username")
(:label :for "password" (:input :type "text"
"Password") :name "username"
(:input :type "password" :id "username"
:name "password" :placeholder "username"))
:id "password" (:div
:placeholder "password")) (:label :for "password"
(:input :type "submit" "Password")
:value "Submit")))) (:input :type "password"
:name "password"
:id "password"
:placeholder "password"))
(:input :type "submit"
:value "Submit"))))))
(defun created-by-at (issue)
(who:with-html-output (*standard-output*)
(:span :class "created-by-at"
"Opened by "
(:span :class "username"
(who:esc
(or
(when-let ((author (author issue)))
(displayname author))
"someone")))
" at "
(:span :class "timestamp"
(who:esc
(format-dottime (created-at issue)))))))
(defun render/index (&key issues) (defun render/index (&key issues)
(render (render
(:h1 "Issues") (:header
(:a :href "/issues/new" "New Issue") (:h1 "Issues")
(:ul (:a
(loop for issue in issues :class "new-issue"
do (who:htm :href "/issues/new" "New Issue"))
(:li (:main
(:a :href (format nil "/issues/~A" (cl-prevalence:get-id issue)) (:ol
(who:esc (subject issue))))))))) :class "issue-list"
(dolist (issue issues)
(let ((issue-id (get-id issue)))
(who:htm
(:li
(:a :href (format nil "/issues/~A" issue-id)
(:p
(:span :class "issue-subject"
(who:esc (subject issue))))
(:span :class "issue-number"
(who:esc (format nil "#~A" issue-id)))
" - "
(created-by-at issue))))))))))
(defun render/new-issue () (defun render/new-issue ()
(render (render
(:h1 "New Issue") (:header
(:form (:h1 "New Issue"))
:method :post :action "/issues" (:main
(:div (:form :method "post"
(:label :for "subject" "Subject") :action "/issues"
(:input :type :text :class "issue-form"
:id "subject" (:div
:name "subject" (:input :type "text"
:placeholder "Subject")) :id "subject"
:name "subject"
:placeholder "Subject"))
(:div (:div
(:textarea :name "body")) (:textarea :name "body"
:placeholder "Description"
:rows 10))
(:input :type :submit (:input :type "submit"
:value "Create Issue")))) :value "Create Issue")))))
(defun created-by-at (issue)
(format nil "Opened by ~A at ~A"
(when-let ((author (author issue)))
(displayname author))
(format-dottime (created-at issue))))
(comment (comment
(format nil "foo: ~A" "foo") (format nil "foo: ~A" "foo")
@ -239,9 +265,13 @@ updated issue"
(defun render/issue (issue) (defun render/issue (issue)
(check-type issue issue) (check-type issue issue)
(render (render
(:h1 (who:esc (subject issue))) (:header
(:p (who:esc (created-by-at issue))) (:h1 (who:esc (subject issue)))
(:p (who:esc (body issue))))) (:div :class "issue-number"
(who:esc (format nil "#~A" (get-id issue)))))
(:main
(:p (created-by-at issue))
(:p (who:esc (body issue))))))
(defun render/not-found (entity-type) (defun render/not-found (entity-type)
(render (render
@ -296,6 +326,10 @@ updated issue"
(issue-not-found (_) (issue-not-found (_)
(render/not-found "Issue")))) (render/not-found "Issue"))))
(defroute styles ("/main.css") ()
(setf (hunchentoot:content-type*) "text/css")
(apply #'lass:compile-and-write panettone.css:styles))
(defvar *acceptor* nil (defvar *acceptor* nil
"Hunchentoot acceptor for Panettone's web server.") "Hunchentoot acceptor for Panettone's web server.")