WIP: add css from stasis

This commit is contained in:
Catalin Constantin Mititiuc 2025-06-16 20:18:04 -07:00
parent d334aaf003
commit c87a2314c5
15 changed files with 1146 additions and 6 deletions

View File

@ -50,3 +50,9 @@
$ docker run --rm -it --init -w /opt -v $PWD:/opt -p 8080:8080 sitegen-openresty
### visit `localhost:8080` in web browser
## todo
* treesitter highlighting for moonscript
* penlight library
* sitegen plugins

734
app.css Normal file
View File

@ -0,0 +1,734 @@
/*@import "tailwindcss/base";*/
/*@import "tailwindcss/components";*/
/*@import "tailwindcss/utilities";*/
/* This file is for your main application CSS */
/*styles.css*/
/*tones #ff2a2a*/
/*ff2a2a*/
/*ef3535*/
/*df3f3f*/
/*cf4a4a*/
/*bf5555*/
/*af5f5f*/
/*9f6a6a*/
/*8f7575*/
/*tints*/
/*.color-1 {color: #ff2a2a;}*/
/*.color-2 {color: #ff4545;}*/
/*.color-3 {color: #ff5f5f;}*/
/*.color-4 {color: #ff7a7a;}*/
/*.color-5 {color: #ff9595;}*/
/*.color-6 {color: #ffafaf;}*/
/*.color-7 {color: #ffcaca;}*/
/*.color-8 {color: #ffe4e4;}*/
a[href]:link {
color: #ff2a2a;
text-decoration: none;
}
a[href]:visited {
color: dodgerblue;
color: skyblue;
color: #FF5733;
color: #900C3F;
/*tones*/
/*color: #ff2a2a;*/
/*color: #ef3535;*/
/*color: #df3f3f;*/
color: #cf4a4a;
/*color: #bf5555;*/
/*color: #af5f5f;*/
/*color: #9f6a6a;*/
/*color: #8f7575;*/
/*tints*/
/*color: #ff2a2a;*/
/*color: #ff4545;*/
/*color: #ff5f5f;*/
/*color: #ff7a7a;*/
/*color: #ff9595;*/
/*color: #ffafaf;*/
/*color: #ffcaca;*/
/*color: #ffe4e4;*/
}
a[href]:hover {
text-decoration: underline;
color: lightblue;
color: #FF5733;
/*tints*/
/*color: #ff2a2a;*/
/*color: #ff4545;*/
/*color: #ff5f5f;*/
color: #ff7a7a;
/*color: #ff9595;*/
/*color: #ffafaf;*/
/*color: #ffcaca;*/
/*color: #ffe4e4;*/
}
a[href]:active {
/*tints*/
/*color: #ff2a2a;*/
/*color: #ff4545;*/
/*color: #ff5f5f;*/
/*color: #ff7a7a;*/
color: #ff9595;
/*color: #ffafaf;*/
/*color: #ffcaca;*/
/*color: #ffe4e4;*/
}
header a[href]:visited {
color: #ff2a2a;
}
/*header a[href]:hover {*/
/* color: #FF5733;*/
/*}*/
/**/
/*header a[href]:active {*/
/* color: #FFC300;*/
/*}*/
.app-source-code-link {
font-size: smaller;
font-weight: normal;
margin-left: 0.5em;
float: right;
}
body {
font: 0.95em/1.692307em 'Bitter', Georgia, 'Times New Roman', Times, serif;
color: #24292e;
display: flex;
margin: 0 auto;
padding: 0;
flex-wrap: wrap;
max-width: 1160px;
flex-flow: column;
}
header {
padding: 0 1em;
text-align: center;
flex-grow: 1;
}
header > dl > dt > a[rel="author"] {
font-weight: bold;
font-size: x-large;
}
nav {
font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
}
header > dl > dd {
font-family: Monaco, monospace, Courier, "Courier New";
margin: 0;
font-size: smaller;
}
header > dl {
margin-bottom: 3em;
}
nav > ul > li {
text-transform: uppercase;
}
main {
text-align: center;
flex-grow: 1;
padding: 0 1em;
}
main * {
text-align: left;
margin-left: auto;
margin-right: auto;
box-sizing: border-box;
}
main > div {
display: inline-block;
}
section, h2 {
max-width: 650px;
box-sizing: border-box;
}
section {
padding-bottom: 1em;
}
section + section {
padding-bottom: 1em;
}
section:first-child h3 {
margin-top: 0.83em;
}
section:last-child {
padding-bottom: 0;
}
section > h3 {
margin-top: 0;
margin-bottom: 0;
}
section + section > h3 {
margin-top: 1em;
}
section > time {
font-weight: bold;
font-size: smaller;
/*color: #99C300;*/
/*color: #FFC300;*/
/*color: #ffca2a;*/
/*color: #b7df3f;*/
/*color: #aecf4a;*/
/*color: #a5bf55;*/
/*color: #9baf5f;*/
/*color: #caff2a;*/
/*color: #b1df25;*/
color: #98bf1f;
/*color: #dfb125;*/
/*color: #bf981f;*/
}
section p {
margin-top: 0.4em;
}
article > * {
max-width: 600px;
margin-left: auto;
margin-right: auto;
box-sizing: border-box;
}
article > time {
display: block;
text-align: center;
margin-bottom: 1.5em;
}
section > time, article > time {
font-family: sans-serif;
}
article > h2:first-child {
text-align: center;
}
article h2, article h3, article h4, article h5 {
margin-top: 2em;
}
.info {
border: 1px solid slategray;
border-radius: 8px;
padding: 0 1em;
background-color: #ddf0f0;
font-family: sans-serif;
color: darkslategray;
}
.info > p:first-child:before {
content: "ⓘ";
float: left;
font-size: xx-large;
color: slategray;
margin-right: 0.25em;
}
.warning {
border: 1px solid orange;
border-radius: 8px;
padding: 0 1em;
background-color: beige;
font-family: sans-serif;
color: darkslategray;
}
.warning > p:first-child:before {
content: "⚠";
float: left;
font-size: xx-large;
color: orange;
margin-right: 0.25em;
}
figure {
text-align: center;
}
figure img {
max-width: 100%;
}
figure figcaption {
text-align: center;
}
pre {
background-color: aliceblue;
padding: 1em;
border: 1px solid lightgray;
border-radius: 3px;
overflow: auto;
}
:not(pre) > code {
background-color: rgb(230, 232, 245);
}
.code-filename-label, .filename-for-code-block > p {
font-weight: bold;
margin-bottom: 0;
}
.code-filename-label code, .filename-for-code-block code {
background-color: transparent;
}
.code-filename-label + pre,
.code-filename-label + div.sourceCode,
.filename-for-code-block + pre,
.filename-for-code-block + div.sourceCode {
margin-top: 0;
}
code {
font-size: 0.85em;
font-family: Monaco, monospace, Courier, "Courier New";
}
li p img {
float: left;
margin-bottom: 40px;
}
li, p {
clear: both;
}
address {
text-align: center;
font-style: normal;
}
blockquote {
border-left: 5px solid #ccc;
padding: 0.1em 0;
padding-left: 1em;
}
footer {
padding: 1em;
font-size: smaller;
font-family: sans-serif;
margin-top: 0;
white-space: nowrap;
color: lightgray;
}
footer ul, nav ul {
margin: 0;
padding-left: 1em;
list-style-type: none;
}
.read-post-link {
white-space: nowrap;
}
/*@media (max-width: 1100px) {*/
footer {
flex-basis: 100%;
text-align: center;
padding-top: 3em;
white-space: normal;
}
footer ul {
list-style: none;
margin-left: 0;
padding-left: 0;
}
footer li, footer ul {
display: inline;
}
footer li::after {
content: ", ";
}
footer li:last-child::after {
content: "";
}
/*}*/
/*@media (max-width: 950px) {*/
nav ul {
list-style: none;
margin-left: 0;
padding-left: 0;
}
nav li, nav ul {
display: inline;
}
nav li + li {
margin-left: 0.25em;
}
.container {
white-space: normal;
}
header {
padding-bottom: 1em;
margin-bottom: 1em;
}
header > a[rel="author"] {
margin-top: 15px;
}
section:first-child h2 {
margin-top: 0;
}
address {
text-align: left;
}
header > dl {
margin-bottom: 1em;
line-height: 1.2em;
}
/*}*/
table > caption {
text-align: center;
font-weight: bold;
font-variant-caps: small-caps;
}
/*highlighting.css*/
pre > code.sourceCode {
white-space: pre;
position: relative;
}
pre > code.sourceCode > span {
display: inline-block;
line-height: 1.25;
}
pre > code.sourceCode > span:empty {
height: 1.2em;
}
code.sourceCode > span {
color: inherit;
text-decoration: inherit;
}
div.sourceCode {
margin: 1em auto;
}
pre.sourceCode {
margin: 0;
}
@media screen {
div.sourceCode {
overflow: auto;
}
}
@media print {
pre > code.sourceCode {
white-space: pre-wrap;
}
pre > code.sourceCode > span {
text-indent: -5em;
padding-left: 5em;
}
}
pre.numberSource code
{
counter-reset: source-line 0;
}
pre.numberSource code > span
{
position: relative;
left: -4em;
counter-increment: source-line;
}
pre.numberSource code > span > a:first-child::before
{
content: counter(source-line);
position: relative;
left: -1em;
text-align: right;
vertical-align: baseline;
border: none;
display: inline-block;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
padding: 0 4px;
width: 4em;
color: #aaaaaa;
}
pre.numberSource {
margin-left: 3em;
border-left: 1px solid #aaaaaa;
padding-left: 4px;
}
div.sourceCode
{
}
@media screen {
pre > code.sourceCode > span > a:first-child::before {
text-decoration: underline;
}
}
code span.al {
color: #ff0000;
font-weight: bold;
}
/* Alert */
code span.an {
color: #60a0b0;
font-weight: bold;
font-style: italic;
}
/* Annotation */
code span.at {
color: #7d9029;
}
/* Attribute */
code span.bn {
color: #40a070;
}
/* BaseN */
code span.bu {
}
/* BuiltIn */
code span.cf {
color: #007020;
font-weight: bold;
}
/* ControlFlow */
code span.ch {
color: #4070a0;
}
/* Char */
code span.cn {
color: #880000;
}
/* Constant */
code span.co {
color: #60a0b0;
font-style: italic;
}
/* Comment */
code span.cv {
color: #60a0b0;
font-weight: bold;
font-style: italic;
}
/* CommentVar */
code span.do {
color: #ba2121;
font-style: italic;
}
/* Documentation */
code span.dt {
color: #902000;
}
/* DataType */
code span.dv {
color: #40a070;
}
/* DecVal */
code span.er {
color: #ff0000;
font-weight: bold;
}
/* Error */
code span.ex {
}
/* Extension */
code span.fl {
color: #40a070;
}
/* Float */
code span.fu {
color: #06287e;
}
/* Function */
code span.im {
}
/* Import */
code span.in {
color: #60a0b0;
font-weight: bold;
font-style: italic;
}
/* Information */
code span.kw {
color: #007020;
font-weight: bold;
}
/* Keyword */
code span.op {
color: #666666;
}
/* Operator */
code span.ot {
color: #007020;
}
/* Other */
code span.pp {
color: #bc7a00;
}
/* Preprocessor */
code span.sc {
color: #4070a0;
}
/* SpecialChar */
code span.ss {
color: #bb6688;
}
/* SpecialString */
code span.st {
color: #4070a0;
}
/* String */
code span.va {
color: #19177c;
}
/* Variable */
code span.vs {
color: #4070a0;
}
/* VerbatimString */
code span.wa {
color: #60a0b0;
font-weight: bold;
font-style: italic;
}
/* Warning */

89
conf/mime.types Normal file
View File

@ -0,0 +1,89 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
image/svg+xml svg svgz;
image/webp webp;
application/font-woff woff;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.wap.wmlc wmlc;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
application/vnd.openxmlformats-officedocument.wordprocessingml.document docx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx;
application/vnd.openxmlformats-officedocument.presentationml.presentation pptx;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

View File

@ -5,14 +5,21 @@ events {
}
http {
server {
root /opt/www;
listen 8080;
include mime.types;
charset utf-8;
location / {
default_type text/html;
# content_by_lua_block {
# ngx.say("<p>hello, world</p>")
# }
}
location /css {
expires 1h;
alias css;
}
}
}

View File

@ -0,0 +1,132 @@
## Introduction
We wish to print a list of all the files in a directory, ignoring files beginning with certain characters, sorted alphabetically, with the directories last.
## Finding the files
### Code
Finds all files in the `path` directory recursively, ignoring any files or directories that start with any of the characters in the `@starts_with` module attribute.
```
defmodule Files do
@starts_with [".", "_"]
def find(path \\ "."), do: find(path, list_contents(path))
defp find(dir, files) do
Enum.reduce(files, {dir, []}, fn file, {path, contents} ->
{path, path |> Path.join(file) |> update_contents(file, contents)}
end)
end
defp list_contents(path), do: path |> File.ls!() |> ignore()
defp update_contents(path, file, contents) do
cond do
File.regular?(path) -> [file | contents]
File.dir?(path) -> [find(path) | contents]
end
end
defp ignore(filenames) do
Enum.reject(filenames, &(String.first(&1) in @starts_with))
end
end
```
### Output
The data structure holding the results of the file search.
```
iex(1)> directory_tree = Files.find("hello")
{"hello",
[
"README.md",
{"hello/test",
[
{"hello/test/support", ["conn_case.ex"]},
"test_helper.exs",
{"hello/test/hello_web",
[{"hello/test/hello_web/controllers", ["error_json_test.exs"]}]}
]},
{"hello/lib",
[
"hello.ex",
{"hello/lib/hello", ["application.ex"]},
"hello_web.ex",
{"hello/lib/hello_web",
[
{"hello/lib/hello_web/controllers", ["error_json.ex"]},
"telemetry.ex",
"router.ex",
"endpoint.ex"
]}
]},
{"hello/priv", [{"hello/priv/static", ["robots.txt", "favicon.ico"]}]},
{"hello/config",
["config.exs", "dev.exs", "test.exs", "prod.exs", "runtime.exs"]},
"mix.exs"
]}
```
## Sorting and printing
### Code
Sort alphabetically, with directories last. Downcase before comparing strings.
```
defmodule Paths do
def puts(dir_tree), do: dir_tree |> print() |> Enum.join("\n") |> IO.puts()
defp print({path, contents}), do: path |> list(contents) |> List.flatten()
defp list(path, contents) do
contents |> Enum.sort(&alpha_asc_dir_last/2) |> Enum.map(&make_path(&1, path))
end
defp alpha_asc_dir_last({a, _}, {b, _}), do: fmt(a) < fmt(b)
defp alpha_asc_dir_last({_, _}, _), do: false
defp alpha_asc_dir_last(_, {_, _}), do: true
defp alpha_asc_dir_last(a, b), do: fmt(a) < fmt(b)
defp make_path(filename, path) when is_binary(filename), do: Path.join(path, filename)
defp make_path({path, contents}, _), do: list(path, contents)
defp fmt(f), do: String.downcase(f)
end
```
### Output
Print all the files sorted.
```
iex(2)> Paths.puts(directory_tree)
hello/mix.exs
hello/README.md
hello/config/config.exs
hello/config/dev.exs
hello/config/prod.exs
hello/config/runtime.exs
hello/config/test.exs
hello/lib/hello.ex
hello/lib/hello_web.ex
hello/lib/hello/application.ex
hello/lib/hello_web/endpoint.ex
hello/lib/hello_web/router.ex
hello/lib/hello_web/telemetry.ex
hello/lib/hello_web/controllers/error_json.ex
hello/priv/static/favicon.ico
hello/priv/static/robots.txt
hello/test/test_helper.exs
hello/test/hello_web/controllers/error_json_test.exs
hello/test/support/conn_case.ex
:ok
```
## Conclusion
Trying to do this without recursion made it difficult to sort directories first.

View File

@ -1 +0,0 @@
# post 1

View File

@ -1 +0,0 @@
# post 2

1
html Symbolic link
View File

@ -0,0 +1 @@
www

View File

@ -1,7 +1,7 @@
{
date: "Thu May 08 2025 19:41:30.000000000"
title: "index"
}
Hello world!
## Posts
- [recursive]($root/posts/2023-08-03-recursively-list-all-files-in-a-directory-with-elixir.html)

View File

@ -18,5 +18,10 @@ posts = (path=".") ->
sitegen.create =>
@title = "Hello World"
@app_name = "stasis"
@version = "0.2.12"
add "index.md"
add path, target: target for path, target in pairs posts "docs"
css = require("sitegen.tools").system_command "cat < %s > %s", "css"
build css, "index.css"

10
templates/app.html.heex Normal file
View File

@ -0,0 +1,10 @@
<section>
<h2>Hi, I'm Catalin Mititiuc</h2>
<p>
Experienced in full-stack web development with Elixir and JavaScript.
</p>
<p>Hiring? Reach me by <a href="mailto:webdevcat@proton.me">email</a>.</p>
</section>
<%= @inner_content %>

9
templates/blog.html.heex Normal file
View File

@ -0,0 +1,9 @@
<section>
<h2><a href="/posts/">Web Log</a></h2>
<p>Elixir, JavaScript, SVG, Containers, Git, Linux</p>
<p>
Questions, comments, feedback? <a href="mailto:webdevcat@proton.me">Contact the author</a>.
</p>
</section>
<%= @inner_content %>

40
templates/home.html.heex Normal file
View File

@ -0,0 +1,40 @@
<h2>Wares</h2>
<section>
<h3>
<a href="/apps/btroops/">BTroops</a>
<a class="app-source-code-link" href="/git/btroops/">
View Source Code
</a>
</h3>
<p>A virtual implementation of FASA's 1989 wargame, Battletroops, for
the browser. Suitable for single-player solo play or two-player hotseat.</p>
<p>Runs entirely on the client after the initial download from the
server. Built with HTML5, SVG and JavaScript. Uses Node.js, Esbuild and
Docker for building and running dev/test servers.</p>
</section>
<section>
<h3>
<a href="https://hex.pm/packages/pandoc">Pandoc</a>
<a class="app-source-code-link" href="/git/pandoc/">
View Source Code
</a>
</h3>
<p>
A Hex package for installing and invoking <a href="https://pandoc.org/">Pandoc</a>
("a universal document converter"), fashioned after Phoenix's
<a href="https://hex.pm/packages/esbuild">Esbuild</a>
and <a href="https://hex.pm/packages/tailwind">Tailwind</a>
packages. Also included is a file system watcher, so that converted
documents are updated as soon as content changes are saved.
</p>
</section>
<%= StasisWeb.PostHTML.index(%{posts: @posts}) %>
<h4 style="text-align: center;">
<.link href={~p"/posts"} method="get">View more posts</.link>
</h4>

33
templates/index.html Normal file
View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en" style="scrollbar-gutter:stable;">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>$title</title>
<link
rel="stylesheet"
id="font-bitter-css"
href="//fonts.googleapis.com/css?family=Bitter:400,700"
type="text/css"
media="screen"
/>
<link rel="stylesheet" href="$root/app.css" />
</head>
<body class="bg-white">
<header>
<div style="display: inline-block;">
<h1><a href="/">Web Dev Solutions</a></h1>
<h3 style="text-align: left">Catalin Mititiuc</h3>
</div>
</header>
<main>
$body
</main>
<footer>
<p>100% Human Made, No AI Used</p>
<p>$app_name $version</p>
</footer>
</body>
</html>

76
templates/root.html.heex Normal file
View File

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en" style="scrollbar-gutter:stable;">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title suffix=" · Catalin Mititiuc">
<%= assigns[:page_title] || "WebDevCat.me" %>
</.live_title>
<link
rel="stylesheet"
id="font-bitter-css"
href="//fonts.googleapis.com/css?family=Bitter:400,700"
type="text/css"
media="screen"
/>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<%= if assigns[:cgit] do %>
<link phx-track-static rel="stylesheet" href={~p"/assets/cgit.css"} />
<style>
article > * { max-width: unset; }
div#cgit table.list {
table-layout: auto;
width: 100%;
display: table;
}
div#cgit div.content {
overflow: scroll;
}
div#cgit table.tabs {
table-layout: auto;
width: 100%;
display: table;
}
div#cgit table.blob {
table-layout: auto;
width: 100%;
display: table;
}
div#cgit table.tabs {
table-layout: auto;
width: 100%;
display: table;
}
td.linenumbers { width: 1px; }
td.lines { max-width: 1px; overflow: hidden; }
td.linenumbers pre, td.lines pre {
line-height: 1.25em;
}
pre { overflow-x: scroll; overflow-y: hidden; }
code { font-size: unset; }
</style>
<% end %>
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
</head>
<body class="bg-white">
<header>
<div style="display: inline-block;">
<h1><a href="/">Web Dev Solutions</a></h1>
<h3 style="text-align: left">Catalin Mititiuc</h3>
</div>
</header>
<main>
<%= @inner_content %>
</main>
<footer>
<p>100% Human Made, No AI Used</p>
<p><%= app_name() %> <%= version() %></p>
</footer>
</body>
</html>