dashboard / git-pr / feat: static assets #3 rss

accepted · opened on 2024-07-19T14:53:23Z by erock
Help
# add changes to patch request
git format-patch main --stdout | ssh pr.pico.sh pr add 3
# add review to patch request
git format-patch main --stdout | ssh pr.pico.sh pr add --review 3
# remove patchset
ssh pr.pico.sh ps rm ps-x
# checkout all patches
ssh pr.pico.sh pr print 3 | git am -3
# print a diff between the last two patches in a patch request
ssh pr.pico.sh pr diff 3
# accept PR
ssh pr.pico.sh pr accept 3
# close PR
ssh pr.pico.sh pr close 3

Logs

erock created pr with ps-3 on 2024-07-19T14:53:23Z
erock added ps-4 on 2024-07-19T14:55:48Z
jolheiser reviewed pr with ps-5 on 2024-07-19T15:42:29Z
jolheiser changed status on 2024-07-19T15:42:29Z {"status":"reviewed"}
erock added ps-9 on 2024-07-19T17:43:47Z
jolheiser changed status on 2024-07-19T18:27:53Z {"status":"accepted"}

Patchsets

ps-3 by erock on 2024-07-19T14:53:23Z
Range Diff ↕
3: 7346122 < -: ------- chore: create patch for smol
4: d8792d5 < -: ------- feat: static folder
1: 8919af5 ! 2: 66cafc6 feat: static assets folder

@@ static/smol.css
+*,
+::before,
+::after {
+  box-sizing: border-box;
+}
+
+::-moz-focus-inner {
+  border-style: none;
+  padding: 0;
+}
+:-moz-focusring {
+  outline: 1px dotted ButtonText;
+}
+:-moz-ui-invalid {
+  box-shadow: none;
+}
+
+@media (prefers-color-scheme: light) {
+  :root {
+    --main-hue: 250;
+    --white: #2e3f53;
+    --white-light: #cfe0f4;
+    --white-dark: #6c6a6a;
+    --code: #52576f;
+    --pre: #e1e7ee;
+    --bg-color: #f4f4f4;
+    --text-color: #24292f;
+    --link-color: #005cc5;
+    --visited: #6f42c1;
+    --blockquote: #005cc5;
+    --blockquote-bg: #cfe0f4;
+    --hover: #c11e7a;
+    --grey: #ccc;
+    --grey-light: #6a708e;
+    --shadow: #e8e8e8;
+  }
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --main-hue: 250;
+    --white: #f2f2f2;
+    --white-light: #f2f2f2;
+    --white-dark: #e8e8e8;
+    --code: #414558;
+    --pre: #252525;
+    --bg-color: #282a36;
+    --text-color: #f2f2f2;
+    --link-color: #8be9fd;
+    --visited: #bd93f9;
+    --blockquote: #bd93f9;
+    --blockquote-bg: #353548;
+    --hover: #ff80bf;
+    --grey: #414558;
+    --grey-light: #6a708e;
+    --shadow: #252525;
+  }
+}
+
+html {
+  background-color: var(--bg-color);
+  color: var(--text-color);
+  font-size: 18px;
+  line-height: 1.5;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
+    Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
+    sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+  -webkit-text-size-adjust: 100%;
+  -moz-tab-size: 4;
+  -o-tab-size: 4;
+  tab-size: 4;
+}
+
+body {
+  margin: 0 auto;
+}
+
+img {
+  max-width: 100%;
+  height: auto;
+}
+
+b,
+strong {
+  font-weight: bold;
+}
+
+code,
+kbd,
+samp,
+pre {
+  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
+    monospace;
+}
+
+code,
+kbd,
+samp {
+  border: 2px solid var(--code);
+}
+
+pre > code {
+  background-color: inherit;
+  padding: 0;
+  border: none;
+}
+
+code {
+  font-size: 90%;
+  border-radius: 0.3rem;
+  padding: 0.1rem 0.3rem;
+}
+
+pre {
+  font-size: 14px;
+  border-radius: 5px;
+  padding: 1rem;
+  margin: 1rem 0;
+  overflow-x: auto;
+  background-color: var(--pre) !important;
+}
+
+small {
+  font-size: 0.8rem;
+}
+
+summary {
+  display: list-item;
+  cursor: pointer;
+}
+
+h1,
+h2,
+h3,
+h4 {
+  margin: 0;
+  padding: 0.5rem 0 0 0;
+  border: 0;
+  font-style: normal;
+  font-weight: inherit;
+  font-size: inherit;
+}
+
+path {
+  fill: var(--text-color);
+  stroke: var(--text-color);
+}
+
+hr {
+  color: inherit;
+  border: 0;
+  margin: 0;
+  height: 2px;
+  background: var(--grey);
+  margin: 1rem auto;
+  text-align: center;
+}
+
+a {
+  text-decoration: none;
+  color: var(--link-color);
+}
+
+a:hover,
+a:visited:hover {
+  text-decoration: underline;
+  color: var(--hover);
+}
+
+a:visited {
+  color: var(--visited);
+}
+
+a.link-grey {
+  text-decoration: underline;
+  color: var(--white);
+}
+
+a.link-grey:visited {
+  color: var(--white);
+}
+
+section {
+  margin-bottom: 1.4rem;
+}
+
+section:last-child {
+  margin-bottom: 0;
+}
+
+header {
+  margin: 1rem auto;
+}
+
+p {
+  margin: 0.5rem 0;
+}
+
+article {
+  overflow-wrap: break-word;
+}
+
+blockquote {
+  border-left: 5px solid var(--blockquote);
+  background-color: var(--blockquote-bg);
+  padding: 0.5rem 0.75rem;
+  margin: 0.5rem 0;
+}
+
+blockquote > p {
+  margin: 0;
+}
+
+blockquote code {
+  border: 1px solid var(--blockquote);
+}
+
+ul,
+ol {
+  padding: 0 0 0 1rem;
+  list-style-position: outside;
+}
+
+ul[style*="list-style-type: none;"] {
+  padding: 0;
+}
+
+li {
+  margin: 0.5rem 0;
+}
+
+li > pre {
+  padding: 0;
+}
+
+footer {
+  text-align: center;
+  margin-bottom: 4rem;
+}
+
+dt {
+  font-weight: bold;
+}
+
+dd {
+  margin-left: 0;
+}
+
+dd:not(:last-child) {
+  margin-bottom: 0.5rem;
+}
+
+figure {
+  margin: 0;
+}
+
+.container {
+  max-width: 50em;
+  width: 100%;
+}
+
+.container-sm {
+  max-width: 40em;
+  width: 100%;
+}
+
+.container-center {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+}
+
+.mono {
+  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
+    monospace;
+}
+
+.link-alt-adj,
+.link-alt-adj:visited,
+.link-alt-adj:visited:hover,
+.link-alt-adj:hover {
+  color: var(--link-color);
+  text-decoration: none;
+}
+
+.link-alt-adj:visited:hover,
+.link-alt-adj:hover {
+  text-decoration: underline;
+}
+
+.link-alt-hover,
+.link-alt-hover:visited,
+.link-alt-hover:visited:hover,
+.link-alt-hover:hover {
+  color: var(--hover);
+  text-decoration: none;
+}
+
+.link-alt-hover:visited:hover,
+.link-alt-hover:hover {
+  text-decoration: underline;
+}
+
+.link-alt,
+.link-alt:visited,
+.link-alt:visited:hover,
+.link-alt:hover {
+  color: var(--white);
+  text-decoration: none;
+}
+
+.link-alt:visited:hover,
+.link-alt:hover {
+  text-decoration: underline;
+}
+
+.text-3xl {
+  font-size: 2.5rem;
+}
+
+.text-2xl {
+  font-size: 1.9rem;
+  line-height: 1.15;
+}
+
+.text-xl {
+  font-size: 1.55rem;
+  line-height: 1.15;
+}
+
+.text-lg {
+  font-size: 1.35rem;
+  line-height: 1.15;
+}
+
+.text-md {
+  font-size: 1.15rem;
+  line-height: 1.15;
+}
+
+.text-sm {
+  font-size: 0.875rem;
+}
+
+.text-xs {
+  font-size: 0.775rem;
+}
+
+.cursor-pointer {
+  cursor: pointer;
+}
+
+.w-full {
+  width: 100%;
+}
+
+.h-full {
+  height: 100%;
+}
+
+.border {
+  border: 2px solid var(--grey-light);
+}
+
+.text-left {
+  text-align: left;
+}
+
+.text-center {
+  text-align: center;
+}
+
+.text-underline {
+  border-bottom: 3px solid var(--text-color);
+  padding-bottom: 3px;
+}
+
+.text-hdr {
+  color: var(--hover);
+}
+
+.text-underline-hdr {
+  border-bottom: 3px solid var(--hover);
+  padding-bottom: 3px;
+}
+
+.font-bold {
+  font-weight: bold;
+}
+
+.font-italic {
+  font-style: italic;
+}
+
+.inline {
+  display: inline;
+}
+
+.inline-block {
+  display: inline-block;
+}
+
+.max-w-half {
+  max-width: 50%;
+}
+
+.h-screen {
+  height: 100vh;
+}
+
+.w-screen {
+  width: 100vw;
+}
+
+.flex {
+  display: flex;
+}
+
+.flex-col {
+  flex-direction: column;
+}
+
+.items-center {
+  align-items: center;
+}
+
+.m-0 {
+  margin: 0;
+}
+
+.mt {
+  margin-top: 0.5rem;
+}
+
+.mt-2 {
+  margin-top: 1rem;
+}
+
+.mt-4 {
+  margin-top: 2rem;
+}
+
+.mt-8 {
+  margin-top: 4rem;
+}
+
+.mb {
+  margin-bottom: 0.5rem;
+}
+
+.mb-2 {
+  margin-bottom: 1rem;
+}
+
+.mb-4 {
+  margin-bottom: 2rem;
+}
+
+.mb-8 {
+  margin-bottom: 4rem;
+}
+
+.mb-16 {
+  margin-bottom: 8rem;
+}
+
+.mr {
+  margin-right: 0.5rem;
+}
+
+.ml-sm {
+  margin-left: 0.25rem;
+}
+
+.ml {
+  margin-left: 0.5rem;
+}
+
+.pt-0 {
+  padding-top: 0;
+}
+
+.my {
+  margin-top: 0.5rem;
+  margin-bottom: 0.5rem;
+}
+
+.my-2 {
+  margin-top: 1rem;
+  margin-bottom: 1rem;
+}
+
+.my-4 {
+  margin-top: 2rem;
+  margin-bottom: 2rem;
+}
+
+.my-8 {
+  margin-top: 4rem;
+  margin-bottom: 4rem;
+}
+
+.mx {
+  margin-left: 0.5rem;
+  margin-right: 0.5rem;
+}
+
+.mx-2 {
+  margin-left: 1rem;
+  margin-right: 1rem;
+}
+
+.m-1 {
+  margin: 0.5rem;
+}
+
+.p-1 {
+  padding: 0.5rem;
+}
+
+.p-0 {
+  padding: 0;
+}
+
+.px-2 {
+  padding-left: 1rem;
+  padding-right: 1rem;
+}
+
+.px-4 {
+  padding-left: 2rem;
+  padding-right: 2rem;
+}
+
+.py {
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+}
+
+.py-2 {
+  padding-top: 1rem;
+  padding-bottom: 1rem;
+}
+
+.py-4 {
+  padding-top: 2rem;
+  padding-bottom: 2rem;
+}
+
+.py-8 {
+  padding-top: 4rem;
+  padding-bottom: 4rem;
+}
+
+.justify-between {
+  justify-content: space-between;
+}
+
+.justify-center {
+  justify-content: center;
+}
+
+.gap {
+  gap: 0.5rem;
+}
+
+.gap-2 {
+  gap: 1rem;
+}
+
+.group {
+  display: flex;
+  flex-direction: column;
+  gap: 0.5rem;
+}
+
+.group-2 {
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+}
+
+.group-h {
+  display: flex;
+  gap: 0.5rem;
+  align-items: center;
+}
+
+.flex-1 {
+  flex: 1;
+}
+
+.items-end {
+  align-items: end;
+}
+
+.items-start {
+  align-items: start;
+}
+
+.justify-end {
+  justify-content: end;
+}
+
+.font-grey-light {
+  color: var(--grey-light);
+}
+
+.hidden {
+  display: none;
+}
+
+.align-right {
+  text-align: right;
+}
+
+/* ==== MARKDOWN ==== */
+
+.md h1,
+.md h2,
+.md h3,
+.md h4 {
+  padding: 0;
+  margin: 1.5rem 0 0.9rem 0;
+  font-weight: bold;
+}
+
+.md h1 a,
+.md h2 a,
+.md h3 a,
+.md h4 a {
+  color: var(--grey-light);
+  text-decoration: none;
+}
+
+.md h1 {
+  font-size: 1.6rem;
+  line-height: 1.15;
+  border-bottom: 2px solid var(--grey);
+  padding-bottom: 0.7rem;
+}
+
+.md h2 {
+  font-size: 1.3rem;
+  line-height: 1.15;
+  color: var(--white-dark);
+}
+
+.md h3 {
+  font-size: 1.2rem;
+  color: var(--white-dark);
+}
+
+.md h4 {
+  font-size: 1rem;
+  color: var(--white-dark);
+}
+
+/* ==== HELPERS ==== */
+
+.logo-header {
+  line-height: 1;
+  display: inline-block;
+  background-color: #FF79C6;
+  background-image: linear-gradient(to right, #FF5555, #FF79C6, #F8F859);
+  color: transparent;
+  background-clip: text;
+  border: 3px solid #FF79C6;
+  padding: 8px 10px 10px 10px;
+  border-radius: 10px;
+  box-shadow: 0px 5px 0px 0px var(--shadow);
+  background-size: 100%;
+  -webkit-background-clip: text;
+  -moz-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  -moz-text-fill-color: transparent;
+}
+
+.btn {
+  border: 2px solid var(--link-color);
+  color: var(--link-color);
+  padding: 0.4rem 1rem;
+  font-weight: bold;
+  display: inline-block;
+}
+
+.btn-link,
+.btn-link:visited {
+  border: 2px solid var(--link-color);
+  color: var(--link-color);
+  padding: 0.4rem 1rem;
+  text-decoration: none;
+  font-weight: bold;
+  display: inline-block;
+}
+
+.btn-link:visited:hover,
+.btn-link:hover {
+  border: 2px solid var(--hover);
+}
+
+.btn-link-alt,
+.btn-link-alt:visited {
+  border: 2px solid var(--white);
+  color: var(--white);
+}
+
+.box {
+  border: 2px solid var(--grey-light);
+  padding: 0.5rem 0.75rem;
+}
+
+.box-sm {
+  border: 2px solid var(--grey-light);
+  padding: 0.15rem 0.35rem;
+}
+
+.box-alert {
+  border: 2px solid var(--hover);
+  padding: 0.5rem 0.75rem;
+}
+
+.box-sm-alert {
+  border: 2px solid var(--hover);
+  padding: 0.15rem 0.35rem;
+}
+
+.list-none {
+  list-style-type: none;
+}
+
+.list-disc {
+  list-style-type: disc;
+}
+
+.list-decimal {
+  list-style-type: decimal;
+}
+
+.pill {
+  border: 1px solid var(--link-color);
+  color: var(--link-color);
+}
+
+.pill-alert {
+  border: 1px solid var(--hover);
+  color: var(--hover);
+}
+
+.pill-info {
+  border: 1px solid var(--visited);
+  color: var(--visited);
+}
+
+@media only screen and (max-width: 40em) {
+  body {
+    padding: 0 1rem;
+  }
+
+  header {
+    margin: 0;
+  }
+
+  .flex-collapse {
+    flex-direction: column;
+  }
+}
2: 3b99dc0 ! 1: 5e76ed3 fix(cli): access control for removing patchsets
ps-4 by erock on 2024-07-19T14:55:48Z
Range Diff ↕
2: 5e76ed3 < -: ------- fix(cli): access control for removing patchsets
1: 66cafc6 = 1: cc56ea1 feat: static assets folder
-: ------- > 2: ef749a4 review: typo and future enhancement comment
ps-5 by jolheiser on 2024-07-19T15:42:29Z
Range Diff ↕
1: cc56ea1 = 1: 0467f9e feat: static assets folder
2: ef749a4 = 2: da1730f review: typo and future enhancement comment
-: ------- > 3: c038404 refactor: per-file override for static folder
ps-9 by erock on 2024-07-19T17:43:47Z

Patchset ps-9

feat: static assets folder

Eric Bower
2024-07-19T04:27:17Z
Makefile
+5 -0
ssh.go
+1 -1
static/smol.css
+726 -0
web.go
+68 -1

review: typo and future enhancement comment

jolheiser
2024-07-19T15:40:15Z
web.go
+3 -0

refactor: per-file override for static folder

Eric Bower
2024-07-19T17:43:12Z
web.go
+27 -15
Back to top

feat: static assets folder

Create a static folder that will be served as-is with the ability for
users to bring-their-own static folder.

If we detect `data_dir/static/` we will serve that instead of the
embedded one we provide by default.
Makefile link
+5 -0
 1diff --git a/Makefile b/Makefile
 2index cb3a367..e521f82 100644
 3--- a/Makefile
 4+++ b/Makefile
 5@@ -26,3 +26,8 @@ bp: bp-setup
 6 	$(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-ssh:$(DOCKER_TAG)" --target release-ssh .
 7 	$(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-web:$(DOCKER_TAG)" --target release-web .
 8 .PHONY: bp
 9+
10+smol:
11+	curl https://pico.sh/smol.css -o ./static/smol.css
12+	cat patches/smol.diff | git apply
13+.PHONY: smol
patches/smol.diff link
+53 -0
 1diff --git a/patches/smol.diff b/patches/smol.diff
 2new file mode 100644
 3index 0000000..ae2df55
 4--- /dev/null
 5+++ b/patches/smol.diff
 6@@ -0,0 +1,53 @@
 7+diff --git a/static/smol.css b/static/smol.css
 8+index e9b59ec..9e9d925 100644
 9+--- a/static/smol.css
10++++ b/static/smol.css
11+@@ -15,48 +15,6 @@
12+   box-shadow: none;
13+ }
14+ 
15+-@media (prefers-color-scheme: light) {
16+-  :root {
17+-    --main-hue: 250;
18+-    --white: #2e3f53;
19+-    --white-light: #cfe0f4;
20+-    --white-dark: #6c6a6a;
21+-    --code: #52576f;
22+-    --pre: #e1e7ee;
23+-    --bg-color: #f4f4f4;
24+-    --text-color: #24292f;
25+-    --link-color: #005cc5;
26+-    --visited: #6f42c1;
27+-    --blockquote: #005cc5;
28+-    --blockquote-bg: #cfe0f4;
29+-    --hover: #c11e7a;
30+-    --grey: #ccc;
31+-    --grey-light: #6a708e;
32+-    --shadow: #e8e8e8;
33+-  }
34+-}
35+-
36+-@media (prefers-color-scheme: dark) {
37+-  :root {
38+-    --main-hue: 250;
39+-    --white: #f2f2f2;
40+-    --white-light: #f2f2f2;
41+-    --white-dark: #e8e8e8;
42+-    --code: #414558;
43+-    --pre: #252525;
44+-    --bg-color: #282a36;
45+-    --text-color: #f2f2f2;
46+-    --link-color: #8be9fd;
47+-    --visited: #bd93f9;
48+-    --blockquote: #bd93f9;
49+-    --blockquote-bg: #353548;
50+-    --hover: #ff80bf;
51+-    --grey: #414558;
52+-    --grey-light: #6a708e;
53+-    --shadow: #252525;
54+-  }
55+-}
56+-
57+ html {
58+   background-color: var(--bg-color);
59+   color: var(--text-color);
ssh.go link
+1 -1
 1diff --git a/ssh.go b/ssh.go
 2index 1fb34d0..3e5e407 100644
 3--- a/ssh.go
 4+++ b/ssh.go
 5@@ -35,7 +35,7 @@ func GitSshServer(cfg *GitCfg) {
 6 	dbpath := filepath.Join(cfg.DataDir, "pr.db")
 7 	dbh, err := Open(dbpath, cfg.Logger)
 8 	if err != nil {
 9-		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
10+		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
11 	}
12 
13 	be := &Backend{
static/smol.css link
+726 -0
  1diff --git a/static/smol.css b/static/smol.css
  2new file mode 100644
  3index 0000000..9e9d925
  4--- /dev/null
  5+++ b/static/smol.css
  6@@ -0,0 +1,726 @@
  7+*,
  8+::before,
  9+::after {
 10+  box-sizing: border-box;
 11+}
 12+
 13+::-moz-focus-inner {
 14+  border-style: none;
 15+  padding: 0;
 16+}
 17+:-moz-focusring {
 18+  outline: 1px dotted ButtonText;
 19+}
 20+:-moz-ui-invalid {
 21+  box-shadow: none;
 22+}
 23+
 24+html {
 25+  background-color: var(--bg-color);
 26+  color: var(--text-color);
 27+  font-size: 18px;
 28+  line-height: 1.5;
 29+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
 30+    Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
 31+    sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
 32+  -webkit-text-size-adjust: 100%;
 33+  -moz-tab-size: 4;
 34+  -o-tab-size: 4;
 35+  tab-size: 4;
 36+}
 37+
 38+body {
 39+  margin: 0 auto;
 40+}
 41+
 42+img {
 43+  max-width: 100%;
 44+  height: auto;
 45+}
 46+
 47+b,
 48+strong {
 49+  font-weight: bold;
 50+}
 51+
 52+code,
 53+kbd,
 54+samp,
 55+pre {
 56+  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
 57+    monospace;
 58+}
 59+
 60+code,
 61+kbd,
 62+samp {
 63+  border: 2px solid var(--code);
 64+}
 65+
 66+pre > code {
 67+  background-color: inherit;
 68+  padding: 0;
 69+  border: none;
 70+}
 71+
 72+code {
 73+  font-size: 90%;
 74+  border-radius: 0.3rem;
 75+  padding: 0.1rem 0.3rem;
 76+}
 77+
 78+pre {
 79+  font-size: 14px;
 80+  border-radius: 5px;
 81+  padding: 1rem;
 82+  margin: 1rem 0;
 83+  overflow-x: auto;
 84+  background-color: var(--pre) !important;
 85+}
 86+
 87+small {
 88+  font-size: 0.8rem;
 89+}
 90+
 91+summary {
 92+  display: list-item;
 93+  cursor: pointer;
 94+}
 95+
 96+h1,
 97+h2,
 98+h3,
 99+h4 {
100+  margin: 0;
101+  padding: 0.5rem 0 0 0;
102+  border: 0;
103+  font-style: normal;
104+  font-weight: inherit;
105+  font-size: inherit;
106+}
107+
108+path {
109+  fill: var(--text-color);
110+  stroke: var(--text-color);
111+}
112+
113+hr {
114+  color: inherit;
115+  border: 0;
116+  margin: 0;
117+  height: 2px;
118+  background: var(--grey);
119+  margin: 1rem auto;
120+  text-align: center;
121+}
122+
123+a {
124+  text-decoration: none;
125+  color: var(--link-color);
126+}
127+
128+a:hover,
129+a:visited:hover {
130+  text-decoration: underline;
131+  color: var(--hover);
132+}
133+
134+a:visited {
135+  color: var(--visited);
136+}
137+
138+a.link-grey {
139+  text-decoration: underline;
140+  color: var(--white);
141+}
142+
143+a.link-grey:visited {
144+  color: var(--white);
145+}
146+
147+section {
148+  margin-bottom: 1.4rem;
149+}
150+
151+section:last-child {
152+  margin-bottom: 0;
153+}
154+
155+header {
156+  margin: 1rem auto;
157+}
158+
159+p {
160+  margin: 0.5rem 0;
161+}
162+
163+article {
164+  overflow-wrap: break-word;
165+}
166+
167+blockquote {
168+  border-left: 5px solid var(--blockquote);
169+  background-color: var(--blockquote-bg);
170+  padding: 0.5rem 0.75rem;
171+  margin: 0.5rem 0;
172+}
173+
174+blockquote > p {
175+  margin: 0;
176+}
177+
178+blockquote code {
179+  border: 1px solid var(--blockquote);
180+}
181+
182+ul,
183+ol {
184+  padding: 0 0 0 1rem;
185+  list-style-position: outside;
186+}
187+
188+ul[style*="list-style-type: none;"] {
189+  padding: 0;
190+}
191+
192+li {
193+  margin: 0.5rem 0;
194+}
195+
196+li > pre {
197+  padding: 0;
198+}
199+
200+footer {
201+  text-align: center;
202+  margin-bottom: 4rem;
203+}
204+
205+dt {
206+  font-weight: bold;
207+}
208+
209+dd {
210+  margin-left: 0;
211+}
212+
213+dd:not(:last-child) {
214+  margin-bottom: 0.5rem;
215+}
216+
217+figure {
218+  margin: 0;
219+}
220+
221+.container {
222+  max-width: 50em;
223+  width: 100%;
224+}
225+
226+.container-sm {
227+  max-width: 40em;
228+  width: 100%;
229+}
230+
231+.container-center {
232+  width: 100%;
233+  height: 100%;
234+  display: flex;
235+  justify-content: center;
236+}
237+
238+.mono {
239+  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
240+    monospace;
241+}
242+
243+.link-alt-adj,
244+.link-alt-adj:visited,
245+.link-alt-adj:visited:hover,
246+.link-alt-adj:hover {
247+  color: var(--link-color);
248+  text-decoration: none;
249+}
250+
251+.link-alt-adj:visited:hover,
252+.link-alt-adj:hover {
253+  text-decoration: underline;
254+}
255+
256+.link-alt-hover,
257+.link-alt-hover:visited,
258+.link-alt-hover:visited:hover,
259+.link-alt-hover:hover {
260+  color: var(--hover);
261+  text-decoration: none;
262+}
263+
264+.link-alt-hover:visited:hover,
265+.link-alt-hover:hover {
266+  text-decoration: underline;
267+}
268+
269+.link-alt,
270+.link-alt:visited,
271+.link-alt:visited:hover,
272+.link-alt:hover {
273+  color: var(--white);
274+  text-decoration: none;
275+}
276+
277+.link-alt:visited:hover,
278+.link-alt:hover {
279+  text-decoration: underline;
280+}
281+
282+.text-3xl {
283+  font-size: 2.5rem;
284+}
285+
286+.text-2xl {
287+  font-size: 1.9rem;
288+  line-height: 1.15;
289+}
290+
291+.text-xl {
292+  font-size: 1.55rem;
293+  line-height: 1.15;
294+}
295+
296+.text-lg {
297+  font-size: 1.35rem;
298+  line-height: 1.15;
299+}
300+
301+.text-md {
302+  font-size: 1.15rem;
303+  line-height: 1.15;
304+}
305+
306+.text-sm {
307+  font-size: 0.875rem;
308+}
309+
310+.text-xs {
311+  font-size: 0.775rem;
312+}
313+
314+.cursor-pointer {
315+  cursor: pointer;
316+}
317+
318+.w-full {
319+  width: 100%;
320+}
321+
322+.h-full {
323+  height: 100%;
324+}
325+
326+.border {
327+  border: 2px solid var(--grey-light);
328+}
329+
330+.text-left {
331+  text-align: left;
332+}
333+
334+.text-center {
335+  text-align: center;
336+}
337+
338+.text-underline {
339+  border-bottom: 3px solid var(--text-color);
340+  padding-bottom: 3px;
341+}
342+
343+.text-hdr {
344+  color: var(--hover);
345+}
346+
347+.text-underline-hdr {
348+  border-bottom: 3px solid var(--hover);
349+  padding-bottom: 3px;
350+}
351+
352+.font-bold {
353+  font-weight: bold;
354+}
355+
356+.font-italic {
357+  font-style: italic;
358+}
359+
360+.inline {
361+  display: inline;
362+}
363+
364+.inline-block {
365+  display: inline-block;
366+}
367+
368+.max-w-half {
369+  max-width: 50%;
370+}
371+
372+.h-screen {
373+  height: 100vh;
374+}
375+
376+.w-screen {
377+  width: 100vw;
378+}
379+
380+.flex {
381+  display: flex;
382+}
383+
384+.flex-col {
385+  flex-direction: column;
386+}
387+
388+.items-center {
389+  align-items: center;
390+}
391+
392+.m-0 {
393+  margin: 0;
394+}
395+
396+.mt {
397+  margin-top: 0.5rem;
398+}
399+
400+.mt-2 {
401+  margin-top: 1rem;
402+}
403+
404+.mt-4 {
405+  margin-top: 2rem;
406+}
407+
408+.mt-8 {
409+  margin-top: 4rem;
410+}
411+
412+.mb {
413+  margin-bottom: 0.5rem;
414+}
415+
416+.mb-2 {
417+  margin-bottom: 1rem;
418+}
419+
420+.mb-4 {
421+  margin-bottom: 2rem;
422+}
423+
424+.mb-8 {
425+  margin-bottom: 4rem;
426+}
427+
428+.mb-16 {
429+  margin-bottom: 8rem;
430+}
431+
432+.mr {
433+  margin-right: 0.5rem;
434+}
435+
436+.ml-sm {
437+  margin-left: 0.25rem;
438+}
439+
440+.ml {
441+  margin-left: 0.5rem;
442+}
443+
444+.pt-0 {
445+  padding-top: 0;
446+}
447+
448+.my {
449+  margin-top: 0.5rem;
450+  margin-bottom: 0.5rem;
451+}
452+
453+.my-2 {
454+  margin-top: 1rem;
455+  margin-bottom: 1rem;
456+}
457+
458+.my-4 {
459+  margin-top: 2rem;
460+  margin-bottom: 2rem;
461+}
462+
463+.my-8 {
464+  margin-top: 4rem;
465+  margin-bottom: 4rem;
466+}
467+
468+.mx {
469+  margin-left: 0.5rem;
470+  margin-right: 0.5rem;
471+}
472+
473+.mx-2 {
474+  margin-left: 1rem;
475+  margin-right: 1rem;
476+}
477+
478+.m-1 {
479+  margin: 0.5rem;
480+}
481+
482+.p-1 {
483+  padding: 0.5rem;
484+}
485+
486+.p-0 {
487+  padding: 0;
488+}
489+
490+.px-2 {
491+  padding-left: 1rem;
492+  padding-right: 1rem;
493+}
494+
495+.px-4 {
496+  padding-left: 2rem;
497+  padding-right: 2rem;
498+}
499+
500+.py {
501+  padding-top: 0.5rem;
502+  padding-bottom: 0.5rem;
503+}
504+
505+.py-2 {
506+  padding-top: 1rem;
507+  padding-bottom: 1rem;
508+}
509+
510+.py-4 {
511+  padding-top: 2rem;
512+  padding-bottom: 2rem;
513+}
514+
515+.py-8 {
516+  padding-top: 4rem;
517+  padding-bottom: 4rem;
518+}
519+
520+.justify-between {
521+  justify-content: space-between;
522+}
523+
524+.justify-center {
525+  justify-content: center;
526+}
527+
528+.gap {
529+  gap: 0.5rem;
530+}
531+
532+.gap-2 {
533+  gap: 1rem;
534+}
535+
536+.group {
537+  display: flex;
538+  flex-direction: column;
539+  gap: 0.5rem;
540+}
541+
542+.group-2 {
543+  display: flex;
544+  flex-direction: column;
545+  gap: 1rem;
546+}
547+
548+.group-h {
549+  display: flex;
550+  gap: 0.5rem;
551+  align-items: center;
552+}
553+
554+.flex-1 {
555+  flex: 1;
556+}
557+
558+.items-end {
559+  align-items: end;
560+}
561+
562+.items-start {
563+  align-items: start;
564+}
565+
566+.justify-end {
567+  justify-content: end;
568+}
569+
570+.font-grey-light {
571+  color: var(--grey-light);
572+}
573+
574+.hidden {
575+  display: none;
576+}
577+
578+.align-right {
579+  text-align: right;
580+}
581+
582+/* ==== MARKDOWN ==== */
583+
584+.md h1,
585+.md h2,
586+.md h3,
587+.md h4 {
588+  padding: 0;
589+  margin: 1.5rem 0 0.9rem 0;
590+  font-weight: bold;
591+}
592+
593+.md h1 a,
594+.md h2 a,
595+.md h3 a,
596+.md h4 a {
597+  color: var(--grey-light);
598+  text-decoration: none;
599+}
600+
601+.md h1 {
602+  font-size: 1.6rem;
603+  line-height: 1.15;
604+  border-bottom: 2px solid var(--grey);
605+  padding-bottom: 0.7rem;
606+}
607+
608+.md h2 {
609+  font-size: 1.3rem;
610+  line-height: 1.15;
611+  color: var(--white-dark);
612+}
613+
614+.md h3 {
615+  font-size: 1.2rem;
616+  color: var(--white-dark);
617+}
618+
619+.md h4 {
620+  font-size: 1rem;
621+  color: var(--white-dark);
622+}
623+
624+/* ==== HELPERS ==== */
625+
626+.logo-header {
627+  line-height: 1;
628+  display: inline-block;
629+  background-color: #FF79C6;
630+  background-image: linear-gradient(to right, #FF5555, #FF79C6, #F8F859);
631+  color: transparent;
632+  background-clip: text;
633+  border: 3px solid #FF79C6;
634+  padding: 8px 10px 10px 10px;
635+  border-radius: 10px;
636+  box-shadow: 0px 5px 0px 0px var(--shadow);
637+  background-size: 100%;
638+  -webkit-background-clip: text;
639+  -moz-background-clip: text;
640+  -webkit-text-fill-color: transparent;
641+  -moz-text-fill-color: transparent;
642+}
643+
644+.btn {
645+  border: 2px solid var(--link-color);
646+  color: var(--link-color);
647+  padding: 0.4rem 1rem;
648+  font-weight: bold;
649+  display: inline-block;
650+}
651+
652+.btn-link,
653+.btn-link:visited {
654+  border: 2px solid var(--link-color);
655+  color: var(--link-color);
656+  padding: 0.4rem 1rem;
657+  text-decoration: none;
658+  font-weight: bold;
659+  display: inline-block;
660+}
661+
662+.btn-link:visited:hover,
663+.btn-link:hover {
664+  border: 2px solid var(--hover);
665+}
666+
667+.btn-link-alt,
668+.btn-link-alt:visited {
669+  border: 2px solid var(--white);
670+  color: var(--white);
671+}
672+
673+.box {
674+  border: 2px solid var(--grey-light);
675+  padding: 0.5rem 0.75rem;
676+}
677+
678+.box-sm {
679+  border: 2px solid var(--grey-light);
680+  padding: 0.15rem 0.35rem;
681+}
682+
683+.box-alert {
684+  border: 2px solid var(--hover);
685+  padding: 0.5rem 0.75rem;
686+}
687+
688+.box-sm-alert {
689+  border: 2px solid var(--hover);
690+  padding: 0.15rem 0.35rem;
691+}
692+
693+.list-none {
694+  list-style-type: none;
695+}
696+
697+.list-disc {
698+  list-style-type: disc;
699+}
700+
701+.list-decimal {
702+  list-style-type: decimal;
703+}
704+
705+.pill {
706+  border: 1px solid var(--link-color);
707+  color: var(--link-color);
708+}
709+
710+.pill-alert {
711+  border: 1px solid var(--hover);
712+  color: var(--hover);
713+}
714+
715+.pill-info {
716+  border: 1px solid var(--visited);
717+  color: var(--visited);
718+}
719+
720+@media only screen and (max-width: 40em) {
721+  body {
722+    padding: 0 1rem;
723+  }
724+
725+  header {
726+    margin: 0;
727+  }
728+
729+  .flex-collapse {
730+    flex-direction: column;
731+  }
732+}
static/vars.css link
+18 -0
 1diff --git a/static/vars.css b/static/vars.css
 2new file mode 100644
 3index 0000000..6300534
 4--- /dev/null
 5+++ b/static/vars.css
 6@@ -0,0 +1,18 @@
 7+:root {
 8+  --main-hue: 250;
 9+  --white: #f2f2f2;
10+  --white-light: #f2f2f2;
11+  --white-dark: #e8e8e8;
12+  --code: #414558;
13+  --pre: #252525;
14+  --bg-color: #282a36;
15+  --text-color: #f2f2f2;
16+  --link-color: #8be9fd;
17+  --visited: #bd93f9;
18+  --blockquote: #bd93f9;
19+  --blockquote-bg: #353548;
20+  --hover: #ff80bf;
21+  --grey: #414558;
22+  --grey-light: #6a708e;
23+  --shadow: #252525;
24+}
tmpl/base.html link
+3 -2
 1diff --git a/tmpl/base.html b/tmpl/base.html
 2index a656cd5..b5f5fb3 100644
 3--- a/tmpl/base.html
 4+++ b/tmpl/base.html
 5@@ -9,8 +9,9 @@
 6     <meta name="keywords" content="git, collaboration, patch, requests" />
 7     {{template "meta" .}}
 8 
 9-    <link rel="stylesheet" href="https://pico.sh/smol.css" />
10-    <link rel="stylesheet" href="https://pico.sh/syntax.css" />
11+    <link rel="stylesheet" href="/static/smol.css" />
12+    <link rel="stylesheet" href="/static/vars.css" />
13+    <link rel="stylesheet" href="/syntax.css" />
14   </head>
15   <body class="container">{{template "body" .}}</body>
16 </html>
web.go link
+68 -1
  1diff --git a/web.go b/web.go
  2index 4e782c8..99bfec6 100644
  3--- a/web.go
  4+++ b/web.go
  5@@ -6,8 +6,12 @@ import (
  6 	"embed"
  7 	"fmt"
  8 	"html/template"
  9+	"io"
 10+	"io/fs"
 11 	"log/slog"
 12+	"mime"
 13 	"net/http"
 14+	"os"
 15 	"path/filepath"
 16 	"slices"
 17 	"strconv"
 18@@ -23,6 +27,9 @@ import (
 19 //go:embed tmpl/*
 20 var tmplFS embed.FS
 21 
 22+//go:embed static/*
 23+var staticFS embed.FS
 24+
 25 type WebCtx struct {
 26 	Pr        *PrCmd
 27 	Backend   *Backend
 28@@ -618,13 +625,68 @@ func chromaStyleHandler(w http.ResponseWriter, r *http.Request) {
 29 	}
 30 }
 31 
 32+func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
 33+	return func(w http.ResponseWriter, r *http.Request) {
 34+		web, err := getWebCtx(r)
 35+		if err != nil {
 36+			w.WriteHeader(http.StatusUnprocessableEntity)
 37+			return
 38+		}
 39+		logger := web.Logger
 40+
 41+		file := r.PathValue("file")
 42+		logger.Info("serving file", "file", file, "fs", staticfs)
 43+		reader, err := staticfs.Open(file)
 44+		if err != nil {
 45+			logger.Error(err.Error())
 46+			http.Error(w, "file not found", 404)
 47+			return
 48+		}
 49+		contents, err := io.ReadAll(reader)
 50+		if err != nil {
 51+			logger.Error(err.Error())
 52+			http.Error(w, "file not found", 404)
 53+			return
 54+		}
 55+		contentType := mime.TypeByExtension(filepath.Ext(file))
 56+		if contentType == "" {
 57+			contentType = http.DetectContentType(contents)
 58+		}
 59+		w.Header().Add("Content-Type", contentType)
 60+
 61+		_, err = w.Write(contents)
 62+		if err != nil {
 63+			logger.Error(err.Error())
 64+			http.Error(w, "server error", 500)
 65+			return
 66+		}
 67+	}
 68+}
 69+
 70+func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
 71+	dir := filepath.Join(datadir, dirName)
 72+	_, err := os.Stat(dir)
 73+	if err == nil {
 74+		logger.Info("found folder in data_dir", "dir", dir)
 75+		return os.DirFS(dir), nil
 76+	}
 77+
 78+	logger.Info("using embeded folder", "dir", dir)
 79+	fsys, err := fs.Sub(ffs, dirName)
 80+	if err != nil {
 81+		return nil, err
 82+	}
 83+
 84+	return fsys, nil
 85+}
 86+
 87 func StartWebServer(cfg *GitCfg) {
 88 	addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
 89 
 90 	dbpath := filepath.Join(cfg.DataDir, "pr.db")
 91 	dbh, err := Open(dbpath, cfg.Logger)
 92 	if err != nil {
 93-		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
 94+		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
 95 	}
 96 
 97 	be := &Backend{
 98@@ -659,6 +721,11 @@ func StartWebServer(cfg *GitCfg) {
 99 	http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
100 	http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
101 	http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
102+	filesys, err := getFileSystem(cfg.Logger, staticFS, cfg.DataDir, "static")
103+	if err != nil {
104+		panic(err)
105+	}
106+	http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(filesys)))
107 
108 	cfg.Logger.Info("starting web server", "addr", addr)
109 	err = http.ListenAndServe(addr, nil)

review: typo and future enhancement comment

Signed-off-by: jolheiser <git@jolheiser.com>
web.go link
+3 -0
 1diff --git a/web.go b/web.go
 2index 99bfec6..022c259 100644
 3--- a/web.go
 4+++ b/web.go
 5@@ -663,6 +663,9 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
 6 	}
 7 }
 8 
 9+// review(jolheiser):
10+// Perhaps in the future this could be extended to support per-file. For example, I may want to override CSS vars without providing an entire static assets.
11+// embeded -> embedded
12 func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
13 	dir := filepath.Join(datadir, dirName)
14 	_, err := os.Stat(dir)

refactor: per-file override for static folder

web.go link
+27 -15
 1diff --git a/web.go b/web.go
 2index 022c259..9419dea 100644
 3--- a/web.go
 4+++ b/web.go
 5@@ -28,7 +28,7 @@ import (
 6 var tmplFS embed.FS
 7 
 8 //go:embed static/*
 9-var staticFS embed.FS
10+var embedStaticFS embed.FS
11 
12 type WebCtx struct {
13 	Pr        *PrCmd
14@@ -625,7 +625,7 @@ func chromaStyleHandler(w http.ResponseWriter, r *http.Request) {
15 	}
16 }
17 
18-func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
19+func serveFile(userfs fs.FS, embedfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
20 	return func(w http.ResponseWriter, r *http.Request) {
21 		web, err := getWebCtx(r)
22 		if err != nil {
23@@ -635,13 +635,26 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
24 		logger := web.Logger
25 
26 		file := r.PathValue("file")
27-		logger.Info("serving file", "file", file, "fs", staticfs)
28-		reader, err := staticfs.Open(file)
29+
30+		logger.Info("serving file", "file", file)
31+		// merging both embedded fs and whatever user provides
32+		var reader fs.File
33+		if userfs == nil {
34+			reader, err = embedfs.Open(file)
35+		} else {
36+			reader, err = userfs.Open(file)
37+			if err != nil {
38+				// serve embeded static folder
39+				reader, err = embedfs.Open(file)
40+			}
41+		}
42+
43 		if err != nil {
44 			logger.Error(err.Error())
45 			http.Error(w, "file not found", 404)
46 			return
47 		}
48+
49 		contents, err := io.ReadAll(reader)
50 		if err != nil {
51 			logger.Error(err.Error())
52@@ -663,23 +676,20 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
53 	}
54 }
55 
56-// review(jolheiser):
57-// Perhaps in the future this could be extended to support per-file. For example, I may want to override CSS vars without providing an entire static assets.
58-// embeded -> embedded
59-func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
60+func getUserDefinedFS(datadir, dirName string) fs.FS {
61 	dir := filepath.Join(datadir, dirName)
62 	_, err := os.Stat(dir)
63-	if err == nil {
64-		logger.Info("found folder in data_dir", "dir", dir)
65-		return os.DirFS(dir), nil
66+	if err != nil {
67+		return nil
68 	}
69+	return os.DirFS(dir)
70+}
71 
72-	logger.Info("using embeded folder", "dir", dir)
73+func getEmbedFS(ffs embed.FS, dirName string) (fs.FS, error) {
74 	fsys, err := fs.Sub(ffs, dirName)
75 	if err != nil {
76 		return nil, err
77 	}
78-
79 	return fsys, nil
80 }
81 
82@@ -724,11 +734,13 @@ func StartWebServer(cfg *GitCfg) {
83 	http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
84 	http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
85 	http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
86-	filesys, err := getFileSystem(cfg.Logger, staticFS, cfg.DataDir, "static")
87+	embedFS, err := getEmbedFS(embedStaticFS, "static")
88 	if err != nil {
89 		panic(err)
90 	}
91-	http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(filesys)))
92+	userFS := getUserDefinedFS(cfg.DataDir, "static")
93+
94+	http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(userFS, embedFS)))
95 
96 	cfg.Logger.Info("starting web server", "addr", addr)
97 	err = http.ListenAndServe(addr, nil)