Commit a50bdfe775
Verified · cmc
Layout: unified · split
README.md +4 −1
| @@ -48,7 +48,9 @@ Open <http://127.0.0.1:3000>. Edit `my-site/index.org`, save, and the page reloa | ||
| 48 | 48 | own — that is the loop you will spend your time in. |
| 49 | 49 | |
| 50 | 50 | `init` writes a starter post, a page layout you can edit, and a config file with every |
| 51 | setting explained in comments. It never overwrites a file you already have. | |
| 51 | setting explained in comments. It never overwrites a file you already have. It also picks | |
| 52 | one of the four built-in themes — `plain`, `blog`, `wiki`, `docs` — so the site is styled | |
| 53 | from the first build; change `theme` in `orgo.toml`, or empty it and write your own CSS. | |
| 52 | 54 | |
| 53 | 55 | ## Org-Mode, Anywhere |
| 54 | 56 | |
| @@ -78,6 +80,7 @@ Each of these is a few lines of config, and each has a page in the guide: | ||
| 78 | 80 | | Tag pages, and an index of tags | [Collections](https://krazywarez.github.io/orgo/guide/03-collections.html) | |
| 79 | 81 | | An RSS feed | [Collections](https://krazywarez.github.io/orgo/guide/03-collections.html) | |
| 80 | 82 | | Numbered pages when a list gets long | [Collections](https://krazywarez.github.io/orgo/guide/03-collections.html) | |
| 83 | | A built-in theme for a blog, a wiki or a doc site | [Configuration](https://krazywarez.github.io/orgo/guide/02-configuration.html) | | |
| 81 | 84 | | Your own design, in ordinary HTML templates | [Templates](https://krazywarez.github.io/orgo/guide/04-templates.html) | |
| 82 | 85 | | Drafts that stay unpublished until you say so | [Authoring](https://krazywarez.github.io/orgo/guide/06-authoring.html) | |
| 83 | 86 | | A table of contents on long posts | [Authoring](https://krazywarez.github.io/orgo/guide/06-authoring.html) | |
docs/guide/02-configuration.org +50
| @@ -18,6 +18,7 @@ title = "orgo site" | ||
| 18 | 18 | base_url = "" |
| 19 | 19 | description = "" |
| 20 | 20 | language = "en" |
| 21 | theme = "" | |
| 21 | 22 | |
| 22 | 23 | [nav] |
| 23 | 24 | mode = "top-level" |
| @@ -52,6 +53,55 @@ Plus any number of =[[collections]]= blocks, documented in [[file:03-collections | ||
| 52 | 53 | | =base_url= | ="" | Absolute origin, *no trailing slash*. | |
| 53 | 54 | | =description= | ="" | Available as ={{ site.description }}=. | |
| 54 | 55 | | =language= | ="en"= | Goes in =<html lang>= in the built-in layout. | |
| 56 | | =theme= | ="" | A built-in stylesheet, written to the output as =theme.css=. | | |
| 57 | ||
| 58 | ** theme | |
| 59 | ||
| 60 | Four themes are compiled into the binary. Name one and each build writes it to the | |
| 61 | output root as =theme.css=, which the built-in layout and the templates =orgo init= | |
| 62 | writes both link. | |
| 63 | ||
| 64 | | Theme | Shape | For | | |
| 65 | |-------+-------+-----| | |
| 66 | | ="plain"= | Narrow, system fonts, hairline rules. | Readable defaults to build your own CSS on. | | |
| 67 | | ="blog"= | Serif prose, a centred masthead, styled post lists. | Dated writing. | | |
| 68 | | ="wiki"= | Wide and dense, contents in the margin, TODO states as badges. | Notes, a reference site. | | |
| 69 | | ="docs"= | Narrow, a contents card, quote blocks as notes, =#+LEDE:=. | A guide read in order. | | |
| 70 | ||
| 71 | All four follow =prefers-color-scheme=, so a site gets a dark mode without a toggle, a | |
| 72 | setting or a line of JavaScript — and all four reflow from a 320px phone up, with tables | |
| 73 | and code blocks scrolling inside their own box rather than widening the page. | |
| 74 | ||
| 75 | The default is empty: no stylesheet is written and no page links one, so the output is | |
| 76 | unstyled HTML. That is deliberate — a site that already ships CSS of its own should not | |
| 77 | find a second stylesheet competing with it, and upgrading orgo should never restyle a | |
| 78 | site. An unknown name is an error listing the four. | |
| 79 | ||
| 80 | A theme styles the markup orgo already emits — headings, tags, TODO keywords, checkbox | |
| 81 | lists, footnotes, tables — plus the chrome the built-in layout puts around it. There is | |
| 82 | no theme-specific HTML, so switching or removing one touches no template. | |
| 83 | ||
| 84 | Every colour is a custom property on =:root=, named =--orgo-*=. To adjust rather than | |
| 85 | replace a theme, ship a stylesheet of your own as an asset, link it after =theme.css=, | |
| 86 | and redefine the handful you care about: | |
| 87 | ||
| 88 | #+BEGIN_SRC css | |
| 89 | :root { | |
| 90 | --orgo-accent: #7a1fa2; | |
| 91 | --orgo-measure: 46rem; | |
| 92 | } | |
| 93 | #+END_SRC | |
| 94 | ||
| 95 | Code /blocks/ are the one part a theme cannot make follow =prefers-color-scheme=: | |
| 96 | =syntax.css= is generated from a single syntect theme, so a theme's dark mode keeps its | |
| 97 | block surface light to stay readable against the default =InspiredGitHub=. Pair a dark | |
| 98 | =highlight.theme= with overrides of =--orgo-code-bg=, =--orgo-code-fg= and | |
| 99 | =--orgo-code-rule=. Those three colour blocks only — inline =~code~= follows the page's | |
| 100 | own scheme, so it stays legible whichever highlight theme you use. The documentation site | |
| 101 | does exactly this; its =style.css= is those three lines and nothing else. | |
| 102 | ||
| 103 | When you outgrow a theme, drop =theme= from the config and write =templates/base.html= | |
| 104 | against your own CSS. Nothing else changes. | |
| 55 | 105 | |
| 56 | 106 | ** base_url |
| 57 | 107 | |
docs/guide/04-templates.org +14
| @@ -150,6 +150,20 @@ depth: | ||
| 150 | 150 | URL of the generated =syntax.css=, relative to this page. Link it or code blocks are |
| 151 | 151 | unstyled. |
| 152 | 152 | |
| 153 | ** theme | |
| 154 | ||
| 155 | URL of =theme.css=, relative to this page — the [[file:02-configuration.org][built-in theme]] named by | |
| 156 | =site.theme=. Empty when there is none, which is the default, so guard it and link it | |
| 157 | *before* =stylesheet= or the theme's code colours would override the highlighter's: | |
| 158 | ||
| 159 | #+BEGIN_SRC html | |
| 160 | {% if theme %}<link rel="stylesheet" href="{{ theme }}">{% endif %} | |
| 161 | {% if stylesheet %}<link rel="stylesheet" href="{{ stylesheet }}">{% endif %} | |
| 162 | #+END_SRC | |
| 163 | ||
| 164 | A layout that ignores it is a layout with its own CSS, which is the point at which you | |
| 165 | have outgrown the setting. | |
| 166 | ||
| 153 | 167 | ** pages, group, groups, paginator |
| 154 | 168 | |
| 155 | 169 | Present on generated pages; see [[file:03-collections.org][Collections]]. =pages= is also |
docs/guide/10-deploying.org +2 −2
| @@ -14,8 +14,8 @@ Two differences from the build you run while writing: | ||
| 14 | 14 | bad build fails rather than shipping. |
| 15 | 15 | - No =--drafts=, so pages marked =#+DRAFT:= stay out. |
| 16 | 16 | |
| 17 | Everything in =_site= is the site: HTML, the generated =syntax.css=, and every asset | |
| 18 | copied from the source. There is no runtime, no server requirement and no build step | |
| 17 | Everything in =_site= is the site: HTML, the generated =syntax.css=, =theme.css= if the | |
| 18 | config names a [[file:02-configuration.org][theme]], and every asset copied from the source. There is no runtime, no server requirement and no build step | |
| 19 | 19 | downstream. |
| 20 | 20 | |
| 21 | 21 | * Set base_url for production |
docs/guide/11-versioning.org +8 −1
| @@ -12,7 +12,7 @@ Changing any of this incompatibly requires a major version. | ||
| 12 | 12 | | Stable | What that covers | |
| 13 | 13 | |--------+------------------| |
| 14 | 14 | | =orgo.toml= keys | Their names, types and meaning. | |
| 15 | | Template context | =page=, =site=, =nav=, =root=, =pages=, =group=, =groups=, =paginator=, =stylesheet=, and the =absolute=, =rfc822= and =truncate= filters. | | |
| 15 | | Template context | =page=, =site=, =nav=, =root=, =pages=, =group=, =groups=, =paginator=, =stylesheet=, =theme=, and the =absolute=, =rfc822= and =truncate= filters. | | |
| 16 | 16 | | The CLI | Command names, flags and exit codes. | |
| 17 | 17 | | URLs | How a source path becomes an output path, =#+SLUG:= included. | |
| 18 | 18 | |
| @@ -42,6 +42,13 @@ The class names the documentation names are the ones to write CSS against: | ||
| 42 | 42 | =post-list=, =post-list-item=, =figure-number=, =table-number=, =section-number-N=, |
| 43 | 43 | =footnote-ref=, =verbatim=, and the =on=/=off=/=trans= classes on checkbox items. |
| 44 | 44 | |
| 45 | ** What a built-in theme looks like | |
| 46 | ||
| 47 | The names — =plain=, =blog=, =wiki=, =docs= — and the fact that the chosen one is written | |
| 48 | to =theme.css= are stable. Its CSS is not: a theme is a starting point that improves | |
| 49 | between releases, and a site that cannot afford that should copy the stylesheet it likes | |
| 50 | into its own assets and stop naming a theme. | |
| 51 | ||
| 45 | 52 | ** The Rust API |
| 46 | 53 | |
| 47 | 54 | The crate is on crates.io so the binary can be installed with =cargo install=. The library |
docs/orgo.toml +5
| @@ -10,6 +10,9 @@ language = "en" | ||
| 10 | 10 | # Left empty so the docs build with relative URLs and open from the filesystem. Set it to |
| 11 | 11 | # your real origin to enable canonical links and feeds. |
| 12 | 12 | base_url = "" |
| 13 | # The built-in theme written for exactly this: a guide read in order, with a contents | |
| 14 | # block that matters. The site's own CSS is six lines of overrides on top of it. | |
| 15 | theme = "docs" | |
| 13 | 16 | |
| 14 | 17 | [nav] |
| 15 | 18 | # Explicit, because the header already links home: listing index.org here as well would |
| @@ -25,6 +28,8 @@ dir = "templates" | ||
| 25 | 28 | # A dark theme, with code blocks styled dark in both colour schemes. syntax.css is |
| 26 | 29 | # generated from a single syntect theme and cannot respond to prefers-color-scheme, so |
| 27 | 30 | # the page CSS matches the theme rather than leaving code unreadable in one of them. |
| 31 | # That match is style.css, which overrides the site theme's --orgo-code-* properties and | |
| 32 | # nothing else — the documented way to adjust a built-in theme instead of replacing it. | |
| 28 | 33 | theme = "base16-ocean.dark" |
| 29 | 34 | |
| 30 | 35 | [html] |
docs/quickstart.org +1 −1
| @@ -19,7 +19,7 @@ already has content is safe and additive. | ||
| 19 | 19 | |
| 20 | 20 | #+BEGIN_EXAMPLE |
| 21 | 21 | my-site/ |
| 22 | orgo.toml every setting, at its default, commented | |
| 22 | orgo.toml every setting, commented — all at their defaults but `theme` | |
| 23 | 23 | index.org the home page |
| 24 | 24 | blog/first-post.org a post, to show the collection working |
| 25 | 25 | templates/ |
docs/style.css +11 −125
| @@ -1,130 +1,16 @@ | ||
| 1 | /* Documentation site styling. | |
| 1 | /* Documentation site styling: the `docs` built-in theme, adjusted rather than replaced. | |
| 2 | 2 | * |
| 3 | 3 | * A plain asset, copied through the build untouched — which is also how any other CSS, |
| 4 | * image or font in a source directory reaches the output. */ | |
| 4 | * image or font in a source directory reaches the output. It is linked after theme.css, | |
| 5 | * which is what lets these definitions win. | |
| 6 | * | |
| 7 | * Only the code surface is changed. syntax.css is generated from one syntect theme and | |
| 8 | * cannot follow prefers-color-scheme, so a site pairing a dark `highlight.theme` with a | |
| 9 | * theme has to say what colour code sits on — here, dark in both schemes, matching | |
| 10 | * base16-ocean.dark. Everything else is the theme's. */ | |
| 5 | 11 | |
| 6 | 12 | :root { |
| 7 | --ink: #1c1f24; | |
| 8 | --muted: #5b6472; | |
| 9 | --rule: #dfe3e8; | |
| 10 | --accent: #0b5fa5; | |
| 11 | --surface: #f6f8fa; | |
| 12 | --measure: 42rem; | |
| 13 | } | |
| 14 | ||
| 15 | @media (prefers-color-scheme: dark) { | |
| 16 | :root { | |
| 17 | --ink: #dee3ea; | |
| 18 | --muted: #9aa4b2; | |
| 19 | --rule: #2b3138; | |
| 20 | --accent: #79b8ff; | |
| 21 | --surface: #171a1f; | |
| 22 | } | |
| 23 | body { background: #0f1216; } | |
| 24 | } | |
| 25 | ||
| 26 | * { box-sizing: border-box; } | |
| 27 | ||
| 28 | body { | |
| 29 | margin: 0; | |
| 30 | color: var(--ink); | |
| 31 | font: 16px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| 32 | } | |
| 33 | ||
| 34 | header.site { | |
| 35 | border-bottom: 1px solid var(--rule); | |
| 36 | padding: 1rem 1.5rem; | |
| 37 | display: flex; | |
| 38 | flex-wrap: wrap; | |
| 39 | gap: 1rem 1.5rem; | |
| 40 | align-items: baseline; | |
| 13 | --orgo-code-bg: #2b303b; | |
| 14 | --orgo-code-fg: #c0c5ce; | |
| 15 | --orgo-code-rule: #1f232b; | |
| 41 | 16 | } |
| 42 | ||
| 43 | header.site .site-title { | |
| 44 | font-weight: 700; | |
| 45 | font-size: 1.05rem; | |
| 46 | color: var(--ink); | |
| 47 | text-decoration: none; | |
| 48 | } | |
| 49 | ||
| 50 | header.site nav { display: flex; gap: 1.25rem; flex-wrap: wrap; } | |
| 51 | header.site nav a { color: var(--muted); text-decoration: none; } | |
| 52 | header.site nav a:hover { color: var(--accent); } | |
| 53 | ||
| 54 | main { | |
| 55 | max-width: var(--measure); | |
| 56 | margin: 0 auto; | |
| 57 | padding: 2.5rem 1.5rem 5rem; | |
| 58 | } | |
| 59 | ||
| 60 | h1 { font-size: 2rem; line-height: 1.2; margin: 0 0 .5rem; letter-spacing: -0.02em; } | |
| 61 | h2 { font-size: 1.35rem; margin: 2.5rem 0 .75rem; letter-spacing: -0.01em; } | |
| 62 | h3 { font-size: 1.1rem; margin: 2rem 0 .5rem; } | |
| 63 | ||
| 64 | p.page-date, p.lede { color: var(--muted); } | |
| 65 | p.lede { font-size: 1.1rem; margin-top: 0; } | |
| 66 | ||
| 67 | a { color: var(--accent); } | |
| 68 | ||
| 69 | code { | |
| 70 | font: 0.875em/1.5 ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; | |
| 71 | background: var(--surface); | |
| 72 | padding: .1em .35em; | |
| 73 | border-radius: 3px; | |
| 74 | } | |
| 75 | ||
| 76 | /* Code blocks are dark in both colour schemes, matching the syntect theme named in | |
| 77 | orgo.toml. One generated stylesheet cannot follow prefers-color-scheme, so the page | |
| 78 | commits to the theme's palette instead of leaving highlighted code unreadable in one | |
| 79 | of the two. */ | |
| 80 | pre { | |
| 81 | background: #2b303b; | |
| 82 | color: #c0c5ce; | |
| 83 | border: 1px solid #1f232b; | |
| 84 | border-radius: 6px; | |
| 85 | padding: .9rem 1rem; | |
| 86 | overflow-x: auto; | |
| 87 | } | |
| 88 | ||
| 89 | pre code { background: none; padding: 0; } | |
| 90 | ||
| 91 | blockquote { | |
| 92 | margin: 1.5rem 0; | |
| 93 | padding: .25rem 0 .25rem 1rem; | |
| 94 | border-left: 3px solid var(--rule); | |
| 95 | color: var(--muted); | |
| 96 | } | |
| 97 | ||
| 98 | table { border-collapse: collapse; width: 100%; margin: 1.25rem 0; display: block; overflow-x: auto; } | |
| 99 | th, td { text-align: left; padding: .5rem .75rem; border-bottom: 1px solid var(--rule); } | |
| 100 | th { font-size: .85rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); } | |
| 101 | ||
| 102 | hr { border: 0; border-top: 1px solid var(--rule); margin: 2.5rem 0; } | |
| 103 | ||
| 104 | /* Table of contents, emitted from page.toc */ | |
| 105 | nav.toc { | |
| 106 | background: var(--surface); | |
| 107 | border: 1px solid var(--rule); | |
| 108 | border-radius: 6px; | |
| 109 | padding: .75rem 1.25rem 1rem; | |
| 110 | margin: 1.5rem 0 2.5rem; | |
| 111 | } | |
| 112 | nav.toc h2 { font-size: .8rem; text-transform: uppercase; letter-spacing: .06em; margin: .25rem 0 .5rem; color: var(--muted); } | |
| 113 | nav.toc ul { margin: 0; padding-left: 1.1rem; } | |
| 114 | nav.toc li { margin: .15rem 0; } | |
| 115 | ||
| 116 | ul.post-list { list-style: none; padding: 0; } | |
| 117 | ul.post-list > li { padding: 1rem 0; border-bottom: 1px solid var(--rule); } | |
| 118 | ul.post-list a { font-weight: 600; font-size: 1.05rem; } | |
| 119 | p.excerpt { margin: .35rem 0 .2rem; color: var(--muted); } | |
| 120 | span.reading-time { font-size: .85rem; color: var(--muted); } | |
| 121 | ||
| 122 | footer.site { | |
| 123 | border-top: 1px solid var(--rule); | |
| 124 | padding: 1.5rem; | |
| 125 | color: var(--muted); | |
| 126 | font-size: .9rem; | |
| 127 | text-align: center; | |
| 128 | } | |
| 129 | ||
| 130 | .tag { font-size: .75rem; background: var(--surface); border: 1px solid var(--rule); border-radius: 999px; padding: .1em .6em; color: var(--muted); } | |
docs/templates/base.html +4 −1
| @@ -8,10 +8,13 @@ | ||
| 8 | 8 | <link rel="canonical" href="{{ page.url | absolute }}"> |
| 9 | 9 | {%- endif %} |
| 10 | 10 | <meta name="description" content="{{ page.excerpt | truncate(150) }}"> |
| 11 | <link rel="stylesheet" href="{{ root }}style.css"> | |
| 11 | {%- if theme %} | |
| 12 | <link rel="stylesheet" href="{{ theme }}"> | |
| 13 | {%- endif %} | |
| 12 | 14 | {%- if stylesheet %} |
| 13 | 15 | <link rel="stylesheet" href="{{ stylesheet }}"> |
| 14 | 16 | {%- endif %} |
| 17 | <link rel="stylesheet" href="{{ root }}style.css"> | |
| 15 | 18 | </head> |
| 16 | 19 | <body> |
| 17 | 20 | <header class="site"> |
src/config.rs +29 −3
| @@ -331,6 +331,14 @@ pub struct Site { | ||
| 331 | 331 | pub description: String, |
| 332 | 332 | /// `<html lang="…">` in the default layout. |
| 333 | 333 | pub language: String, |
| 334 | /// A built-in theme name — see [`crate::theme::THEMES`] — written to the output root | |
| 335 | /// as `theme.css` and linked by the built-in layout and the starter templates. | |
| 336 | /// | |
| 337 | /// Empty by default, which emits no stylesheet and leaves the HTML unstyled. A theme | |
| 338 | /// is a convenience for a site that has not grown its own CSS yet, and defaulting | |
| 339 | /// one on would restyle every existing site on upgrade and fight the stylesheets | |
| 340 | /// people already ship as assets. | |
| 341 | pub theme: String, | |
| 334 | 342 | } |
| 335 | 343 | |
| 336 | 344 | impl Default for Site { |
| @@ -340,6 +348,7 @@ impl Default for Site { | ||
| 340 | 348 | base_url: String::new(), |
| 341 | 349 | description: String::new(), |
| 342 | 350 | language: "en".to_string(), |
| 351 | theme: String::new(), | |
| 343 | 352 | } |
| 344 | 353 | } |
| 345 | 354 | } |
| @@ -539,6 +548,14 @@ impl Config { | ||
| 539 | 548 | ); |
| 540 | 549 | } |
| 541 | 550 | } |
| 551 | if !self.site.theme.is_empty() && crate::theme::theme_css(&self.site.theme).is_none() { | |
| 552 | anyhow::bail!( | |
| 553 | "unknown site.theme {:?}. Available: {} — or leave it empty for no \ | |
| 554 | stylesheet", | |
| 555 | self.site.theme, | |
| 556 | crate::theme::available_themes().join(", ") | |
| 557 | ); | |
| 558 | } | |
| 542 | 559 | if !self.site.base_url.is_empty() && self.site.base_url.ends_with('/') { |
| 543 | 560 | anyhow::bail!( |
| 544 | 561 | "site.base_url must not end with a slash (got {:?}) — URLs are joined \ |
| @@ -551,9 +568,12 @@ impl Config { | ||
| 551 | 568 | } |
| 552 | 569 | |
| 553 | 570 | /// The starter config written by `orgo init`, and the documentation of record for |
| 554 | /// what is configurable. Every value shown is the default, so deleting any line is safe. | |
| 555 | pub const STARTER_CONFIG: &str = r#"# orgo configuration. Every setting here is optional and shown at its default, | |
| 556 | # so you can delete any line you do not need — or the whole file. | |
| 571 | /// what is configurable. Every value shown is the default — except `site.theme`, which | |
| 572 | /// picks a stylesheet so a new site looks like something on its first build — so | |
| 573 | /// deleting any line is safe. | |
| 574 | pub const STARTER_CONFIG: &str = r#"# orgo configuration. Every setting here is optional and shown at its default — apart | |
| 575 | # from `theme`, noted below — so you can delete any line you do not need, or the whole | |
| 576 | # file. | |
| 557 | 577 | |
| 558 | 578 | [site] |
| 559 | 579 | title = "orgo site" |
| @@ -562,6 +582,12 @@ title = "orgo site" | ||
| 562 | 582 | base_url = "" |
| 563 | 583 | description = "" |
| 564 | 584 | language = "en" |
| 585 | # A built-in stylesheet, written to the output as theme.css: "plain" (readable defaults | |
| 586 | # to build your own CSS on), "blog" (serif prose, masthead, styled post lists), "wiki" | |
| 587 | # (wide and dense, contents in the margin, TODO states shown) or "docs" (a guide read in | |
| 588 | # order). The one line here that is not a default: the default is "", which emits no | |
| 589 | # stylesheet at all. Your own base.html can ignore theme.css and link whatever it likes. | |
| 590 | theme = "blog" | |
| 565 | 591 | |
| 566 | 592 | [nav] |
| 567 | 593 | # Which pages appear in the shared navigation: |
src/lib.rs +1
| @@ -20,5 +20,6 @@ pub mod resolve; | ||
| 20 | 20 | pub mod serve; |
| 21 | 21 | pub mod site; |
| 22 | 22 | pub mod template; |
| 23 | pub mod theme; | |
| 23 | 24 | pub mod util; |
| 24 | 25 | pub mod watch; |
src/main.rs +13 −1
| @@ -10,7 +10,7 @@ use orgo::parser::parse; | ||
| 10 | 10 | use orgo::config::{self, Config}; |
| 11 | 11 | use orgo::render::{self, render, Html, SyntectHighlighter}; |
| 12 | 12 | use orgo::resolve::ResolvedDoc; |
| 13 | use orgo::site::{build_site, BuildOptions, SYNTAX_STYLESHEET}; | |
| 13 | use orgo::site::{build_site, BuildOptions, SYNTAX_STYLESHEET, THEME_STYLESHEET}; | |
| 14 | 14 | use orgo::template::{PageContext, RenderContext, SiteContext, Templater}; |
| 15 | 15 | |
| 16 | 16 | #[derive(Parser)] |
| @@ -325,6 +325,13 @@ fn build_file(input: &Utf8Path, output: &Utf8Path) -> Result<()> { | ||
| 325 | 325 | }; |
| 326 | 326 | let mut ctx = RenderContext::new(&site, &page_ctx, &[], SYNTAX_STYLESHEET, ""); |
| 327 | 327 | ctx.body = &fragment; |
| 328 | // The page and its stylesheets are written side by side here, so the link is a bare | |
| 329 | // filename rather than a path back to a site root that does not exist. | |
| 330 | ctx.theme = if config.site.theme.is_empty() { | |
| 331 | "" | |
| 332 | } else { | |
| 333 | THEME_STYLESHEET | |
| 334 | }; | |
| 328 | 335 | // `#+TEMPLATE:` and `[[pages]]` apply here too, so `build one.org` and a whole-site |
| 329 | 336 | // build put the same page through the same layout. |
| 330 | 337 | let name = config::page_template( |
| @@ -339,5 +346,10 @@ fn build_file(input: &Utf8Path, output: &Utf8Path) -> Result<()> { | ||
| 339 | 346 | |
| 340 | 347 | let css = output.with_file_name(SYNTAX_STYLESHEET); |
| 341 | 348 | fs::write(&css, css_text).with_context(|| format!("writing stylesheet {css}"))?; |
| 349 | ||
| 350 | if let Some(theme_css) = orgo::theme::theme_css(&config.site.theme) { | |
| 351 | let path = output.with_file_name(THEME_STYLESHEET); | |
| 352 | fs::write(&path, theme_css).with_context(|| format!("writing stylesheet {path}"))?; | |
| 353 | } | |
| 342 | 354 | Ok(()) |
| 343 | 355 | } |
src/site.rs +26
| @@ -32,6 +32,7 @@ use crate::template::{ | ||
| 32 | 32 | GroupContext, NavItem, PageContext, Paginator, PaginatorPage, RenderContext, SiteContext, |
| 33 | 33 | Templater, |
| 34 | 34 | }; |
| 35 | use crate::theme; | |
| 35 | 36 | use crate::util::{ |
| 36 | 37 | document_text, first_paragraph, is_draft, iso_date, iso_time, option_enabled, |
| 37 | 38 | output_path, output_url, |
| @@ -882,8 +883,10 @@ fn render_page( | ||
| 882 | 883 | // Relative to the *output* path, since `#+SLUG:` can move a page between depths. |
| 883 | 884 | let root = relative_root(&p.output); |
| 884 | 885 | let stylesheet = format!("{root}{SYNTAX_STYLESHEET}"); |
| 886 | let theme = theme_href(config, &root); | |
| 885 | 887 | let mut ctx = RenderContext::new(site, &p.context, &p.nav, &stylesheet, &root); |
| 886 | 888 | ctx.body = &fragment; |
| 889 | ctx.theme = &theme; | |
| 887 | 890 | ctx.pages = pages; |
| 888 | 891 | templater |
| 889 | 892 | .render(&p.template, &ctx) |
| @@ -893,6 +896,20 @@ fn render_page( | ||
| 893 | 896 | /// Site-root-relative name of the generated syntax stylesheet. Every page links to it. |
| 894 | 897 | pub const SYNTAX_STYLESHEET: &str = "syntax.css"; |
| 895 | 898 | |
| 899 | /// Site-root-relative name of the built-in theme's stylesheet, written only when | |
| 900 | /// `site.theme` names one. | |
| 901 | pub const THEME_STYLESHEET: &str = "theme.css"; | |
| 902 | ||
| 903 | /// Where a page at `root` finds `theme.css` — empty when the site has no theme, which | |
| 904 | /// is how a template knows to link nothing at all. | |
| 905 | fn theme_href(config: &Config, root: &str) -> String { | |
| 906 | if config.site.theme.is_empty() { | |
| 907 | String::new() | |
| 908 | } else { | |
| 909 | format!("{root}{THEME_STYLESHEET}") | |
| 910 | } | |
| 911 | } | |
| 912 | ||
| 896 | 913 | /// Site-root-relative name of the generated sitemap. |
| 897 | 914 | pub const SITEMAP: &str = "sitemap.xml"; |
| 898 | 915 | |
| @@ -1132,7 +1149,9 @@ pub fn build_site(src: &Utf8Path, out: &Utf8Path, opts: &BuildOptions) -> Result | ||
| 1132 | 1149 | let with_content = listing |
| 1133 | 1150 | .include_content |
| 1134 | 1151 | .then(|| entries_with_content(&listing.entries, &preps, &highlighter, &cfg)); |
| 1152 | let theme = theme_href(&cfg, &root); | |
| 1135 | 1153 | let mut ctx = RenderContext::new(&site, &page_ctx, &nav, &stylesheet, &root); |
| 1154 | ctx.theme = &theme; | |
| 1136 | 1155 | ctx.pages = Some(with_content.as_deref().unwrap_or(&listing.entries)); |
| 1137 | 1156 | ctx.group = listing.group.as_ref(); |
| 1138 | 1157 | ctx.groups = &listing.groups; |
| @@ -1167,6 +1186,13 @@ pub fn build_site(src: &Utf8Path, out: &Utf8Path, opts: &BuildOptions) -> Result | ||
| 1167 | 1186 | fs::write(out.join(SYNTAX_STYLESHEET), &syntax_css) |
| 1168 | 1187 | .with_context(|| format!("writing {SYNTAX_STYLESHEET} under {out}"))?; |
| 1169 | 1188 | |
| 1189 | // The built-in theme, on the same terms: compiled in, written every build, and named | |
| 1190 | // in the config hash so switching themes re-renders the pages that link it. | |
| 1191 | if let Some(css) = theme::theme_css(&cfg.site.theme) { | |
| 1192 | fs::write(out.join(THEME_STYLESHEET), css) | |
| 1193 | .with_context(|| format!("writing {THEME_STYLESHEET} under {out}"))?; | |
| 1194 | } | |
| 1195 | ||
| 1170 | 1196 | // A sitemap covers every page the build emits, authored and generated alike, so it is |
| 1171 | 1197 | // written here rather than declared as a collection: a collection lists the pages it |
| 1172 | 1198 | // was pointed at, and this one has to know about all of them including itself. |
src/template.rs +16
| @@ -90,6 +90,9 @@ const BASE_TEMPLATE: &str = r##"<!DOCTYPE html> | ||
| 90 | 90 | {%- if page.description %} |
| 91 | 91 | <meta name="description" content="{{ page.description }}"> |
| 92 | 92 | {%- endif %} |
| 93 | {%- if theme %} | |
| 94 | <link rel="stylesheet" href="{{ theme }}"> | |
| 95 | {%- endif %} | |
| 93 | 96 | {%- if stylesheet %} |
| 94 | 97 | <link rel="stylesheet" href="{{ stylesheet }}"> |
| 95 | 98 | {%- endif %} |
| @@ -267,6 +270,7 @@ impl Templater { | ||
| 267 | 270 | body => ctx.body, |
| 268 | 271 | nav => ctx.nav, |
| 269 | 272 | stylesheet => ctx.stylesheet, |
| 273 | theme => ctx.theme, | |
| 270 | 274 | root => ctx.root, |
| 271 | 275 | pages => ctx.pages, |
| 272 | 276 | group => ctx.group, |
| @@ -338,6 +342,9 @@ pub struct RenderContext<'a> { | ||
| 338 | 342 | pub nav: &'a [NavItem], |
| 339 | 343 | /// URL of the syntax stylesheet, relative to this page. |
| 340 | 344 | pub stylesheet: &'a str, |
| 345 | /// Path to the built-in theme's `theme.css`, relative to this page — empty when | |
| 346 | /// `site.theme` names no theme, which is the default. | |
| 347 | pub theme: &'a str, | |
| 341 | 348 | /// `../`-prefix back to the site root from this page. |
| 342 | 349 | pub root: &'a str, |
| 343 | 350 | /// The pages this listing shows, or every page when `expose_page_list` is on. |
| @@ -366,6 +373,9 @@ impl<'a> RenderContext<'a> { | ||
| 366 | 373 | body: "", |
| 367 | 374 | nav, |
| 368 | 375 | stylesheet, |
| 376 | // Assigned after construction, like `body`: most callers have no theme, and | |
| 377 | // an empty one is exactly "link no theme stylesheet". | |
| 378 | theme: "", | |
| 369 | 379 | root, |
| 370 | 380 | pages: None, |
| 371 | 381 | group: None, |
| @@ -383,6 +393,9 @@ pub const STARTER_TAGS_TEMPLATE: &str = r#"<!DOCTYPE html> | ||
| 383 | 393 | <meta charset="utf-8"> |
| 384 | 394 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 385 | 395 | <title>{{ page.title }} · {{ site.title }}</title> |
| 396 | {%- if theme %} | |
| 397 | <link rel="stylesheet" href="{{ theme }}"> | |
| 398 | {%- endif %} | |
| 386 | 399 | {%- if stylesheet %} |
| 387 | 400 | <link rel="stylesheet" href="{{ stylesheet }}"> |
| 388 | 401 | {%- endif %} |
| @@ -418,6 +431,9 @@ pub const STARTER_LIST_TEMPLATE: &str = r#"<!DOCTYPE html> | ||
| 418 | 431 | <meta charset="utf-8"> |
| 419 | 432 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 420 | 433 | <title>{{ page.title }} · {{ site.title }}</title> |
| 434 | {%- if theme %} | |
| 435 | <link rel="stylesheet" href="{{ theme }}"> | |
| 436 | {%- endif %} | |
| 421 | 437 | {%- if stylesheet %} |
| 422 | 438 | <link rel="stylesheet" href="{{ stylesheet }}"> |
| 423 | 439 | {%- endif %} |
src/theme.rs added +43
| @@ -0,0 +1,43 @@ | ||
| 1 | //! Built-in site themes: whole stylesheets compiled into the binary. | |
| 2 | //! | |
| 3 | //! A theme is one CSS file and nothing else. It styles the markup the RENDER stage | |
| 4 | //! emits — org's headings, tags, TODO keywords, checkbox lists, footnotes — plus the | |
| 5 | //! chrome the built-in layout and the starter templates put around it. There is no | |
| 6 | //! theme-specific HTML, so a theme can be switched, or removed, without touching a | |
| 7 | //! template. | |
| 8 | //! | |
| 9 | //! Compiled in for the same reason the syntax definitions are: `cargo install orgo` | |
| 10 | //! gives you one binary, and a site that needs a stylesheet fetched from somewhere else | |
| 11 | //! before it looks like anything is not that. The chosen theme is written to the output | |
| 12 | //! root as `theme.css` on every build, the way [`crate::render::syntax_css`] writes | |
| 13 | //! `syntax.css`. | |
| 14 | //! | |
| 15 | //! Nothing here is a wrapper you have to work through: `site.theme` empty emits no | |
| 16 | //! stylesheet at all, and a `base.html` of your own can ignore `theme.css` and link | |
| 17 | //! whatever it likes. | |
| 18 | ||
| 19 | /// Every built-in theme, as `(name, stylesheet)`, in the order they are offered. | |
| 20 | /// | |
| 21 | /// - `plain` — readable defaults with no design opinion, to build your own CSS on. | |
| 22 | /// - `blog` — dated writing: serif prose, a masthead, styled listing pages. | |
| 23 | /// - `wiki` — a dense reference site: wide, sidebar contents, tables and TODO states. | |
| 24 | /// - `docs` — a guide read in order: prominent contents, code-forward, `#+LEDE:`. | |
| 25 | pub const THEMES: &[(&str, &str)] = &[ | |
| 26 | ("plain", include_str!("../themes/plain.css")), | |
| 27 | ("blog", include_str!("../themes/blog.css")), | |
| 28 | ("wiki", include_str!("../themes/wiki.css")), | |
| 29 | ("docs", include_str!("../themes/docs.css")), | |
| 30 | ]; | |
| 31 | ||
| 32 | /// The stylesheet for a built-in theme, or `None` if no theme goes by that name. | |
| 33 | pub fn theme_css(name: &str) -> Option<&'static str> { | |
| 34 | THEMES | |
| 35 | .iter() | |
| 36 | .find(|(theme, _)| *theme == name) | |
| 37 | .map(|(_, css)| *css) | |
| 38 | } | |
| 39 | ||
| 40 | /// Every theme name [`theme_css`] accepts, for error messages and documentation. | |
| 41 | pub fn available_themes() -> Vec<&'static str> { | |
| 42 | THEMES.iter().map(|(name, _)| *name).collect() | |
| 43 | } | |
tests/config.rs +110
| @@ -2499,3 +2499,113 @@ fn well_known_is_published_but_other_dot_entries_are_not() { | ||
| 2499 | 2499 | assert!(!out.join(".git/config").exists(), ".git stays out"); |
| 2500 | 2500 | assert!(!out.join(".env").exists(), ".env stays out"); |
| 2501 | 2501 | } |
| 2502 | ||
| 2503 | // --------------------------------------------------------------------------- | |
| 2504 | // Built-in themes | |
| 2505 | // --------------------------------------------------------------------------- | |
| 2506 | ||
| 2507 | /// A theme is one compiled-in stylesheet: named in the config, written to the output | |
| 2508 | /// root, linked from every page at whatever depth that page sits. | |
| 2509 | #[test] | |
| 2510 | fn a_built_in_theme_is_written_once_and_linked_from_every_depth() { | |
| 2511 | let root = tmpdir("theme-site"); | |
| 2512 | let src = root.join("src"); | |
| 2513 | std::fs::create_dir_all(&src).unwrap(); | |
| 2514 | write_site(&src); | |
| 2515 | std::fs::write(src.join("orgo.toml"), "[site]\ntheme = \"wiki\"\n").unwrap(); | |
| 2516 | let out = root.join("out"); | |
| 2517 | build(&src, &out); | |
| 2518 | ||
| 2519 | let css = std::fs::read_to_string(out.join("theme.css")).expect("theme.css is written"); | |
| 2520 | assert_eq!( | |
| 2521 | css, | |
| 2522 | orgo::theme::theme_css("wiki").unwrap(), | |
| 2523 | "verbatim, not a rebuilt approximation of it" | |
| 2524 | ); | |
| 2525 | ||
| 2526 | assert!( | |
| 2527 | page(&out, "index.html").contains("<link rel=\"stylesheet\" href=\"theme.css\">"), | |
| 2528 | "a root page links it directly" | |
| 2529 | ); | |
| 2530 | assert!( | |
| 2531 | page(&out, "blog/post.html").contains("<link rel=\"stylesheet\" href=\"../theme.css\">"), | |
| 2532 | "a nested page reaches back up to it" | |
| 2533 | ); | |
| 2534 | } | |
| 2535 | ||
| 2536 | /// The theme has to come before `syntax.css`, or a theme's `pre code` colour would | |
| 2537 | /// override the highlighter's and code blocks would render in one flat colour. | |
| 2538 | #[test] | |
| 2539 | fn the_theme_is_linked_ahead_of_the_syntax_stylesheet() { | |
| 2540 | let root = tmpdir("theme-order"); | |
| 2541 | let src = root.join("src"); | |
| 2542 | std::fs::create_dir_all(&src).unwrap(); | |
| 2543 | write_site(&src); | |
| 2544 | std::fs::write(src.join("orgo.toml"), "[site]\ntheme = \"docs\"\n").unwrap(); | |
| 2545 | let out = root.join("out"); | |
| 2546 | build(&src, &out); | |
| 2547 | ||
| 2548 | let home = page(&out, "index.html"); | |
| 2549 | let theme = home.find("theme.css").expect("theme link"); | |
| 2550 | let syntax = home.find("syntax.css").expect("syntax link"); | |
| 2551 | assert!(theme < syntax, "theme first, highlighting on top of it: {home}"); | |
| 2552 | } | |
| 2553 | ||
| 2554 | /// No theme is the default. An existing site upgrading must not find itself restyled, | |
| 2555 | /// and a site with a stylesheet of its own must not have a second one competing with it. | |
| 2556 | #[test] | |
| 2557 | fn no_theme_is_the_default_and_emits_no_stylesheet() { | |
| 2558 | let root = tmpdir("theme-none"); | |
| 2559 | let src = root.join("src"); | |
| 2560 | std::fs::create_dir_all(&src).unwrap(); | |
| 2561 | write_site(&src); | |
| 2562 | let out = root.join("out"); | |
| 2563 | build(&src, &out); | |
| 2564 | ||
| 2565 | assert!(!out.join("theme.css").exists(), "nothing to write"); | |
| 2566 | assert!( | |
| 2567 | !page(&out, "index.html").contains("theme.css"), | |
| 2568 | "and nothing to link" | |
| 2569 | ); | |
| 2570 | } | |
| 2571 | ||
| 2572 | /// A misspelled theme name would otherwise emit an unstyled site with no complaint, | |
| 2573 | /// which looks exactly like the theme setting doing nothing. | |
| 2574 | #[test] | |
| 2575 | fn an_unknown_site_theme_is_rejected_with_the_available_ones() { | |
| 2576 | let root = tmpdir("theme-unknown"); | |
| 2577 | let src = root.join("src"); | |
| 2578 | std::fs::create_dir_all(&src).unwrap(); | |
| 2579 | write_site(&src); | |
| 2580 | std::fs::write(src.join("orgo.toml"), "[site]\ntheme = \"blogg\"\n").unwrap(); | |
| 2581 | ||
| 2582 | let err = build_site(&src, &root.join("out"), &BuildOptions::default()) | |
| 2583 | .expect_err("unknown theme must fail"); | |
| 2584 | let message = format!("{err:#}"); | |
| 2585 | assert!(message.contains("blogg"), "names the bad theme: {message}"); | |
| 2586 | for name in orgo::theme::available_themes() { | |
| 2587 | assert!(message.contains(name), "lists {name}: {message}"); | |
| 2588 | } | |
| 2589 | } | |
| 2590 | ||
| 2591 | /// Switching themes changes every page's `<head>`, so every page has to be re-rendered. | |
| 2592 | /// The theme name lives in the config hash, which is what makes that happen. | |
| 2593 | #[test] | |
| 2594 | fn switching_the_theme_re_renders_the_pages_that_link_it() { | |
| 2595 | let root = tmpdir("theme-switch"); | |
| 2596 | let src = root.join("src"); | |
| 2597 | std::fs::create_dir_all(&src).unwrap(); | |
| 2598 | write_site(&src); | |
| 2599 | std::fs::write(src.join("orgo.toml"), "[site]\ntheme = \"plain\"\n").unwrap(); | |
| 2600 | let out = root.join("out"); | |
| 2601 | build(&src, &out); | |
| 2602 | ||
| 2603 | std::fs::write(src.join("orgo.toml"), "[site]\ntheme = \"blog\"\n").unwrap(); | |
| 2604 | let report = build(&src, &out); | |
| 2605 | assert!(report.skipped.is_empty(), "no page may keep the old head"); | |
| 2606 | assert_eq!( | |
| 2607 | std::fs::read_to_string(out.join("theme.css")).unwrap(), | |
| 2608 | orgo::theme::theme_css("blog").unwrap(), | |
| 2609 | "and the stylesheet on disk is the new one" | |
| 2610 | ); | |
| 2611 | } | |
themes/blog.css added +298
| @@ -0,0 +1,298 @@ | ||
| 1 | /* orgo built-in theme: blog | |
| 2 | * | |
| 3 | * A personal site of dated writing. Serif prose at a narrow measure, a masthead rather | |
| 4 | * than a toolbar, and listing pages — the blog index, a tag page, a feed's HTML | |
| 5 | * counterpart — treated as first-class rather than as an afterthought. | |
| 6 | * | |
| 7 | * Every colour is a custom property on :root, so a stylesheet of your own loaded after | |
| 8 | * this one can retheme the site by redefining a handful of values. */ | |
| 9 | ||
| 10 | :root { | |
| 11 | --orgo-ink: #23201d; | |
| 12 | --orgo-muted: #6b645d; | |
| 13 | --orgo-rule: #e5e0d8; | |
| 14 | --orgo-accent: #9a3412; | |
| 15 | --orgo-surface: #faf7f2; | |
| 16 | --orgo-bg: #fdfcfa; | |
| 17 | --orgo-measure: 36rem; | |
| 18 | --orgo-font: Iowan Old Style, Charter, Palatino, Georgia, "Times New Roman", serif; | |
| 19 | --orgo-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| 20 | --orgo-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; | |
| 21 | /* These three colour code *blocks* only; inline code follows the page. A block keeps | |
| 22 | a light surface in both colour schemes because syntax.css is generated from one | |
| 23 | syntect theme (`highlight.theme`, InspiredGitHub by default) and cannot follow | |
| 24 | prefers-color-scheme. Pair a dark highlight theme with overrides of these three. */ | |
| 25 | --orgo-todo: #b02a37; | |
| 26 | --orgo-done: #2c7a4b; | |
| 27 | --orgo-code-bg: #f5f2ec; | |
| 28 | --orgo-code-fg: #323232; | |
| 29 | --orgo-code-rule: #e5e0d8; | |
| 30 | } | |
| 31 | ||
| 32 | @media (prefers-color-scheme: dark) { | |
| 33 | :root { | |
| 34 | --orgo-ink: #e6e1da; | |
| 35 | --orgo-muted: #a49c93; | |
| 36 | --orgo-rule: #322e2a; | |
| 37 | --orgo-accent: #f0a066; | |
| 38 | --orgo-surface: #1b1917; | |
| 39 | --orgo-todo: #f0909a; | |
| 40 | --orgo-done: #74c795; | |
| 41 | --orgo-bg: #141312; | |
| 42 | } | |
| 43 | } | |
| 44 | ||
| 45 | * { box-sizing: border-box; } | |
| 46 | ||
| 47 | body { | |
| 48 | margin: 0; | |
| 49 | background: var(--orgo-bg); | |
| 50 | color: var(--orgo-ink); | |
| 51 | font: 18px/1.7 var(--orgo-font); | |
| 52 | -webkit-font-smoothing: antialiased; | |
| 53 | /* A bare URL or a long identifier must wrap, not widen the page on a phone. */ | |
| 54 | overflow-wrap: break-word; | |
| 55 | } | |
| 56 | ||
| 57 | /* Masthead ----------------------------------------------------------------- */ | |
| 58 | ||
| 59 | body > header { | |
| 60 | max-width: var(--orgo-measure); | |
| 61 | margin: 0 auto; | |
| 62 | padding: 3rem 1.5rem 1.5rem; | |
| 63 | text-align: center; | |
| 64 | display: flex; | |
| 65 | flex-direction: column; | |
| 66 | align-items: center; | |
| 67 | gap: .75rem; | |
| 68 | } | |
| 69 | ||
| 70 | .site-title { | |
| 71 | font-size: 1.5rem; | |
| 72 | font-weight: 400; | |
| 73 | letter-spacing: .02em; | |
| 74 | color: var(--orgo-ink); | |
| 75 | text-decoration: none; | |
| 76 | } | |
| 77 | ||
| 78 | body > header nav { | |
| 79 | display: flex; | |
| 80 | flex-wrap: wrap; | |
| 81 | justify-content: center; | |
| 82 | gap: 1.5rem; | |
| 83 | font: .8rem/1 var(--orgo-ui); | |
| 84 | text-transform: uppercase; | |
| 85 | letter-spacing: .1em; | |
| 86 | } | |
| 87 | body > header nav a { color: var(--orgo-muted); text-decoration: none; } | |
| 88 | body > header nav a:hover { color: var(--orgo-accent); } | |
| 89 | ||
| 90 | /* Page body ---------------------------------------------------------------- */ | |
| 91 | ||
| 92 | main { | |
| 93 | max-width: var(--orgo-measure); | |
| 94 | margin: 0 auto; | |
| 95 | padding: 2rem 1.5rem 5rem; | |
| 96 | } | |
| 97 | ||
| 98 | h1 { | |
| 99 | font-size: 2.1rem; | |
| 100 | font-weight: 400; | |
| 101 | line-height: 1.25; | |
| 102 | letter-spacing: -0.01em; | |
| 103 | margin: 0 0 .5rem; | |
| 104 | } | |
| 105 | h2 { font-size: 1.4rem; font-weight: 600; margin: 2.5rem 0 .75rem; } | |
| 106 | h3 { font-size: 1.15rem; font-weight: 600; margin: 2rem 0 .5rem; } | |
| 107 | h4, h5, h6 { font-size: 1rem; font-weight: 600; margin: 1.5rem 0 .5rem; } | |
| 108 | ||
| 109 | a { color: var(--orgo-accent); text-underline-offset: .15em; } | |
| 110 | ||
| 111 | /* The byline the built-in layout prints under the title. */ | |
| 112 | p.page-date { | |
| 113 | font: .8rem/1 var(--orgo-ui); | |
| 114 | text-transform: uppercase; | |
| 115 | letter-spacing: .1em; | |
| 116 | color: var(--orgo-muted); | |
| 117 | margin: 0 0 2.5rem; | |
| 118 | } | |
| 119 | ||
| 120 | img, video { max-width: 100%; height: auto; } | |
| 121 | ||
| 122 | figure { margin: 2rem 0; } | |
| 123 | figcaption { | |
| 124 | font: .85rem/1.5 var(--orgo-ui); | |
| 125 | color: var(--orgo-muted); | |
| 126 | margin-top: .5rem; | |
| 127 | text-align: center; | |
| 128 | } | |
| 129 | .figure-number, .table-number { font-weight: 600; } | |
| 130 | ||
| 131 | blockquote { | |
| 132 | margin: 2rem 0; | |
| 133 | padding: 0 1.5rem; | |
| 134 | border-left: 2px solid var(--orgo-accent); | |
| 135 | font-style: italic; | |
| 136 | color: var(--orgo-muted); | |
| 137 | } | |
| 138 | ||
| 139 | hr { | |
| 140 | border: 0; | |
| 141 | margin: 3rem auto; | |
| 142 | width: 6rem; | |
| 143 | border-top: 1px solid var(--orgo-rule); | |
| 144 | } | |
| 145 | ||
| 146 | .center { text-align: center; } | |
| 147 | .verse { font-family: var(--orgo-mono); font-size: .95rem; white-space: pre-wrap; } | |
| 148 | ||
| 149 | dt { font-weight: 600; margin-top: .75rem; } | |
| 150 | dd { margin: 0 0 0 1.5rem; } | |
| 151 | ||
| 152 | /* Code --------------------------------------------------------------------- */ | |
| 153 | ||
| 154 | /* Inline code is prose furniture, so it follows the page rather than the code blocks. */ | |
| 155 | code { | |
| 156 | font: .85em/1.5 var(--orgo-mono); | |
| 157 | background: var(--orgo-surface); | |
| 158 | color: inherit; | |
| 159 | padding: .1em .35em; | |
| 160 | border-radius: 3px; | |
| 161 | } | |
| 162 | ||
| 163 | pre { | |
| 164 | background: var(--orgo-code-bg); | |
| 165 | color: var(--orgo-code-fg); | |
| 166 | border: 1px solid var(--orgo-code-rule); | |
| 167 | border-radius: 4px; | |
| 168 | padding: 1rem 1.1rem; | |
| 169 | font-size: .9rem; | |
| 170 | line-height: 1.55; | |
| 171 | overflow-x: auto; | |
| 172 | } | |
| 173 | ||
| 174 | pre code { background: none; color: inherit; padding: 0; } | |
| 175 | ||
| 176 | /* Tables ------------------------------------------------------------------- */ | |
| 177 | ||
| 178 | table { | |
| 179 | border-collapse: collapse; | |
| 180 | margin: 2rem 0; | |
| 181 | font: .95rem/1.5 var(--orgo-ui); | |
| 182 | display: block; | |
| 183 | overflow-x: auto; | |
| 184 | } | |
| 185 | caption { text-align: left; color: var(--orgo-muted); font-size: .9rem; padding-bottom: .5rem; } | |
| 186 | th, td { text-align: left; padding: .5rem .85rem; border-bottom: 1px solid var(--orgo-rule); } | |
| 187 | th { | |
| 188 | font-size: .75rem; | |
| 189 | text-transform: uppercase; | |
| 190 | letter-spacing: .06em; | |
| 191 | color: var(--orgo-muted); | |
| 192 | } | |
| 193 | ||
| 194 | /* Org-specific markup ------------------------------------------------------ */ | |
| 195 | ||
| 196 | .tag { | |
| 197 | font: .7rem/1.6 var(--orgo-ui); | |
| 198 | text-transform: uppercase; | |
| 199 | letter-spacing: .08em; | |
| 200 | color: var(--orgo-muted); | |
| 201 | background: var(--orgo-surface); | |
| 202 | border: 1px solid var(--orgo-rule); | |
| 203 | border-radius: 999px; | |
| 204 | padding: .1em .7em; | |
| 205 | vertical-align: middle; | |
| 206 | } | |
| 207 | ||
| 208 | .todo, .done { font: .7em/1 var(--orgo-mono); letter-spacing: .05em; } | |
| 209 | .todo { color: var(--orgo-todo); } | |
| 210 | .done { color: var(--orgo-done); } | |
| 211 | .priority { color: var(--orgo-muted); font-family: var(--orgo-mono); font-size: .8em; } | |
| 212 | ||
| 213 | time.timestamp { font: .9em/1 var(--orgo-ui); color: var(--orgo-muted); } | |
| 214 | ||
| 215 | li.on, li.trans { color: var(--orgo-muted); } | |
| 216 | li.on { text-decoration: line-through; } | |
| 217 | ||
| 218 | .footnotes { | |
| 219 | margin-top: 4rem; | |
| 220 | font-size: .9rem; | |
| 221 | color: var(--orgo-muted); | |
| 222 | } | |
| 223 | .footnote-ref a { text-decoration: none; } | |
| 224 | ||
| 225 | /* Table of contents, from page.toc ----------------------------------------- */ | |
| 226 | ||
| 227 | nav.toc { | |
| 228 | border-top: 1px solid var(--orgo-rule); | |
| 229 | border-bottom: 1px solid var(--orgo-rule); | |
| 230 | padding: 1rem 0; | |
| 231 | margin: 2rem 0 3rem; | |
| 232 | font: .95rem/1.6 var(--orgo-ui); | |
| 233 | } | |
| 234 | nav.toc h2 { | |
| 235 | font-size: .7rem; | |
| 236 | text-transform: uppercase; | |
| 237 | letter-spacing: .1em; | |
| 238 | color: var(--orgo-muted); | |
| 239 | margin: 0 0 .5rem; | |
| 240 | } | |
| 241 | nav.toc ul { margin: 0; padding-left: 1.1rem; } | |
| 242 | nav.toc li { margin: .15rem 0; } | |
| 243 | ||
| 244 | /* Listing pages ------------------------------------------------------------ */ | |
| 245 | ||
| 246 | ul.post-list { list-style: none; padding: 0; margin: 2rem 0 0; } | |
| 247 | ul.post-list > li { padding: 1.75rem 0; border-bottom: 1px solid var(--orgo-rule); } | |
| 248 | ul.post-list > li:first-child { padding-top: 0; } | |
| 249 | ul.post-list time { | |
| 250 | display: block; | |
| 251 | font: .75rem/1 var(--orgo-ui); | |
| 252 | text-transform: uppercase; | |
| 253 | letter-spacing: .1em; | |
| 254 | color: var(--orgo-muted); | |
| 255 | margin-bottom: .4rem; | |
| 256 | } | |
| 257 | ul.post-list a { | |
| 258 | font-size: 1.3rem; | |
| 259 | line-height: 1.3; | |
| 260 | color: var(--orgo-ink); | |
| 261 | text-decoration: none; | |
| 262 | } | |
| 263 | ul.post-list a:hover { color: var(--orgo-accent); } | |
| 264 | p.excerpt { margin: .5rem 0 .35rem; color: var(--orgo-muted); font-size: .95rem; } | |
| 265 | span.reading-time { font: .75rem/1 var(--orgo-ui); letter-spacing: .06em; color: var(--orgo-muted); } | |
| 266 | ||
| 267 | ul.tag-list { | |
| 268 | list-style: none; | |
| 269 | padding: 0; | |
| 270 | display: flex; | |
| 271 | flex-wrap: wrap; | |
| 272 | gap: .75rem 1.25rem; | |
| 273 | font: .95rem/1.6 var(--orgo-ui); | |
| 274 | } | |
| 275 | ||
| 276 | nav.pagination { | |
| 277 | display: flex; | |
| 278 | justify-content: space-between; | |
| 279 | align-items: baseline; | |
| 280 | gap: 1rem; | |
| 281 | margin-top: 3rem; | |
| 282 | font: .85rem/1 var(--orgo-ui); | |
| 283 | text-transform: uppercase; | |
| 284 | letter-spacing: .08em; | |
| 285 | color: var(--orgo-muted); | |
| 286 | } | |
| 287 | ||
| 288 | /* Footer ------------------------------------------------------------------- */ | |
| 289 | ||
| 290 | body > footer { | |
| 291 | max-width: var(--orgo-measure); | |
| 292 | margin: 0 auto; | |
| 293 | border-top: 1px solid var(--orgo-rule); | |
| 294 | padding: 1.5rem; | |
| 295 | font: .85rem/1.6 var(--orgo-ui); | |
| 296 | color: var(--orgo-muted); | |
| 297 | text-align: center; | |
| 298 | } | |
themes/docs.css added +247
| @@ -0,0 +1,247 @@ | ||
| 1 | /* orgo built-in theme: docs | |
| 2 | * | |
| 3 | * A documentation site: a guide read in order, with a contents block that matters, code | |
| 4 | * blocks that carry as much of the meaning as the prose, and a `#+LEDE:` line under the | |
| 5 | * title. Cooler and more technical than `blog`, narrower and more designed than `wiki`. | |
| 6 | * | |
| 7 | * Every colour is a custom property on :root, so a stylesheet of your own loaded after | |
| 8 | * this one can retheme the site by redefining a handful of values. */ | |
| 9 | ||
| 10 | :root { | |
| 11 | --orgo-ink: #1c1f24; | |
| 12 | --orgo-muted: #5b6472; | |
| 13 | --orgo-rule: #dfe3e8; | |
| 14 | --orgo-accent: #0b5fa5; | |
| 15 | --orgo-surface: #f6f8fa; | |
| 16 | --orgo-bg: #ffffff; | |
| 17 | --orgo-measure: 42rem; | |
| 18 | --orgo-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| 19 | --orgo-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; | |
| 20 | /* These three colour code *blocks* only; inline code follows the page. A block keeps | |
| 21 | a light surface in both colour schemes because syntax.css is generated from one | |
| 22 | syntect theme (`highlight.theme`, InspiredGitHub by default) and cannot follow | |
| 23 | prefers-color-scheme. Pair a dark highlight theme with overrides of these three. */ | |
| 24 | --orgo-todo: #b02a37; | |
| 25 | --orgo-done: #2c7a4b; | |
| 26 | --orgo-code-bg: #f6f8fa; | |
| 27 | --orgo-code-fg: #323232; | |
| 28 | --orgo-code-rule: #e3e7ec; | |
| 29 | } | |
| 30 | ||
| 31 | @media (prefers-color-scheme: dark) { | |
| 32 | :root { | |
| 33 | --orgo-ink: #dee3ea; | |
| 34 | --orgo-muted: #9aa4b2; | |
| 35 | --orgo-rule: #2b3138; | |
| 36 | --orgo-accent: #79b8ff; | |
| 37 | --orgo-surface: #171a1f; | |
| 38 | --orgo-todo: #f0909a; | |
| 39 | --orgo-done: #74c795; | |
| 40 | --orgo-bg: #0f1216; | |
| 41 | } | |
| 42 | } | |
| 43 | ||
| 44 | * { box-sizing: border-box; } | |
| 45 | ||
| 46 | body { | |
| 47 | margin: 0; | |
| 48 | background: var(--orgo-bg); | |
| 49 | color: var(--orgo-ink); | |
| 50 | font: 16px/1.65 var(--orgo-font); | |
| 51 | /* A bare URL or a long identifier must wrap, not widen the page on a phone. */ | |
| 52 | overflow-wrap: break-word; | |
| 53 | } | |
| 54 | ||
| 55 | /* Header ------------------------------------------------------------------- */ | |
| 56 | ||
| 57 | body > header { | |
| 58 | position: sticky; | |
| 59 | top: 0; | |
| 60 | z-index: 1; | |
| 61 | background: var(--orgo-bg); | |
| 62 | border-bottom: 1px solid var(--orgo-rule); | |
| 63 | padding: 1rem 1.5rem; | |
| 64 | display: flex; | |
| 65 | flex-wrap: wrap; | |
| 66 | gap: .5rem 1.5rem; | |
| 67 | align-items: baseline; | |
| 68 | } | |
| 69 | ||
| 70 | .site-title { | |
| 71 | font-weight: 700; | |
| 72 | font-size: 1.05rem; | |
| 73 | color: var(--orgo-ink); | |
| 74 | text-decoration: none; | |
| 75 | } | |
| 76 | ||
| 77 | body > header nav { display: flex; flex-wrap: wrap; gap: 1.25rem; } | |
| 78 | body > header nav a { | |
| 79 | color: var(--orgo-muted); | |
| 80 | text-decoration: none; | |
| 81 | padding-bottom: .15rem; | |
| 82 | border-bottom: 2px solid transparent; | |
| 83 | } | |
| 84 | body > header nav a:hover { color: var(--orgo-ink); border-bottom-color: var(--orgo-accent); } | |
| 85 | ||
| 86 | /* Page body ---------------------------------------------------------------- */ | |
| 87 | ||
| 88 | main { | |
| 89 | max-width: var(--orgo-measure); | |
| 90 | margin: 0 auto; | |
| 91 | padding: 2.5rem 1.5rem 5rem; | |
| 92 | } | |
| 93 | ||
| 94 | h1 { font-size: 2rem; line-height: 1.2; letter-spacing: -0.02em; margin: 0 0 .5rem; } | |
| 95 | h2 { font-size: 1.35rem; letter-spacing: -0.01em; margin: 2.75rem 0 .75rem; } | |
| 96 | h3 { font-size: 1.1rem; margin: 2rem 0 .5rem; } | |
| 97 | h4, h5, h6 { font-size: 1rem; margin: 1.5rem 0 .5rem; } | |
| 98 | ||
| 99 | [class^="section-number-"] { color: var(--orgo-muted); font-weight: 400; } | |
| 100 | ||
| 101 | a { color: var(--orgo-accent); } | |
| 102 | ||
| 103 | /* `#+LEDE:` reaches the layout as page.keywords.lede; page.date as the byline. */ | |
| 104 | p.lede { font-size: 1.1rem; color: var(--orgo-muted); margin-top: 0; } | |
| 105 | p.page-date { color: var(--orgo-muted); font-size: .9rem; } | |
| 106 | ||
| 107 | img, video { max-width: 100%; height: auto; } | |
| 108 | ||
| 109 | figure { margin: 1.75rem 0; } | |
| 110 | figcaption { color: var(--orgo-muted); font-size: .9rem; margin-top: .4rem; } | |
| 111 | .figure-number, .table-number { font-weight: 600; } | |
| 112 | ||
| 113 | /* A quote block reads as a note or a caution in a documentation site. */ | |
| 114 | blockquote { | |
| 115 | margin: 1.5rem 0; | |
| 116 | padding: .75rem 1rem; | |
| 117 | background: var(--orgo-surface); | |
| 118 | border-left: 3px solid var(--orgo-accent); | |
| 119 | border-radius: 0 4px 4px 0; | |
| 120 | color: var(--orgo-muted); | |
| 121 | } | |
| 122 | blockquote > :first-child { margin-top: 0; } | |
| 123 | blockquote > :last-child { margin-bottom: 0; } | |
| 124 | ||
| 125 | hr { border: 0; border-top: 1px solid var(--orgo-rule); margin: 2.5rem 0; } | |
| 126 | ||
| 127 | .center { text-align: center; } | |
| 128 | .verse { font-family: var(--orgo-mono); white-space: pre-wrap; } | |
| 129 | ||
| 130 | dt { font-weight: 600; margin-top: .75rem; font-family: var(--orgo-mono); font-size: .95rem; } | |
| 131 | dd { margin: 0 0 0 1.5rem; } | |
| 132 | ||
| 133 | /* Code --------------------------------------------------------------------- */ | |
| 134 | ||
| 135 | /* Inline code is prose furniture, so it follows the page rather than the code blocks. */ | |
| 136 | code { | |
| 137 | font: .875em/1.5 var(--orgo-mono); | |
| 138 | background: var(--orgo-surface); | |
| 139 | color: inherit; | |
| 140 | padding: .1em .35em; | |
| 141 | border-radius: 3px; | |
| 142 | } | |
| 143 | ||
| 144 | pre { | |
| 145 | background: var(--orgo-code-bg); | |
| 146 | color: var(--orgo-code-fg); | |
| 147 | border: 1px solid var(--orgo-code-rule); | |
| 148 | border-radius: 6px; | |
| 149 | padding: .9rem 1.1rem; | |
| 150 | font-size: .9rem; | |
| 151 | line-height: 1.55; | |
| 152 | overflow-x: auto; | |
| 153 | } | |
| 154 | ||
| 155 | pre code { background: none; color: inherit; padding: 0; } | |
| 156 | ||
| 157 | /* Tables ------------------------------------------------------------------- */ | |
| 158 | ||
| 159 | table { | |
| 160 | border-collapse: collapse; | |
| 161 | width: 100%; | |
| 162 | margin: 1.5rem 0; | |
| 163 | display: block; | |
| 164 | overflow-x: auto; | |
| 165 | } | |
| 166 | caption { text-align: left; color: var(--orgo-muted); font-size: .9rem; padding-bottom: .4rem; } | |
| 167 | th, td { text-align: left; padding: .5rem .75rem; border-bottom: 1px solid var(--orgo-rule); } | |
| 168 | th { | |
| 169 | font-size: .8rem; | |
| 170 | text-transform: uppercase; | |
| 171 | letter-spacing: .05em; | |
| 172 | color: var(--orgo-muted); | |
| 173 | } | |
| 174 | ||
| 175 | /* Org-specific markup ------------------------------------------------------ */ | |
| 176 | ||
| 177 | .tag { | |
| 178 | font: .72rem/1.6 var(--orgo-mono); | |
| 179 | color: var(--orgo-muted); | |
| 180 | background: var(--orgo-surface); | |
| 181 | border: 1px solid var(--orgo-rule); | |
| 182 | border-radius: 999px; | |
| 183 | padding: .05em .6em; | |
| 184 | vertical-align: middle; | |
| 185 | } | |
| 186 | ||
| 187 | .todo, .done { font: .72rem/1.6 var(--orgo-mono); letter-spacing: .04em; } | |
| 188 | .todo { color: var(--orgo-todo); } | |
| 189 | .done { color: var(--orgo-done); } | |
| 190 | .priority { color: var(--orgo-muted); font-family: var(--orgo-mono); font-size: .8em; } | |
| 191 | ||
| 192 | time.timestamp { color: var(--orgo-muted); font-family: var(--orgo-mono); font-size: .9em; } | |
| 193 | ||
| 194 | li.on, li.trans { color: var(--orgo-muted); } | |
| 195 | li.on { text-decoration: line-through; } | |
| 196 | ||
| 197 | .footnotes { margin-top: 3rem; font-size: .9rem; color: var(--orgo-muted); } | |
| 198 | .footnote-ref a { text-decoration: none; } | |
| 199 | ||
| 200 | /* Table of contents: a card, because in a guide it is navigation ----------- */ | |
| 201 | ||
| 202 | nav.toc { | |
| 203 | background: var(--orgo-surface); | |
| 204 | border: 1px solid var(--orgo-rule); | |
| 205 | border-radius: 6px; | |
| 206 | padding: .75rem 1.25rem 1rem; | |
| 207 | margin: 1.75rem 0 2.5rem; | |
| 208 | } | |
| 209 | nav.toc h2 { | |
| 210 | font-size: .8rem; | |
| 211 | text-transform: uppercase; | |
| 212 | letter-spacing: .06em; | |
| 213 | color: var(--orgo-muted); | |
| 214 | margin: .25rem 0 .5rem; | |
| 215 | } | |
| 216 | nav.toc ul { margin: 0; padding-left: 1.1rem; } | |
| 217 | nav.toc li { margin: .15rem 0; } | |
| 218 | ||
| 219 | /* Listing pages: a guide's contents page, a tag index ---------------------- */ | |
| 220 | ||
| 221 | ul.post-list { list-style: none; padding: 0; } | |
| 222 | ul.post-list > li { padding: 1rem 0; border-bottom: 1px solid var(--orgo-rule); } | |
| 223 | ul.post-list a { font-weight: 600; font-size: 1.05rem; } | |
| 224 | ul.post-list time { color: var(--orgo-muted); font-size: .9rem; } | |
| 225 | p.excerpt { margin: .35rem 0 .2rem; color: var(--orgo-muted); } | |
| 226 | span.reading-time { font-size: .85rem; color: var(--orgo-muted); } | |
| 227 | ||
| 228 | ul.tag-list { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: .6rem 1.25rem; } | |
| 229 | ||
| 230 | nav.pagination { | |
| 231 | display: flex; | |
| 232 | gap: 1rem; | |
| 233 | align-items: baseline; | |
| 234 | margin-top: 2.5rem; | |
| 235 | color: var(--orgo-muted); | |
| 236 | font-size: .9rem; | |
| 237 | } | |
| 238 | ||
| 239 | /* Footer ------------------------------------------------------------------- */ | |
| 240 | ||
| 241 | body > footer { | |
| 242 | border-top: 1px solid var(--orgo-rule); | |
| 243 | padding: 1.5rem; | |
| 244 | color: var(--orgo-muted); | |
| 245 | font-size: .9rem; | |
| 246 | text-align: center; | |
| 247 | } | |
themes/plain.css added +205
| @@ -0,0 +1,205 @@ | ||
| 1 | /* orgo built-in theme: plain | |
| 2 | * | |
| 3 | * Readable defaults and nothing else. System fonts, one measure, hairline rules — the | |
| 4 | * styling a page needs to be read comfortably, with no design opinion for a site to | |
| 5 | * grow out of. Start here when you intend to write your own CSS eventually. | |
| 6 | * | |
| 7 | * Every colour is a custom property on :root, so a stylesheet of your own loaded after | |
| 8 | * this one can retheme the site by redefining a handful of values. */ | |
| 9 | ||
| 10 | :root { | |
| 11 | --orgo-ink: #1c1f24; | |
| 12 | --orgo-muted: #5b6472; | |
| 13 | --orgo-rule: #dfe3e8; | |
| 14 | --orgo-accent: #0b5fa5; | |
| 15 | --orgo-surface: #f6f8fa; | |
| 16 | --orgo-bg: #ffffff; | |
| 17 | --orgo-measure: 40rem; | |
| 18 | --orgo-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| 19 | --orgo-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; | |
| 20 | /* These three colour code *blocks* only; inline code follows the page. A block keeps | |
| 21 | a light surface in both colour schemes because syntax.css is generated from one | |
| 22 | syntect theme (`highlight.theme`, InspiredGitHub by default) and cannot follow | |
| 23 | prefers-color-scheme. Pair a dark highlight theme with overrides of these three. */ | |
| 24 | --orgo-todo: #b02a37; | |
| 25 | --orgo-done: #2c7a4b; | |
| 26 | --orgo-code-bg: #f6f8fa; | |
| 27 | --orgo-code-fg: #323232; | |
| 28 | --orgo-code-rule: #e3e7ec; | |
| 29 | } | |
| 30 | ||
| 31 | @media (prefers-color-scheme: dark) { | |
| 32 | :root { | |
| 33 | --orgo-ink: #dee3ea; | |
| 34 | --orgo-muted: #9aa4b2; | |
| 35 | --orgo-rule: #2b3138; | |
| 36 | --orgo-accent: #79b8ff; | |
| 37 | --orgo-surface: #171a1f; | |
| 38 | --orgo-todo: #f0909a; | |
| 39 | --orgo-done: #74c795; | |
| 40 | --orgo-bg: #0f1216; | |
| 41 | } | |
| 42 | } | |
| 43 | ||
| 44 | * { box-sizing: border-box; } | |
| 45 | ||
| 46 | body { | |
| 47 | margin: 0; | |
| 48 | background: var(--orgo-bg); | |
| 49 | color: var(--orgo-ink); | |
| 50 | font: 16px/1.65 var(--orgo-font); | |
| 51 | /* A bare URL or a long identifier must wrap, not widen the page on a phone. */ | |
| 52 | overflow-wrap: break-word; | |
| 53 | } | |
| 54 | ||
| 55 | /* Header and navigation ---------------------------------------------------- */ | |
| 56 | ||
| 57 | body > header { | |
| 58 | border-bottom: 1px solid var(--orgo-rule); | |
| 59 | padding: 1rem 1.5rem; | |
| 60 | display: flex; | |
| 61 | flex-wrap: wrap; | |
| 62 | gap: .5rem 1.5rem; | |
| 63 | align-items: baseline; | |
| 64 | } | |
| 65 | ||
| 66 | .site-title { font-weight: 700; color: var(--orgo-ink); text-decoration: none; } | |
| 67 | ||
| 68 | body > header nav { display: flex; flex-wrap: wrap; gap: 1.25rem; } | |
| 69 | body > header nav a { color: var(--orgo-muted); text-decoration: none; } | |
| 70 | body > header nav a:hover { color: var(--orgo-accent); text-decoration: underline; } | |
| 71 | ||
| 72 | /* Page body ---------------------------------------------------------------- */ | |
| 73 | ||
| 74 | main { | |
| 75 | max-width: var(--orgo-measure); | |
| 76 | margin: 0 auto; | |
| 77 | padding: 2.5rem 1.5rem 4rem; | |
| 78 | } | |
| 79 | ||
| 80 | h1 { font-size: 1.9rem; line-height: 1.2; margin: 0 0 1rem; } | |
| 81 | h2 { font-size: 1.35rem; margin: 2.25rem 0 .75rem; } | |
| 82 | h3 { font-size: 1.1rem; margin: 1.75rem 0 .5rem; } | |
| 83 | h4, h5, h6 { font-size: 1rem; margin: 1.5rem 0 .5rem; } | |
| 84 | ||
| 85 | a { color: var(--orgo-accent); } | |
| 86 | ||
| 87 | p.page-date { color: var(--orgo-muted); margin-top: -.5rem; } | |
| 88 | ||
| 89 | img, video { max-width: 100%; height: auto; } | |
| 90 | ||
| 91 | figure { margin: 1.5rem 0; } | |
| 92 | figcaption { color: var(--orgo-muted); font-size: .9rem; margin-top: .4rem; } | |
| 93 | .figure-number, .table-number { font-weight: 600; } | |
| 94 | ||
| 95 | blockquote { | |
| 96 | margin: 1.5rem 0; | |
| 97 | padding-left: 1rem; | |
| 98 | border-left: 3px solid var(--orgo-rule); | |
| 99 | color: var(--orgo-muted); | |
| 100 | } | |
| 101 | ||
| 102 | hr { border: 0; border-top: 1px solid var(--orgo-rule); margin: 2rem 0; } | |
| 103 | ||
| 104 | .center { text-align: center; } | |
| 105 | .verse { font-family: var(--orgo-mono); white-space: pre-wrap; } | |
| 106 | ||
| 107 | dt { font-weight: 600; margin-top: .75rem; } | |
| 108 | dd { margin: 0 0 0 1.5rem; } | |
| 109 | ||
| 110 | /* Code --------------------------------------------------------------------- */ | |
| 111 | ||
| 112 | /* Inline code is prose furniture, so it follows the page rather than the code blocks. */ | |
| 113 | code { | |
| 114 | font: .875em/1.5 var(--orgo-mono); | |
| 115 | background: var(--orgo-surface); | |
| 116 | color: inherit; | |
| 117 | padding: .1em .35em; | |
| 118 | border-radius: 3px; | |
| 119 | } | |
| 120 | ||
| 121 | pre { | |
| 122 | background: var(--orgo-code-bg); | |
| 123 | color: var(--orgo-code-fg); | |
| 124 | border: 1px solid var(--orgo-code-rule); | |
| 125 | border-radius: 4px; | |
| 126 | padding: .9rem 1rem; | |
| 127 | overflow-x: auto; | |
| 128 | } | |
| 129 | ||
| 130 | pre code { background: none; color: inherit; padding: 0; } | |
| 131 | ||
| 132 | /* Tables ------------------------------------------------------------------- */ | |
| 133 | ||
| 134 | table { border-collapse: collapse; margin: 1.25rem 0; display: block; overflow-x: auto; } | |
| 135 | caption { text-align: left; color: var(--orgo-muted); font-size: .9rem; padding-bottom: .4rem; } | |
| 136 | th, td { text-align: left; padding: .45rem .75rem; border-bottom: 1px solid var(--orgo-rule); } | |
| 137 | th { font-weight: 600; } | |
| 138 | ||
| 139 | /* Org-specific markup ------------------------------------------------------ */ | |
| 140 | ||
| 141 | .tag { | |
| 142 | font-size: .75rem; | |
| 143 | font-family: var(--orgo-mono); | |
| 144 | color: var(--orgo-muted); | |
| 145 | border: 1px solid var(--orgo-rule); | |
| 146 | border-radius: 999px; | |
| 147 | padding: .05em .55em; | |
| 148 | vertical-align: middle; | |
| 149 | } | |
| 150 | ||
| 151 | .todo, .done { font-size: .75em; font-family: var(--orgo-mono); letter-spacing: .04em; } | |
| 152 | .todo { color: var(--orgo-todo); } | |
| 153 | .done { color: var(--orgo-done); } | |
| 154 | .priority { color: var(--orgo-muted); font-family: var(--orgo-mono); font-size: .8em; } | |
| 155 | ||
| 156 | time.timestamp { color: var(--orgo-muted); font-family: var(--orgo-mono); font-size: .9em; } | |
| 157 | ||
| 158 | li.on, li.trans { color: var(--orgo-muted); } | |
| 159 | li.on { text-decoration: line-through; } | |
| 160 | ||
| 161 | .footnotes { margin-top: 3rem; font-size: .9rem; color: var(--orgo-muted); } | |
| 162 | .footnote-ref a { text-decoration: none; } | |
| 163 | ||
| 164 | /* Table of contents, from page.toc ----------------------------------------- */ | |
| 165 | ||
| 166 | nav.toc { margin: 1.5rem 0 2.5rem; } | |
| 167 | nav.toc h2 { | |
| 168 | font-size: .8rem; | |
| 169 | text-transform: uppercase; | |
| 170 | letter-spacing: .06em; | |
| 171 | color: var(--orgo-muted); | |
| 172 | margin: 0 0 .35rem; | |
| 173 | } | |
| 174 | nav.toc ul { margin: 0; padding-left: 1.1rem; } | |
| 175 | nav.toc li { margin: .1rem 0; } | |
| 176 | ||
| 177 | /* Listing pages: collections, tag indexes, pagination ---------------------- */ | |
| 178 | ||
| 179 | ul.post-list { list-style: none; padding: 0; } | |
| 180 | ul.post-list > li { padding: .9rem 0; border-bottom: 1px solid var(--orgo-rule); } | |
| 181 | ul.post-list a { font-weight: 600; } | |
| 182 | ul.post-list time { color: var(--orgo-muted); font-size: .9rem; } | |
| 183 | p.excerpt { margin: .3rem 0 .2rem; color: var(--orgo-muted); } | |
| 184 | span.reading-time { font-size: .85rem; color: var(--orgo-muted); } | |
| 185 | ||
| 186 | ul.tag-list { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: .5rem 1rem; } | |
| 187 | ||
| 188 | nav.pagination { | |
| 189 | display: flex; | |
| 190 | gap: 1rem; | |
| 191 | align-items: baseline; | |
| 192 | margin-top: 2rem; | |
| 193 | color: var(--orgo-muted); | |
| 194 | font-size: .9rem; | |
| 195 | } | |
| 196 | ||
| 197 | /* Footer ------------------------------------------------------------------- */ | |
| 198 | ||
| 199 | body > footer { | |
| 200 | border-top: 1px solid var(--orgo-rule); | |
| 201 | padding: 1.5rem; | |
| 202 | color: var(--orgo-muted); | |
| 203 | font-size: .9rem; | |
| 204 | text-align: center; | |
| 205 | } | |
themes/wiki.css added +275
| @@ -0,0 +1,275 @@ | ||
| 1 | /* orgo built-in theme: wiki | |
| 2 | * | |
| 3 | * A dense reference site: notes, a personal knowledge base, a manual. Wide and | |
| 4 | * information-first rather than roomy — a table of contents parked in the margin on a | |
| 5 | * large screen, compact headings with visible structure, tables and task lists styled | |
| 6 | * because a wiki is mostly made of them, and org's TODO keywords shown as badges since | |
| 7 | * in a notes site they are content rather than decoration. | |
| 8 | * | |
| 9 | * Every colour is a custom property on :root, so a stylesheet of your own loaded after | |
| 10 | * this one can retheme the site by redefining a handful of values. */ | |
| 11 | ||
| 12 | :root { | |
| 13 | --orgo-ink: #1f2328; | |
| 14 | --orgo-muted: #656d76; | |
| 15 | --orgo-rule: #d8dee4; | |
| 16 | --orgo-accent: #0969da; | |
| 17 | --orgo-surface: #f6f8fa; | |
| 18 | --orgo-bg: #ffffff; | |
| 19 | --orgo-measure: 46rem; | |
| 20 | --orgo-wide: 74rem; | |
| 21 | --orgo-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| 22 | --orgo-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; | |
| 23 | /* These three colour code *blocks* only; inline code follows the page. A block keeps | |
| 24 | a light surface in both colour schemes because syntax.css is generated from one | |
| 25 | syntect theme (`highlight.theme`, InspiredGitHub by default) and cannot follow | |
| 26 | prefers-color-scheme. Pair a dark highlight theme with overrides of these three. */ | |
| 27 | --orgo-todo: #b02a37; | |
| 28 | --orgo-done: #2c7a4b; | |
| 29 | --orgo-code-bg: #f6f8fa; | |
| 30 | --orgo-code-fg: #323232; | |
| 31 | --orgo-code-rule: #d8dee4; | |
| 32 | } | |
| 33 | ||
| 34 | @media (prefers-color-scheme: dark) { | |
| 35 | :root { | |
| 36 | --orgo-ink: #e6edf3; | |
| 37 | --orgo-muted: #8d96a0; | |
| 38 | --orgo-rule: #30363d; | |
| 39 | --orgo-accent: #6cb6ff; | |
| 40 | --orgo-surface: #161b22; | |
| 41 | --orgo-bg: #0d1117; | |
| 42 | } | |
| 43 | } | |
| 44 | ||
| 45 | * { box-sizing: border-box; } | |
| 46 | ||
| 47 | body { | |
| 48 | margin: 0; | |
| 49 | background: var(--orgo-bg); | |
| 50 | color: var(--orgo-ink); | |
| 51 | font: 15px/1.6 var(--orgo-font); | |
| 52 | /* A bare URL or a long identifier must wrap, not widen the page on a phone. */ | |
| 53 | overflow-wrap: break-word; | |
| 54 | } | |
| 55 | ||
| 56 | /* Header ------------------------------------------------------------------- */ | |
| 57 | ||
| 58 | body > header { | |
| 59 | background: var(--orgo-surface); | |
| 60 | border-bottom: 1px solid var(--orgo-rule); | |
| 61 | padding: .7rem 1.5rem; | |
| 62 | display: flex; | |
| 63 | flex-wrap: wrap; | |
| 64 | gap: .5rem 1.5rem; | |
| 65 | align-items: baseline; | |
| 66 | } | |
| 67 | ||
| 68 | .site-title { | |
| 69 | font-weight: 700; | |
| 70 | font-size: 1rem; | |
| 71 | color: var(--orgo-ink); | |
| 72 | text-decoration: none; | |
| 73 | } | |
| 74 | ||
| 75 | body > header nav { display: flex; flex-wrap: wrap; gap: 1.1rem; font-size: .9rem; } | |
| 76 | body > header nav a { color: var(--orgo-muted); text-decoration: none; } | |
| 77 | body > header nav a:hover { color: var(--orgo-accent); text-decoration: underline; } | |
| 78 | ||
| 79 | /* Page body ---------------------------------------------------------------- */ | |
| 80 | ||
| 81 | main { | |
| 82 | max-width: var(--orgo-measure); | |
| 83 | margin: 0 auto; | |
| 84 | padding: 2rem 1.5rem 4rem; | |
| 85 | } | |
| 86 | ||
| 87 | h1 { | |
| 88 | font-size: 1.8rem; | |
| 89 | line-height: 1.25; | |
| 90 | margin: 0 0 1rem; | |
| 91 | padding-bottom: .4rem; | |
| 92 | border-bottom: 1px solid var(--orgo-rule); | |
| 93 | } | |
| 94 | h2 { | |
| 95 | font-size: 1.3rem; | |
| 96 | margin: 2rem 0 .6rem; | |
| 97 | padding-bottom: .25rem; | |
| 98 | border-bottom: 1px solid var(--orgo-rule); | |
| 99 | } | |
| 100 | h3 { font-size: 1.05rem; margin: 1.5rem 0 .4rem; } | |
| 101 | h4, h5, h6 { font-size: .95rem; margin: 1.25rem 0 .4rem; } | |
| 102 | ||
| 103 | /* Section numbers, when html.section_numbers is on. */ | |
| 104 | [class^="section-number-"] { color: var(--orgo-muted); font-weight: 400; } | |
| 105 | ||
| 106 | a { color: var(--orgo-accent); text-decoration: none; } | |
| 107 | a:hover { text-decoration: underline; } | |
| 108 | ||
| 109 | p.page-date { color: var(--orgo-muted); font-size: .9rem; margin-top: -.5rem; } | |
| 110 | ||
| 111 | p, ul, ol { margin: .6rem 0; } | |
| 112 | li { margin: .15rem 0; } | |
| 113 | ||
| 114 | img, video { max-width: 100%; height: auto; } | |
| 115 | ||
| 116 | figure { margin: 1.25rem 0; } | |
| 117 | figcaption { color: var(--orgo-muted); font-size: .85rem; margin-top: .35rem; } | |
| 118 | .figure-number, .table-number { font-weight: 600; } | |
| 119 | ||
| 120 | blockquote { | |
| 121 | margin: 1rem 0; | |
| 122 | padding: .1rem 0 .1rem 1rem; | |
| 123 | border-left: 3px solid var(--orgo-rule); | |
| 124 | color: var(--orgo-muted); | |
| 125 | } | |
| 126 | ||
| 127 | hr { border: 0; border-top: 1px solid var(--orgo-rule); margin: 1.75rem 0; } | |
| 128 | ||
| 129 | .center { text-align: center; } | |
| 130 | .verse { font-family: var(--orgo-mono); white-space: pre-wrap; } | |
| 131 | ||
| 132 | dt { font-weight: 600; margin-top: .5rem; } | |
| 133 | dd { margin: 0 0 0 1.5rem; color: var(--orgo-muted); } | |
| 134 | ||
| 135 | /* Code --------------------------------------------------------------------- */ | |
| 136 | ||
| 137 | /* Inline code is prose furniture, so it follows the page rather than the code blocks. */ | |
| 138 | code { | |
| 139 | font: .875em/1.5 var(--orgo-mono); | |
| 140 | background: var(--orgo-surface); | |
| 141 | color: inherit; | |
| 142 | border: 1px solid var(--orgo-rule); | |
| 143 | padding: .05em .3em; | |
| 144 | border-radius: 3px; | |
| 145 | } | |
| 146 | ||
| 147 | pre { | |
| 148 | background: var(--orgo-code-bg); | |
| 149 | color: var(--orgo-code-fg); | |
| 150 | border: 1px solid var(--orgo-code-rule); | |
| 151 | border-radius: 4px; | |
| 152 | padding: .8rem 1rem; | |
| 153 | font-size: .875rem; | |
| 154 | overflow-x: auto; | |
| 155 | } | |
| 156 | ||
| 157 | pre code { background: none; color: inherit; border: 0; padding: 0; } | |
| 158 | ||
| 159 | /* Tables — a wiki lives in them, so they get zebra rows and a sticky-looking head. */ | |
| 160 | ||
| 161 | table { | |
| 162 | border-collapse: collapse; | |
| 163 | margin: 1rem 0; | |
| 164 | font-size: .9rem; | |
| 165 | display: block; | |
| 166 | overflow-x: auto; | |
| 167 | } | |
| 168 | caption { text-align: left; color: var(--orgo-muted); font-size: .85rem; padding-bottom: .35rem; } | |
| 169 | th, td { text-align: left; padding: .35rem .7rem; border: 1px solid var(--orgo-rule); } | |
| 170 | th { background: var(--orgo-surface); font-weight: 600; } | |
| 171 | tbody tr:nth-child(even) td { background: var(--orgo-surface); } | |
| 172 | ||
| 173 | /* Org-specific markup ------------------------------------------------------ */ | |
| 174 | ||
| 175 | .tag { | |
| 176 | font: .7rem/1.6 var(--orgo-mono); | |
| 177 | color: var(--orgo-muted); | |
| 178 | background: var(--orgo-surface); | |
| 179 | border: 1px solid var(--orgo-rule); | |
| 180 | border-radius: 3px; | |
| 181 | padding: .05em .45em; | |
| 182 | vertical-align: middle; | |
| 183 | } | |
| 184 | ||
| 185 | /* TODO state is content in a notes site, not decoration. */ | |
| 186 | .todo, .done { | |
| 187 | font: .7rem/1.6 var(--orgo-mono); | |
| 188 | letter-spacing: .04em; | |
| 189 | border-radius: 3px; | |
| 190 | padding: .05em .45em; | |
| 191 | vertical-align: middle; | |
| 192 | color: #ffffff; | |
| 193 | } | |
| 194 | .todo { background: var(--orgo-todo); } | |
| 195 | .done { background: var(--orgo-done); } | |
| 196 | .priority { color: var(--orgo-todo); font-family: var(--orgo-mono); font-size: .8em; } | |
| 197 | ||
| 198 | time.timestamp { color: var(--orgo-muted); font-family: var(--orgo-mono); font-size: .9em; } | |
| 199 | time.inactive { opacity: .75; } | |
| 200 | ||
| 201 | /* Checkbox lists: the three org states, including `[-]`, which HTML has no input for. | |
| 202 | The `[X]` marker is the bullet, so the list's own is dropped. */ | |
| 203 | li.on, li.off, li.trans { list-style: none; } | |
| 204 | li.on { color: var(--orgo-muted); text-decoration: line-through; } | |
| 205 | li.trans { color: var(--orgo-muted); } | |
| 206 | ||
| 207 | .footnotes { margin-top: 2.5rem; font-size: .9rem; color: var(--orgo-muted); } | |
| 208 | .footnote-ref a { text-decoration: none; } | |
| 209 | ||
| 210 | /* Table of contents: in the margin once there is a margin to put it in ------ */ | |
| 211 | ||
| 212 | nav.toc { | |
| 213 | background: var(--orgo-surface); | |
| 214 | border: 1px solid var(--orgo-rule); | |
| 215 | border-radius: 4px; | |
| 216 | padding: .6rem 1rem .8rem; | |
| 217 | margin: 0 0 1.5rem; | |
| 218 | font-size: .875rem; | |
| 219 | } | |
| 220 | nav.toc h2 { | |
| 221 | font-size: .7rem; | |
| 222 | text-transform: uppercase; | |
| 223 | letter-spacing: .07em; | |
| 224 | color: var(--orgo-muted); | |
| 225 | margin: 0 0 .35rem; | |
| 226 | padding: 0; | |
| 227 | border: 0; | |
| 228 | } | |
| 229 | nav.toc ul { margin: 0; padding-left: 1rem; } | |
| 230 | nav.toc li { margin: .1rem 0; } | |
| 231 | ||
| 232 | @media (min-width: 62rem) { | |
| 233 | main { max-width: var(--orgo-wide); } | |
| 234 | /* Floated rather than gridded: the layout puts the contents block wherever it likes | |
| 235 | in the document, and a float lands in the margin from any of those positions. */ | |
| 236 | nav.toc { | |
| 237 | float: right; | |
| 238 | width: 16rem; | |
| 239 | margin: 0 0 1.5rem 2.5rem; | |
| 240 | } | |
| 241 | /* Prose keeps its measure even though the page is wide; the things that actually | |
| 242 | want the extra room — code, tables, figures — are let out of it. */ | |
| 243 | main > * { max-width: var(--orgo-measure); } | |
| 244 | main > pre, main > table, main > figure, main > nav.toc { max-width: none; } | |
| 245 | } | |
| 246 | ||
| 247 | /* Listing pages ------------------------------------------------------------ */ | |
| 248 | ||
| 249 | ul.post-list { list-style: none; padding: 0; } | |
| 250 | ul.post-list > li { padding: .6rem 0; border-bottom: 1px solid var(--orgo-rule); } | |
| 251 | ul.post-list a { font-weight: 600; } | |
| 252 | ul.post-list time { color: var(--orgo-muted); font-family: var(--orgo-mono); font-size: .85rem; } | |
| 253 | p.excerpt { margin: .25rem 0; color: var(--orgo-muted); font-size: .9rem; } | |
| 254 | span.reading-time { font-size: .8rem; color: var(--orgo-muted); } | |
| 255 | ||
| 256 | ul.tag-list { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: .5rem 1.25rem; } | |
| 257 | ||
| 258 | nav.pagination { | |
| 259 | display: flex; | |
| 260 | gap: 1rem; | |
| 261 | align-items: baseline; | |
| 262 | margin-top: 1.5rem; | |
| 263 | font-size: .9rem; | |
| 264 | color: var(--orgo-muted); | |
| 265 | } | |
| 266 | ||
| 267 | /* Footer ------------------------------------------------------------------- */ | |
| 268 | ||
| 269 | body > footer { | |
| 270 | border-top: 1px solid var(--orgo-rule); | |
| 271 | padding: 1.25rem 1.5rem; | |
| 272 | color: var(--orgo-muted); | |
| 273 | font-size: .85rem; | |
| 274 | text-align: center; | |
| 275 | } | |