Current rework
This commit is contained in:
parent
53e053e9a7
commit
42ba2bf972
|
|
@ -3,10 +3,28 @@ _site/
|
||||||
_cache/
|
_cache/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
.env
|
||||||
# Defense-in-depth: catch any stray .env / .env.* anywhere in the tree
|
# Defense-in-depth: the auto-snapshot in `make build` stages content/
|
||||||
# (the auto-snapshot in the Makefile stages content/ on every build).
|
# wholesale. These patterns prevent any stray credential-shaped file
|
||||||
|
# (dropped accidentally during writing) from being staged + pushed.
|
||||||
|
# To intentionally commit one of these (rare), use `git add -f`.
|
||||||
**/.env
|
**/.env
|
||||||
**/.env.*
|
**/.env.*
|
||||||
|
**/*.env
|
||||||
|
**/*.key
|
||||||
|
**/*.pem
|
||||||
|
**/*.p12
|
||||||
|
**/*.pfx
|
||||||
|
**/id_rsa*
|
||||||
|
**/id_dsa*
|
||||||
|
**/id_ecdsa*
|
||||||
|
**/id_ed25519*
|
||||||
|
**/.netrc
|
||||||
|
**/.npmrc
|
||||||
|
**/.pypirc
|
||||||
|
**/credentials
|
||||||
|
**/credentials.json
|
||||||
|
**/credentials.yaml
|
||||||
|
**/credentials.yml
|
||||||
|
|
||||||
# Editor backup/swap files
|
# Editor backup/swap files
|
||||||
*~
|
*~
|
||||||
|
|
@ -58,6 +76,9 @@ data/semantic-meta.json
|
||||||
# IGNORE.txt is for the local build and need not be synced.
|
# IGNORE.txt is for the local build and need not be synced.
|
||||||
IGNORE.txt
|
IGNORE.txt
|
||||||
|
|
||||||
|
# Working notes / planning docs at the repo root (not site content).
|
||||||
|
checklist.md
|
||||||
|
|
||||||
# CV/résumé build pipeline (YAML → Jinja → xelatex). The canonical PDFs
|
# CV/résumé build pipeline (YAML → Jinja → xelatex). The canonical PDFs
|
||||||
# live under static/ and ship with the site; the pipeline itself is
|
# live under static/ and ship with the site; the pipeline itself is
|
||||||
# kept locally for regeneration but not version-controlled here.
|
# kept locally for regeneration but not version-controlled here.
|
||||||
|
|
|
||||||
17
Makefile
17
Makefile
|
|
@ -16,17 +16,12 @@ build:
|
||||||
# either reuses it (no new content/ changes) or appends another
|
# either reuses it (no new content/ changes) or appends another
|
||||||
# snapshot on top, so failures don't disappear from the log.
|
# snapshot on top, so failures don't disappear from the log.
|
||||||
#
|
#
|
||||||
# Pathspec is explicit (not `git add content/`) so a stray .env,
|
# `git add content/` respects .gitignore, which excludes credential-
|
||||||
# credential file, or other non-content artifact dropped under
|
# shaped patterns (.env, *.key, *.pem, id_rsa*, credentials*, etc.)
|
||||||
# content/ is NOT auto-staged. The :(glob) magic prefix makes `**`
|
# so a stray secret dropped under content/ is NOT auto-staged. To
|
||||||
# match across path components (git default fnmatch does not).
|
# intentionally commit a normally-ignored file, use `git add -f`
|
||||||
# Add new extensions here if a new asset type is introduced.
|
# manually before running `make build`.
|
||||||
@git add ':(glob)content/**/*.md' ':(glob)content/**/*.html' ':(glob)content/**/*.bib' \
|
@git add content/
|
||||||
':(glob)content/**/*.png' ':(glob)content/**/*.jpg' ':(glob)content/**/*.jpeg' \
|
|
||||||
':(glob)content/**/*.svg' ':(glob)content/**/*.gif' ':(glob)content/**/*.pdf' \
|
|
||||||
':(glob)content/**/*.mp3' ':(glob)content/**/*.ogg' ':(glob)content/**/*.flac' \
|
|
||||||
':(glob)content/**/*.yaml' ':(glob)content/**/*.yml' ':(glob)content/**/*.json' \
|
|
||||||
':(glob)content/**/*.css' ':(glob)content/**/*.tex'
|
|
||||||
@git diff --cached --quiet || git commit -m "auto: $$(date -u +%Y-%m-%dT%H:%M:%SZ) [skip ci]"
|
@git diff --cached --quiet || git commit -m "auto: $$(date -u +%Y-%m-%dT%H:%M:%SZ) [skip ci]"
|
||||||
@mkdir -p data
|
@mkdir -p data
|
||||||
@date +%s > data/build-start.txt
|
@date +%s > data/build-start.txt
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,281 @@
|
||||||
|
{-# LANGUAGE GHC2021 #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
-- | Now page: loads data/now.yaml and renders the active-projects view
|
||||||
|
-- and the recently-shipped archive for /current.html. Page-level
|
||||||
|
-- "Last updated" stamp is exposed as a context field; relative time
|
||||||
|
-- ("4 days ago") is computed at build time from getCurrentTime.
|
||||||
|
module Now
|
||||||
|
( nowCtx
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.Aeson (FromJSON (..), withObject, (.:), (.:?), (.!=))
|
||||||
|
import Data.Char (toUpper)
|
||||||
|
import Data.List (nub, sortBy)
|
||||||
|
import Data.Maybe (fromMaybe)
|
||||||
|
import Data.Ord (Down (..), comparing)
|
||||||
|
import Data.Time.Calendar (Day, diffDays)
|
||||||
|
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
||||||
|
import Data.Time.Format (defaultTimeLocale, formatTime, parseTimeM)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text.Encoding as TE
|
||||||
|
import qualified Data.Yaml as Y
|
||||||
|
import Hakyll hiding (escapeHtml)
|
||||||
|
import Contexts (siteCtx)
|
||||||
|
import Utils (escapeHtml)
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Entry types
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
data NowEntry = NowEntry
|
||||||
|
{ neTitle :: String
|
||||||
|
, neSection :: String
|
||||||
|
, neStatus :: String
|
||||||
|
, neUpdated :: String
|
||||||
|
, neLink :: Maybe String
|
||||||
|
, neNote :: Maybe String
|
||||||
|
, nePriority :: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
instance FromJSON NowEntry where
|
||||||
|
parseJSON = withObject "NowEntry" $ \o -> NowEntry
|
||||||
|
<$> o .: "title"
|
||||||
|
<*> o .: "section"
|
||||||
|
<*> o .: "status"
|
||||||
|
<*> o .: "updated"
|
||||||
|
<*> o .:? "link"
|
||||||
|
<*> o .:? "note"
|
||||||
|
<*> o .:? "priority" .!= 0
|
||||||
|
|
||||||
|
data NowShipped = NowShipped
|
||||||
|
{ nsTitle :: String
|
||||||
|
, nsCompleted :: String
|
||||||
|
, nsLink :: Maybe String
|
||||||
|
, nsNote :: Maybe String
|
||||||
|
}
|
||||||
|
|
||||||
|
instance FromJSON NowShipped where
|
||||||
|
parseJSON = withObject "NowShipped" $ \o -> NowShipped
|
||||||
|
<$> o .: "title"
|
||||||
|
<*> o .: "completed"
|
||||||
|
<*> o .:? "link"
|
||||||
|
<*> o .:? "note"
|
||||||
|
|
||||||
|
data NowDoc = NowDoc
|
||||||
|
{ nLastUpdated :: String
|
||||||
|
, nEntries :: [NowEntry]
|
||||||
|
, nShipped :: [NowShipped]
|
||||||
|
}
|
||||||
|
|
||||||
|
instance FromJSON NowDoc where
|
||||||
|
parseJSON = withObject "NowDoc" $ \o -> NowDoc
|
||||||
|
<$> o .: "last-updated"
|
||||||
|
<*> o .:? "entries" .!= []
|
||||||
|
<*> o .:? "shipped" .!= []
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Helpers
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | Section ordering follows first-appearance in entries. Reorder the
|
||||||
|
-- YAML to reorder the page; no separate ordering key required.
|
||||||
|
sectionOrder :: [NowEntry] -> [String]
|
||||||
|
sectionOrder = nub . map neSection
|
||||||
|
|
||||||
|
-- | Status ordering — "how close to shipping." Lower rank sorts first.
|
||||||
|
-- Statuses not listed sort below all known ones (rank 99) so a typo
|
||||||
|
-- surfaces visibly at the bottom of its section instead of silently
|
||||||
|
-- ranking next-to-the-top.
|
||||||
|
statusRanks :: [(String, Int)]
|
||||||
|
statusRanks =
|
||||||
|
[ ("in-review", 1)
|
||||||
|
, ("revising", 2)
|
||||||
|
, ("drafting", 3)
|
||||||
|
, ("building", 4)
|
||||||
|
, ("early-stage", 5)
|
||||||
|
, ("paused", 6)
|
||||||
|
]
|
||||||
|
|
||||||
|
statusRank :: String -> Int
|
||||||
|
statusRank s = fromMaybe 99 (lookup s statusRanks)
|
||||||
|
|
||||||
|
-- | Three-tier sort key for active entries:
|
||||||
|
-- 1. priority — manual override; higher floats up (default 0)
|
||||||
|
-- 2. statusRank — how close to shipping (lower is closer)
|
||||||
|
-- 3. updated — recency tiebreaker within the same rank
|
||||||
|
-- Sectioning is applied to the *unsorted* list so section ordering
|
||||||
|
-- continues to follow YAML source order; sorting happens within each
|
||||||
|
-- section's filtered slice.
|
||||||
|
entrySortKey :: NowEntry -> (Down Int, Int, Down String)
|
||||||
|
entrySortKey e =
|
||||||
|
( Down (nePriority e)
|
||||||
|
, statusRank (neStatus e)
|
||||||
|
, Down (neUpdated e)
|
||||||
|
)
|
||||||
|
|
||||||
|
-- | "early-stage" → "Early Stage", "research" → "Research".
|
||||||
|
titleCaseWords :: String -> String
|
||||||
|
titleCaseWords = unwords . map cap . wordsOnDash
|
||||||
|
where
|
||||||
|
cap [] = []
|
||||||
|
cap (x:xs) = toUpper x : xs
|
||||||
|
wordsOnDash s = case break (== '-') s of
|
||||||
|
(a, []) -> [a]
|
||||||
|
(a, _:rest) -> a : wordsOnDash rest
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- HTML rendering
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
renderStatusChip :: String -> String
|
||||||
|
renderStatusChip s = concat
|
||||||
|
[ "<span class=\"now-status now-status--", escapeHtml s, "\">"
|
||||||
|
, escapeHtml (titleCaseWords s)
|
||||||
|
, "</span>"
|
||||||
|
]
|
||||||
|
|
||||||
|
-- | Active-entry card. Reuses the .item-card / .item-card-* classes from
|
||||||
|
-- item-card.css so the Now page picks up the existing typographic
|
||||||
|
-- register; the .now-* classes layer status-chip + spacing on top.
|
||||||
|
renderEntry :: NowEntry -> String
|
||||||
|
renderEntry e = concat
|
||||||
|
[ "<li class=\"item-card now-card\">"
|
||||||
|
, "<span class=\"item-card-kind now-kind\">"
|
||||||
|
, renderStatusChip (neStatus e)
|
||||||
|
, "</span>"
|
||||||
|
, "<div class=\"item-card-main\">"
|
||||||
|
, "<div class=\"item-card-header\">"
|
||||||
|
, renderTitle (neLink e) (neTitle e)
|
||||||
|
, "<time class=\"item-card-date\" datetime=\"", escapeHtml (neUpdated e), "\">"
|
||||||
|
, escapeHtml (neUpdated e)
|
||||||
|
, "</time>"
|
||||||
|
, "</div>"
|
||||||
|
, maybe "" (\n -> "<p class=\"item-card-abstract is-full\">" ++ escapeHtml n ++ "</p>") (neNote e)
|
||||||
|
, "</div>"
|
||||||
|
, "</li>"
|
||||||
|
]
|
||||||
|
|
||||||
|
renderShippedEntry :: NowShipped -> String
|
||||||
|
renderShippedEntry s = concat
|
||||||
|
[ "<li class=\"item-card now-card now-card--shipped\">"
|
||||||
|
, "<span class=\"item-card-kind now-kind\">"
|
||||||
|
, renderStatusChip "shipped"
|
||||||
|
, "</span>"
|
||||||
|
, "<div class=\"item-card-main\">"
|
||||||
|
, "<div class=\"item-card-header\">"
|
||||||
|
, renderTitle (nsLink s) (nsTitle s)
|
||||||
|
, "<time class=\"item-card-date\" datetime=\"", escapeHtml (nsCompleted s), "\">"
|
||||||
|
, escapeHtml (nsCompleted s)
|
||||||
|
, "</time>"
|
||||||
|
, "</div>"
|
||||||
|
, maybe "" (\n -> "<p class=\"item-card-abstract is-full\">" ++ escapeHtml n ++ "</p>") (nsNote s)
|
||||||
|
, "</div>"
|
||||||
|
, "</li>"
|
||||||
|
]
|
||||||
|
|
||||||
|
renderTitle :: Maybe String -> String -> String
|
||||||
|
renderTitle mu title = case mu of
|
||||||
|
Just url -> "<a class=\"item-card-title\" href=\"" ++ escapeHtml url ++ "\">" ++ escapeHtml title ++ "</a>"
|
||||||
|
Nothing -> "<span class=\"item-card-title\">" ++ escapeHtml title ++ "</span>"
|
||||||
|
|
||||||
|
renderSection :: String -> [NowEntry] -> String
|
||||||
|
renderSection sec es = concat
|
||||||
|
[ "<section class=\"now-section library-section\">"
|
||||||
|
, "<h2 class=\"now-section-heading\">"
|
||||||
|
, escapeHtml (titleCaseWords sec)
|
||||||
|
, "</h2>"
|
||||||
|
, "<ul class=\"item-card-list\">"
|
||||||
|
, concatMap renderEntry es
|
||||||
|
, "</ul>"
|
||||||
|
, "</section>"
|
||||||
|
]
|
||||||
|
|
||||||
|
renderEntries :: [NowEntry] -> String
|
||||||
|
renderEntries [] = ""
|
||||||
|
renderEntries entries = concatMap renderOne (sectionOrder entries)
|
||||||
|
where
|
||||||
|
renderOne sec =
|
||||||
|
let inSec = filter ((== sec) . neSection) entries
|
||||||
|
sorted = sortBy (comparing entrySortKey) inSec
|
||||||
|
in renderSection sec sorted
|
||||||
|
|
||||||
|
renderShippedAll :: [NowShipped] -> String
|
||||||
|
renderShippedAll [] = ""
|
||||||
|
renderShippedAll items = concat
|
||||||
|
[ "<section class=\"now-section now-section--shipped library-section\">"
|
||||||
|
, "<h2 class=\"now-section-heading\">Recently Shipped</h2>"
|
||||||
|
, "<ul class=\"item-card-list\">"
|
||||||
|
, concatMap renderShippedEntry sorted
|
||||||
|
, "</ul>"
|
||||||
|
, "</section>"
|
||||||
|
]
|
||||||
|
where
|
||||||
|
sorted = sortBy (comparing (Down . nsCompleted)) items
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Date formatters — runs at build time
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | "2026-04-26" → "26 April 2026". Falls back to the raw ISO string
|
||||||
|
-- if the date is unparseable, so a typo in @last-updated@ surfaces
|
||||||
|
-- in the rendered page rather than blowing up the build.
|
||||||
|
formatWriterly :: String -> String
|
||||||
|
formatWriterly iso =
|
||||||
|
case parseTimeM True defaultTimeLocale "%Y-%m-%d" iso :: Maybe Day of
|
||||||
|
Nothing -> iso
|
||||||
|
Just d -> formatTime defaultTimeLocale "%-d %B %Y" d
|
||||||
|
|
||||||
|
relativeTime :: Day -> String -> String
|
||||||
|
relativeTime today iso =
|
||||||
|
case parseTimeM True defaultTimeLocale "%Y-%m-%d" iso :: Maybe Day of
|
||||||
|
Nothing -> ""
|
||||||
|
Just d -> bucket (diffDays today d)
|
||||||
|
where
|
||||||
|
bucket n
|
||||||
|
| n < 0 = ""
|
||||||
|
| n == 0 = "today"
|
||||||
|
| n == 1 = "yesterday"
|
||||||
|
| n < 7 = show n ++ " days ago"
|
||||||
|
| n < 28 = pluralize (n `div` 7) "week"
|
||||||
|
| n < 365 = pluralize (n `div` 30) "month"
|
||||||
|
| otherwise = pluralize (n `div` 365) "year"
|
||||||
|
pluralize 1 unit = "1 " ++ unit ++ " ago"
|
||||||
|
pluralize k unit = show k ++ " " ++ unit ++ "s ago"
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Load
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | UTF-8 round-trip String → ByteString. Hakyll's @getResourceBody@
|
||||||
|
-- hands us a 'String' (Unicode codepoints); the yaml library wants
|
||||||
|
-- a UTF-8 'ByteString'. 'Data.ByteString.Char8.pack' would truncate
|
||||||
|
-- each 'Char' to 8 bits — fine for ASCII, silent corruption for any
|
||||||
|
-- codepoint above 0x7F (e.g. em-dash 0x2014 → control char 0x14).
|
||||||
|
loadNow :: Compiler NowDoc
|
||||||
|
loadNow = do
|
||||||
|
rawItem <- load (fromFilePath "data/now.yaml") :: Compiler (Item String)
|
||||||
|
case Y.decodeEither' (TE.encodeUtf8 (T.pack (itemBody rawItem))) of
|
||||||
|
Left err -> fail ("now.yaml: " ++ show err)
|
||||||
|
Right doc -> return doc
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Context
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
nowCtx :: Context String
|
||||||
|
nowCtx =
|
||||||
|
constField "now" "true"
|
||||||
|
<> field "now-last-updated" (\_ -> nLastUpdated <$> loadNow)
|
||||||
|
<> field "now-last-updated-display" (\_ -> formatWriterly . nLastUpdated <$> loadNow)
|
||||||
|
<> field "now-last-updated-relative" (\_ -> do
|
||||||
|
doc <- loadNow
|
||||||
|
nowT <- unsafeCompiler getCurrentTime
|
||||||
|
let today = utctDay nowT
|
||||||
|
rel = relativeTime today (nLastUpdated doc)
|
||||||
|
if null rel
|
||||||
|
then noResult "no relative time"
|
||||||
|
else return rel
|
||||||
|
)
|
||||||
|
<> field "now-entries-html" (\_ -> renderEntries . nEntries <$> loadNow)
|
||||||
|
<> field "now-shipped-html" (\_ -> renderShippedAll . nShipped <$> loadNow)
|
||||||
|
<> siteCtx
|
||||||
|
|
@ -27,6 +27,7 @@ import Compilers (essayCompiler, postCompiler, pageCompiler, poetryCompiler, fi
|
||||||
compositionCompiler, sidecarCompiler)
|
compositionCompiler, sidecarCompiler)
|
||||||
import Catalog (musicCatalogCtx)
|
import Catalog (musicCatalogCtx)
|
||||||
import Commonplace (commonplaceCtx)
|
import Commonplace (commonplaceCtx)
|
||||||
|
import Now (nowCtx)
|
||||||
import Contexts (siteCtx, essayCtx, postCtx, pageCtx, poetryCtx, fictionCtx, compositionCtx,
|
import Contexts (siteCtx, essayCtx, postCtx, pageCtx, poetryCtx, fictionCtx, compositionCtx,
|
||||||
contentKindField, recentFirstByDisplay,
|
contentKindField, recentFirstByDisplay,
|
||||||
tagLinksFieldExcludingTopSegment)
|
tagLinksFieldExcludingTopSegment)
|
||||||
|
|
@ -206,6 +207,10 @@ rules = do
|
||||||
-- with dependency tracking by the commonplace page compiler.
|
-- with dependency tracking by the commonplace page compiler.
|
||||||
match "data/commonplace.yaml" $ compile getResourceBody
|
match "data/commonplace.yaml" $ compile getResourceBody
|
||||||
|
|
||||||
|
-- Now YAML — same pattern as commonplace. Loaded by Now.nowCtx for
|
||||||
|
-- /current.html. Re-compiles current.html when the YAML changes.
|
||||||
|
match "data/now.yaml" $ compile getResourceBody
|
||||||
|
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
-- Homepage
|
-- Homepage
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
@ -261,6 +266,19 @@ rules = do
|
||||||
>>= loadAndApplyTemplate "templates/default.html" commonplaceCtx
|
>>= loadAndApplyTemplate "templates/default.html" commonplaceCtx
|
||||||
>>= relativizeUrls
|
>>= relativizeUrls
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Now — research-first status page driven by data/now.yaml. Same
|
||||||
|
-- structural pattern as the commonplace page: markdown body
|
||||||
|
-- (optional intro prose) + structured YAML rendered into HTML by
|
||||||
|
-- Now.nowCtx, then assembled by templates/current.html.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
match "content/current.md" $ do
|
||||||
|
route $ constRoute "current.html"
|
||||||
|
compile $ pageCompiler
|
||||||
|
>>= loadAndApplyTemplate "templates/current.html" nowCtx
|
||||||
|
>>= loadAndApplyTemplate "templates/default.html" nowCtx
|
||||||
|
>>= relativizeUrls
|
||||||
|
|
||||||
match "content/colophon.md" $ do
|
match "content/colophon.md" $ do
|
||||||
route $ constRoute "colophon.html"
|
route $ constRoute "colophon.html"
|
||||||
compile $ essayCompiler
|
compile $ essayCompiler
|
||||||
|
|
@ -272,6 +290,7 @@ rules = do
|
||||||
.&&. complement "content/index.md"
|
.&&. complement "content/index.md"
|
||||||
.&&. complement "content/commonplace.md"
|
.&&. complement "content/commonplace.md"
|
||||||
.&&. complement "content/colophon.md"
|
.&&. complement "content/colophon.md"
|
||||||
|
.&&. complement "content/current.md"
|
||||||
.&&. complement "content/library.md") $ do
|
.&&. complement "content/library.md") $ do
|
||||||
route $ gsubRoute "content/" (const "")
|
route $ gsubRoute "content/" (const "")
|
||||||
`composeRoutes` setExtension "html"
|
`composeRoutes` setExtension "html"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
# Now — research-first status feed for /current.html.
|
||||||
|
#
|
||||||
|
# `last-updated` is the page-level temporal stamp. Bump it whenever
|
||||||
|
# you push any change here, even a one-line note edit; the value is
|
||||||
|
# rendered prominently and a relative-time tail ("4 days ago") is
|
||||||
|
# computed at build time.
|
||||||
|
#
|
||||||
|
# `entries:` are active items. Each carries:
|
||||||
|
# - title : display name
|
||||||
|
# - section : free-form section key (research, engineering, …);
|
||||||
|
# sections render in first-appearance order
|
||||||
|
# - status : in-review | revising | drafting | building | early-stage |
|
||||||
|
# paused. Free-form, but the listed values are the canonical
|
||||||
|
# ladder (closest-to-shipping → furthest) and have CSS accents.
|
||||||
|
# - updated : YYYY-MM-DD when this entry last moved (NOT page-stamp)
|
||||||
|
# - link : optional URL (artifact, preprint, repo, etc.)
|
||||||
|
# - note : optional one-sentence what's-happening-now line
|
||||||
|
# - priority : optional integer (default 0). Manual override on the sort
|
||||||
|
# order: positive floats up, negative sinks down. Use sparingly
|
||||||
|
# — the status ladder already captures most of the signal;
|
||||||
|
# priority is for "this is the headline regardless of where
|
||||||
|
# its status puts it" (or vice versa).
|
||||||
|
#
|
||||||
|
# Sort within each section: priority desc → status rank asc → updated desc.
|
||||||
|
# Section order: first-appearance in this file. Rearrange to taste.
|
||||||
|
#
|
||||||
|
# `shipped:` are recently-completed items. Each carries:
|
||||||
|
# - title, completed (date), optional link, optional note
|
||||||
|
# Sorted newest-first at render time. Curate by hand — items don't
|
||||||
|
# auto-prune. Move things off the list when they stop earning the slot.
|
||||||
|
|
||||||
|
last-updated: 2026-04-26
|
||||||
|
|
||||||
|
entries:
|
||||||
|
- title: "Order-Invariant ICD-10-CM Embedding (JAMA submission)"
|
||||||
|
section: research
|
||||||
|
status: in-review
|
||||||
|
updated: 2026-04-10
|
||||||
|
link: /essays/beyond-comorbidity-indices/
|
||||||
|
note: "Under review at JAMA Network Open. Calculator deployed at levineuwirth.github.io/icd_embeddings."
|
||||||
|
|
||||||
|
- title: "Semantic-embeddings citation work"
|
||||||
|
section: research
|
||||||
|
status: drafting
|
||||||
|
updated: 2026-04-22
|
||||||
|
link: https://neuroai.health
|
||||||
|
note: "Early-stage manuscript with NeuroAI. Preprint expected summer 2026."
|
||||||
|
|
||||||
|
- title: "SIMD / PQC, Phase 2 & 3"
|
||||||
|
section: research
|
||||||
|
status: building
|
||||||
|
updated: 2026-04-19
|
||||||
|
link: /essays/where-does-simd-help-post-quantum-cryptography/
|
||||||
|
note: "Hardware performance counters (PAPI), RAPL energy, and cross-ISA ports (ARM NEON/SVE, RISC-V V) on Brown's OSCAR cluster."
|
||||||
|
|
||||||
|
- title: "NeuroPose clinical-implications manuscript"
|
||||||
|
section: research
|
||||||
|
status: drafting
|
||||||
|
updated: 2026-04-08
|
||||||
|
link: /essays/neuropose/
|
||||||
|
note: "In preparation; target submission 2026–2027."
|
||||||
|
|
||||||
|
- title: "Magic: The Gathering reinforcement learning"
|
||||||
|
section: research
|
||||||
|
status: early-stage
|
||||||
|
updated: 2026-04-15
|
||||||
|
note: "Early-stage RL on Brown's OSCAR HPC cluster; expected late 2026."
|
||||||
|
|
||||||
|
- title: "CHASE 2026 conference submission"
|
||||||
|
section: research
|
||||||
|
status: in-review
|
||||||
|
updated: 2026-04-05
|
||||||
|
note: "IEEE/ACM Conference on Connected Health (CHASE), August 2026, in review."
|
||||||
|
|
||||||
|
- title: "Levshell"
|
||||||
|
section: engineering
|
||||||
|
status: building
|
||||||
|
updated: 2026-04-26
|
||||||
|
note: "Comprehensive rewrite of my Wayland-targeted productivity and research shell. Major focus over the summer of 2026."
|
||||||
|
|
||||||
|
- title: "Pmacs"
|
||||||
|
section: engineering
|
||||||
|
status: building
|
||||||
|
updated: 2026-04-20
|
||||||
|
note: "Emacs-inspired IDE in Zig with first-class parallelism."
|
||||||
|
|
||||||
|
shipped:
|
||||||
|
- title: "Where Does SIMD Help Post-Quantum Cryptography?"
|
||||||
|
completed: 2026-04-15
|
||||||
|
link: /essays/where-does-simd-help-post-quantum-cryptography/
|
||||||
|
note: "Phase 1 technical report and reproducible artifact. Brown CS Department."
|
||||||
|
|
@ -16,6 +16,7 @@ executable site
|
||||||
Authors
|
Authors
|
||||||
Catalog
|
Catalog
|
||||||
Commonplace
|
Commonplace
|
||||||
|
Now
|
||||||
Backlinks
|
Backlinks
|
||||||
Dingbat
|
Dingbat
|
||||||
SimilarLinks
|
SimilarLinks
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
/* now.css — /current.html.
|
||||||
|
*
|
||||||
|
* The Now page is a research-first status surface curated like the
|
||||||
|
* Library, with explicit per-item temporality. It composes on top of
|
||||||
|
* item-card.css (gated separately in head.html via $if(now)$) and
|
||||||
|
* library.css's section primitives are deliberately not pulled in —
|
||||||
|
* Now uses its own header treatment without dingbats or shelf
|
||||||
|
* dividers, since sections here are project-states rather than
|
||||||
|
* content shelves.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
PAGE-LEVEL "LAST UPDATED" MASTHEAD
|
||||||
|
Title leads, date follows in display-weight Spectral italic.
|
||||||
|
Descending visual hierarchy — BIG sans title → medium serif
|
||||||
|
italic date → tiny italic relative. The date carries enough
|
||||||
|
typographic weight to be the page's thesis without competing
|
||||||
|
with the title for the "what page is this" anchor.
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.now-header {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The page title remains the primary anchor — sized & weighted
|
||||||
|
by .page-title in components.css. Generous bottom margin gives
|
||||||
|
the masthead-date its own breathing room below. */
|
||||||
|
.now-header .page-title {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.now-stamp {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.15rem;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.now-stamp-label {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-variant: all-small-caps;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
color: var(--text-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The masthead datum. Spectral italic at ~1.85rem so it carries
|
||||||
|
presence as the page's thesis without competing with the
|
||||||
|
2.6rem sans title above it. Old-style numerals keep the
|
||||||
|
literary register; tabular-nums stay aligned if the day
|
||||||
|
width changes. */
|
||||||
|
.now-stamp-date {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 1.85rem;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-top: 0.05rem;
|
||||||
|
font-feature-settings: "onum" 1, "tnum" 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small italic footer line under the masthead — qualifies
|
||||||
|
the absolute date with a human-readable "how recent." */
|
||||||
|
.now-stamp-relative {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--text-faint);
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
INTRO PROSE
|
||||||
|
Optional body content from current.md, rendered between the
|
||||||
|
stamp and the first section. Sits in normal page measure; no
|
||||||
|
drop cap, no special treatment.
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.now-intro {
|
||||||
|
margin: 0 0 2.5rem;
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.now-intro p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
SECTION HEADINGS
|
||||||
|
Spectral small-caps in a quieter accent than library shelves.
|
||||||
|
No ornaments — sections are project-states, not content shelves,
|
||||||
|
and shouldn't carry the same identity weight.
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.now-section {
|
||||||
|
margin-bottom: 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.now-section-heading {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-variant: all-small-caps;
|
||||||
|
font-feature-settings: "smcp" 1;
|
||||||
|
letter-spacing: 0.09em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: none;
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0 0 0.85rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The recently-shipped section uses a slightly fainter heading +
|
||||||
|
a thin top divider to separate it visually from active work. */
|
||||||
|
.now-section--shipped {
|
||||||
|
margin-top: 3.5rem;
|
||||||
|
padding-top: 2.25rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.now-section--shipped .now-section-heading {
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-style: italic;
|
||||||
|
letter-spacing: 0.11em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
STATUS CHIP
|
||||||
|
Replaces the kind badge slot in item-card. Same min-width as
|
||||||
|
.item-card-kind so titles align across active and shipped
|
||||||
|
sections. Tabular sans-caps, very low-contrast frame.
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.now-card .now-kind {
|
||||||
|
/* Override item-card-kind text-only treatment with chip layout */
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.now-status {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.66rem;
|
||||||
|
font-variant: all-small-caps;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0.32em 0.55em 0.28em;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 2px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: transparent;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Per-status accents — each is intentionally restrained. Active
|
||||||
|
states (in-review, drafting, building) get slightly stronger
|
||||||
|
ink; passive states (paused, shipped) recede. */
|
||||||
|
.now-status--in-review { color: var(--text); border-color: var(--text-muted); }
|
||||||
|
.now-status--revising { color: var(--text); border-color: var(--text-muted); }
|
||||||
|
.now-status--drafting { color: var(--text); }
|
||||||
|
.now-status--building { color: var(--text); }
|
||||||
|
.now-status--early-stage { color: var(--text-muted); border-style: dashed; }
|
||||||
|
.now-status--paused { color: var(--text-faint); border-style: dashed; opacity: 0.75; }
|
||||||
|
.now-status--shipped { color: var(--text-faint); border-color: var(--text-faint); }
|
||||||
|
|
||||||
|
.now-card--shipped {
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
MOBILE
|
||||||
|
Mirror item-card.css's mobile rules for header stacking,
|
||||||
|
then shrink the chip column so titles get real room.
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
@media (max-width: 540px) {
|
||||||
|
.now-card .now-kind {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.now-status {
|
||||||
|
font-size: 0.62rem;
|
||||||
|
padding: 0.28em 0.45em 0.24em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.now-stamp-date {
|
||||||
|
font-size: 1.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.now-stamp-relative {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<main id="markdownBody" data-pagefind-body>
|
||||||
|
<header class="now-header">
|
||||||
|
<h1 class="page-title">$title$</h1>
|
||||||
|
<p class="now-stamp">
|
||||||
|
<span class="now-stamp-label">Last updated</span>
|
||||||
|
<time class="now-stamp-date" datetime="$now-last-updated$">$now-last-updated-display$</time>
|
||||||
|
$if(now-last-updated-relative)$<span class="now-stamp-relative">$now-last-updated-relative$</span>$endif$
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
$if(body)$<div class="now-intro">$body$</div>$endif$
|
||||||
|
|
||||||
|
$now-entries-html$
|
||||||
|
$now-shipped-html$
|
||||||
|
</main>
|
||||||
|
|
@ -40,6 +40,8 @@ $if(list-page)$<link rel="stylesheet" href="/css/item-card.css">$endif$
|
||||||
$if(memento-mori)$<link rel="stylesheet" href="/css/memento-mori.css">$endif$
|
$if(memento-mori)$<link rel="stylesheet" href="/css/memento-mori.css">$endif$
|
||||||
$if(catalog)$<link rel="stylesheet" href="/css/catalog.css">$endif$
|
$if(catalog)$<link rel="stylesheet" href="/css/catalog.css">$endif$
|
||||||
$if(commonplace)$<link rel="stylesheet" href="/css/commonplace.css">$endif$
|
$if(commonplace)$<link rel="stylesheet" href="/css/commonplace.css">$endif$
|
||||||
|
$if(now)$<link rel="stylesheet" href="/css/item-card.css">$endif$
|
||||||
|
$if(now)$<link rel="stylesheet" href="/css/now.css">$endif$
|
||||||
$if(build)$<link rel="stylesheet" href="/css/build.css">$endif$
|
$if(build)$<link rel="stylesheet" href="/css/build.css">$endif$
|
||||||
$if(reading)$<link rel="stylesheet" href="/css/reading.css">$endif$
|
$if(reading)$<link rel="stylesheet" href="/css/reading.css">$endif$
|
||||||
$if(composition)$<link rel="stylesheet" href="/css/score-reader.css">$endif$
|
$if(composition)$<link rel="stylesheet" href="/css/score-reader.css">$endif$
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@
|
||||||
<div class="nav-primary">
|
<div class="nav-primary">
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="/library.html">Library</a>
|
<a href="/library.html">Library</a>
|
||||||
|
<a href="/links.html">Links</a>
|
||||||
<a href="/me.html">Me</a>
|
<a href="/me.html">Me</a>
|
||||||
<a href="/new.html">New</a>
|
<a href="/new.html">New</a>
|
||||||
<a href="/links.html">Links</a>
|
<a href="/current.html">Current</a>
|
||||||
<a href="/search.html">Search</a>
|
<a href="/search.html">Search</a>
|
||||||
|
<a href="/about.html">Vita</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-controls">
|
<div class="nav-controls">
|
||||||
<button type="button" class="nav-portal-toggle" aria-label="Toggle portals" aria-expanded="false">
|
<button type="button" class="nav-portal-toggle" aria-label="Toggle portals" aria-expanded="false">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue