diff --git a/src/components/dev/DevBanner.astro b/src/components/dev/DevBanner.astro index 201bb4c..94ccf13 100644 --- a/src/components/dev/DevBanner.astro +++ b/src/components/dev/DevBanner.astro @@ -22,25 +22,33 @@ const site = import.meta.env.DEV ? await getSite() : null const here = site ? [...forEntry(site.diagnostics, collection, id), ...diagnostics] : [] const elsewhere = site ? site.diagnostics.filter((d) => !here.includes(d)) : [] const notice = status ? NOTICE[status] : undefined +const label = [status && status !== 'published' ? status : null, here.length || null] + .filter(Boolean) + .join(' · ') --- {site && (here.length || elsewhere.length || notice) && ( - +
+ + Dev{label && {label}} + +
+ {notice &&

{notice}

} + {here.length > 0 && ( + + )} + {elsewhere.length > 0 && ( +
+ {elsewhere.length} elsewhere +
{formatDiagnostics(elsewhere)}
+
+ )} +
+
)} diff --git a/src/components/home/ActivityStrip.astro b/src/components/home/ActivityStrip.astro new file mode 100644 index 0000000..bea4f7d --- /dev/null +++ b/src/components/home/ActivityStrip.astro @@ -0,0 +1,31 @@ +--- +import type { Activity } from '@/lib/site/types' + +type Props = { + activity: Activity[] +} + +const { activity } = Astro.props +const total = activity.length +const touched = activity.filter((cell) => cell.touched).length +const latest = activity.at(-1)?.touched ?? false +const summary = `Updated in ${touched} of ${total} recent release${total === 1 ? '' : 's'}${latest ? ', including the latest' : ''}` +--- + +{total > 0 && ( +

+ {summary} +

+)} diff --git a/src/components/home/Changelog.astro b/src/components/home/Changelog.astro new file mode 100644 index 0000000..5b3213f --- /dev/null +++ b/src/components/home/Changelog.astro @@ -0,0 +1,47 @@ +--- +import { CHANGE_TYPES } from '@/lib/constants/changelog' +import { toMilitaryDTG } from '@/lib/content/dates' +import { trailOf } from '@/lib/site/navigation' +import type { Change, Release } from '@/lib/site/types' + +type Props = { + releases: Release[] +} + +const LIMIT = 4 + +const { releases } = Astro.props + +const labelOf = (kind: Change['kind']) => + CHANGE_TYPES.find((option) => option.value === kind)?.label ?? kind + +const workOf = (change: Change) => (change.article ? trailOf(change.article)[0].title : 'Site') +--- + +
    + {releases.map((release) => { + const more = release.changes.length - LIMIT + return ( +
  1. +
    +

    v{release.version}

    + +
    + + {more > 0 &&

    +{more} more

    } +
  2. + ) + })} +
diff --git a/src/components/home/HomeHero.astro b/src/components/home/HomeHero.astro new file mode 100644 index 0000000..7b9caf7 --- /dev/null +++ b/src/components/home/HomeHero.astro @@ -0,0 +1,67 @@ +--- +import { SITE } from '@/lib/constants/site' +import { toMilitaryDTG } from '@/lib/content/dates' +import type { Release } from '@/lib/site/types' + +type Props = { + works: number + entries: number + release?: Release +} + +const { works, entries, release } = Astro.props +--- + +
+
+

Index

+

{SITE.title}

+
+
+
+ + About +
+
{SITE.tagline}
+
+
+
+ + Works +
+
{works}
+
Top-level
+
+
+
+ + Entries +
+
{entries}
+
Articles
+
+
+
+ + Latest release +
+
{release ? `v${release.version}` : '—'}
+
+ {release ? ( + + ) : ( + 'None yet' + )} +
+
+
+
+
diff --git a/src/components/home/HomeSection.astro b/src/components/home/HomeSection.astro new file mode 100644 index 0000000..aef6b60 --- /dev/null +++ b/src/components/home/HomeSection.astro @@ -0,0 +1,21 @@ +--- +import { toRoman } from '@/lib/content/numerals' + +type Props = { + index: number + title: string +} + +const { index, title } = Astro.props +const id = `home-section-${index}` +--- + +
+

+ + {title} +

+ +
diff --git a/src/components/home/WorkList.astro b/src/components/home/WorkList.astro new file mode 100644 index 0000000..02847c0 --- /dev/null +++ b/src/components/home/WorkList.astro @@ -0,0 +1,44 @@ +--- +import { toRoman } from '@/lib/content/numerals' +import { activityOf } from '@/lib/site/activity' +import type { Article, Release } from '@/lib/site/types' + +import ActivityStrip from './ActivityStrip.astro' + +type Props = { + works: Article[] + releases: Release[] +} + +const { works, releases } = Astro.props +--- + +
    + {works.map((work, i) => ( +
  1. + +
    +

    + {work.title} +

    + {work.summary.excerpt &&

    {work.summary.excerpt}

    } +
    +
    + {work.children.length > 0 ? ( + + ) : ( +

    —

    + )} + +
    +
  2. + ))} +
diff --git a/src/content/general/articles/awq.mdoc b/src/content/general/articles/awq.mdoc new file mode 100644 index 0000000..960fecd --- /dev/null +++ b/src/content/general/articles/awq.mdoc @@ -0,0 +1,15 @@ +--- +title: Advanced Warhammer Quest +status: draft +noindex: false +stage: stub +route: + index: 3 + sequence: false +summary: + cover: + effect: none + ratio: original +published: 2026-09-30T16:46:00.000Z +tags: [] +--- diff --git a/src/content/general/articles/chainbreaker.mdoc b/src/content/general/articles/chainbreaker.mdoc index 7b3aafd..5b7ffa2 100644 --- a/src/content/general/articles/chainbreaker.mdoc +++ b/src/content/general/articles/chainbreaker.mdoc @@ -5,6 +5,7 @@ status: draft noindex: false stage: stub route: + index: 2 sequence: false summary: cover: diff --git a/src/content/general/articles/the-crucible.mdoc b/src/content/general/articles/the-crucible.mdoc index 213d6e5..8652269 100644 --- a/src/content/general/articles/the-crucible.mdoc +++ b/src/content/general/articles/the-crucible.mdoc @@ -2,12 +2,17 @@ title: The Crucible status: draft noindex: false -route: {} +stage: stub +route: + index: 1 + sequence: false summary: excerpt: >- Where we explore the crucible – a materialistic alchemist Worldbuilding Framework - cover: {} + cover: + effect: none + ratio: original published: 2026-09-21T12:33:00.000Z tags: - the-crucible diff --git a/src/content/general/pages/imprint.mdoc b/src/content/general/pages/imprint.mdoc index 38f8a2d..c88f5a6 100644 --- a/src/content/general/pages/imprint.mdoc +++ b/src/content/general/pages/imprint.mdoc @@ -5,3 +5,4 @@ noindex: false route: {} summary: {} --- +Set in Blaka{% footnote note="Jdajhdasljkh" /%}. diff --git a/src/layouts/general/ArticleLayout.astro b/src/layouts/general/ArticleLayout.astro index 4e5b653..990a715 100644 --- a/src/layouts/general/ArticleLayout.astro +++ b/src/layouts/general/ArticleLayout.astro @@ -31,12 +31,16 @@ const sequence = sequenceOf(article)
- -
- -
+ {Astro.slots.has('margin') && ( + + )} + {rendered.footnotes.length > 0 && ( +
+ +
+ )} @@ -61,7 +65,6 @@ const sequence = sequenceOf(article) @mixin layout-columns; display: grid; - row-gap: var(--layout-gutter); align-items: start; } } @@ -95,6 +98,7 @@ const sequence = sequenceOf(article) .notes { grid-column: body-start / body-end; + margin-top: var(--layout-gutter); } } diff --git a/src/layouts/general/PageLayout.astro b/src/layouts/general/PageLayout.astro index f10a0f6..4bcd2ba 100644 --- a/src/layouts/general/PageLayout.astro +++ b/src/layouts/general/PageLayout.astro @@ -1,5 +1,6 @@ --- import DevBanner from '@/components/dev/DevBanner.astro' +import FootnoteList from '@/components/markdoc/blocks/general/FootnoteList.astro' import Node from '@/components/markdoc/Node.astro' import SimpleHeader from '@/components/post/Header/SimpleHeader.astro' import BaseLayout from '@/layouts/BaseLayout.astro' @@ -29,6 +30,11 @@ const meta = metaOf(page)
+ {rendered.footnotes.length > 0 && ( +
+ +
+ )} @@ -38,15 +44,24 @@ const meta = metaOf(page) @mixin layout-wrapper; @mixin my var(--size-12); + display: flex; + flex-direction: column; + gap: var(--layout-gutter); + @media (--bp-desktop) { @mixin layout-columns; display: grid; + row-gap: var(--layout-gutter); } } .body { position: relative; + } + + .body, + .notes { min-width: 0; @media (--bp-desktop) { diff --git a/src/lib/constants/site.ts b/src/lib/constants/site.ts new file mode 100644 index 0000000..abf11c2 --- /dev/null +++ b/src/lib/constants/site.ts @@ -0,0 +1,4 @@ +export const SITE = { + title: 'dave-dmg', + tagline: 'Essays, frameworks and game tools', +} as const diff --git a/src/lib/site/activity.ts b/src/lib/site/activity.ts new file mode 100644 index 0000000..2329977 --- /dev/null +++ b/src/lib/site/activity.ts @@ -0,0 +1,16 @@ +import { trailOf } from './navigation' +import type { Activity, Article, Release } from './types' + +const LENGTH = 10 + +export function activityOf(work: Article, releases: Release[]): Activity[] { + return releases + .slice(0, LENGTH) + .reverse() + .map((release) => ({ + release, + touched: release.changes.some( + (change) => change.article && trailOf(change.article)[0] === work, + ), + })) +} diff --git a/src/lib/site/types.ts b/src/lib/site/types.ts index 0e781f2..4e025a5 100644 --- a/src/lib/site/types.ts +++ b/src/lib/site/types.ts @@ -61,3 +61,8 @@ export type DirectoryEntry = { parent: string | null depth: number } + +export type Activity = { + release: Release + touched: boolean +} diff --git a/src/pages/index.astro b/src/pages/index.astro index c7d8da6..bd8da9a 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,8 +1,28 @@ --- +import Changelog from '@/components/home/Changelog.astro' +import HomeHero from '@/components/home/HomeHero.astro' +import HomeSection from '@/components/home/HomeSection.astro' +import WorkList from '@/components/home/WorkList.astro' import BaseLayout from '@/layouts/BaseLayout.astro' +import { SITE } from '@/lib/constants/site' +import { getSite } from '@/lib/site' + +const site = await getSite() +const entries = [...site.articles.values()].filter((article) => article.listed).length +const releases = site.releases.slice(0, 3) +const hasChangelog = releases.length > 0 --- - -

I AM TEST

-

ARE YOU TEST?

+ + + {hasChangelog && ( + + + + )} + {site.roots.length > 0 && ( + + + + )} diff --git a/src/styles/components/dev/dev-banner.css b/src/styles/components/dev/dev-banner.css new file mode 100644 index 0000000..b1e0ba1 --- /dev/null +++ b/src/styles/components/dev/dev-banner.css @@ -0,0 +1,122 @@ +@layer components.blocks { + .dev-banner { + @mixin border-t var(--border-boundary), solid, var(--color-tertiary); + + position: fixed; + z-index: 2; + bottom: var(--size-6); + left: var(--size-6); + + overflow-y: auto; + + max-width: min(28rem, 100% - 2 * var(--size-6)); + max-height: 50dvh; + + color: var(--color-text-primary); + + background-color: var(--color-surface-base); + box-shadow: var(--shadow-raised); + } + + .dev-banner__bar { + @mixin typo_eyebrow; + @mixin py var(--size-4); + @mixin px var(--size-6); + + cursor: pointer; + font-weight: var(--font-weight-emphasis); + color: var(--color-tertiary); + list-style: none; + + &::-webkit-details-marker { + display: none; + } + + &:focus-visible { + @mixin focus-ring; + } + } + + .dev-banner__crumb { + &::before { + content: "⟫" / ""; + margin-inline: var(--size-2); + font-family: var(--font-body); + } + } + + .dev-banner__body { + @mixin px var(--size-6); + @mixin pb var(--size-6); + + display: grid; + gap: var(--size-4); + } + + .dev-banner__notice { + @mixin typo_eyebrow; + @mixin ma var(--size-0); + + color: var(--color-text-secondary); + } + + .dev-banner__list { + @mixin ma var(--size-0); + @mixin pa var(--size-0); + + display: grid; + gap: var(--size-2); + list-style: none; + } + + .dev-banner__item { + @mixin typo_code; + + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: var(--size-4); + + &::before { + font-family: var(--font-body); + } + } + + .dev-banner__item--error { + &::before { + content: "✖" / ""; + color: var(--color-state-error); + } + } + + .dev-banner__item--warning { + &::before { + content: "▲" / ""; + color: var(--color-state-warning); + } + } + + .dev-banner__elsewhere { + @mixin pt var(--size-4); + @mixin border-t var(--border-hairline), solid, var(--color-border-subtle); + } + + .dev-banner__toggle { + @mixin typo_eyebrow; + + cursor: pointer; + color: var(--color-text-quiet); + + &:focus-visible { + @mixin focus-ring; + } + } + + .dev-banner__log { + @mixin typo_code; + @mixin mt var(--size-4); + @mixin mb var(--size-0); + + color: var(--color-text-secondary); + white-space: pre-wrap; + } +} diff --git a/src/styles/components/dev/index.css b/src/styles/components/dev/index.css new file mode 100644 index 0000000..ee89123 --- /dev/null +++ b/src/styles/components/dev/index.css @@ -0,0 +1 @@ +@import url("dev-banner.css"); diff --git a/src/styles/components/home/activity-strip.css b/src/styles/components/home/activity-strip.css new file mode 100644 index 0000000..00e179f --- /dev/null +++ b/src/styles/components/home/activity-strip.css @@ -0,0 +1,30 @@ +@layer components.blocks { + .activity-strip { + @mixin ma var(--size-0); + } + + .activity-strip__summary { + @mixin visually-hidden; + } + + .activity-strip__cells { + display: flex; + gap: var(--size-1); + } + + .activity-strip__cell { + aspect-ratio: 1; + width: var(--size-5); + border: var(--border-hairline) solid var(--color-border-normal); + } + + .activity-strip__cell--touched { + border-color: var(--color-surface-inverse); + background-color: var(--color-surface-inverse); + } + + .activity-strip__cell--latest { + border-color: var(--color-primary); + background-color: var(--color-primary); + } +} diff --git a/src/styles/components/home/changelog.css b/src/styles/components/home/changelog.css new file mode 100644 index 0000000..90634a9 --- /dev/null +++ b/src/styles/components/home/changelog.css @@ -0,0 +1,121 @@ +@layer components.blocks { + .changelog { + @mixin ma var(--size-0); + @mixin pa var(--size-0); + + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--border-card); + + border: var(--border-card) solid var(--color-border-strong); + + list-style: none; + + background-color: var(--color-border-strong); + + @media (--bp-tablet-up) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + } + + .changelog__release { + @mixin pa var(--size-8); + + display: flex; + flex-direction: column; + gap: var(--size-6); + background-color: var(--color-surface-elevated-1); + } + + .changelog__head { + display: flex; + gap: var(--size-4); + align-items: baseline; + justify-content: space-between; + } + + .changelog__version { + @mixin ma var(--size-0); + + font-family: var(--font-header); + font-size: var(--type-6); + font-weight: var(--font-weight-heading); + line-height: 1; + } + + .changelog__date { + @mixin typo_tag; + + &::before { + content: "⟫" / ""; + margin-inline-end: var(--size-2); + font-family: var(--font-body); + } + } + + .changelog__changes { + @mixin ma var(--size-0); + @mixin pa var(--size-0); + + display: grid; + gap: var(--size-6); + list-style: none; + } + + .changelog__change { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: var(--size-2) var(--size-4); + align-items: center; + } + + .changelog__kind { + @mixin typo_tag; + @mixin py var(--size-1); + @mixin px var(--size-3); + + justify-self: start; + color: var(--color-text-inverse); + background-color: var(--color-surface-inverse); + } + + .changelog__work { + @mixin typo_eyebrow; + + overflow: hidden; + + color: var(--color-text-quiet); + text-align: end; + text-overflow: ellipsis; + white-space: nowrap; + } + + .changelog__text { + @mixin typo_body-small; + @mixin ma var(--size-0); + + grid-column: 1 / -1; + + & a { + text-decoration: underline; + text-decoration-color: var(--color-border-normal); + text-underline-offset: 0.2em; + + &:hover { + text-decoration-color: var(--color-primary); + } + + &:focus-visible { + @mixin focus-ring; + } + } + } + + .changelog__more { + @mixin typo_eyebrow; + @mixin mt auto; + @mixin mb var(--size-0); + + color: var(--color-text-quiet); + } +} diff --git a/src/styles/components/home/home-hero.css b/src/styles/components/home/home-hero.css new file mode 100644 index 0000000..ea04e73 --- /dev/null +++ b/src/styles/components/home/home-hero.css @@ -0,0 +1,114 @@ +@layer components.blocks { + .home-hero { + color: var(--color-text-primary); + background-color: var(--color-surface-base); + } + + .home-hero__inner { + @mixin layout-wrapper; + @mixin py var(--size-12); + + display: grid; + gap: var(--size-10); + } + + .home-hero__eyebrow { + @mixin typo_eyebrow; + @mixin ma var(--size-0); + @mixin pb var(--size-4); + @mixin border-b var(--border-hairline), solid, var(--color-border-subtle); + + color: var(--color-text-quiet); + } + + .home-hero__title { + @mixin ma var(--size-0); + + font-family: var(--font-display); + font-size: var(--type-slab); + font-weight: var(--font-weight-display); + line-height: 0.9; + text-shadow: var(--size-2) var(--size-2) 0 var(--color-primary); + letter-spacing: var(--tracking-display); + } + + .home-hero__stats { + @mixin ma var(--size-0); + + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--border-hairline); + + border: var(--border-hairline) solid var(--color-border-subtle); + + background-color: var(--color-border-subtle); + + @media (--bp-tablet-up) { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + } + + .home-hero__stat { + @mixin pa var(--size-8); + + display: flex; + flex-direction: column; + gap: var(--size-3); + + min-height: var(--size-16); + + background-color: var(--color-surface-elevated-1); + + & dd { + @mixin ma var(--size-0); + } + } + + .home-hero__stat--about { + grid-column: 1 / -1; + color: var(--color-text-on-accent); + background-color: var(--color-primary); + + @media (--bp-tablet-up) { + grid-column: auto; + } + } + + .home-hero__label { + @mixin typo_eyebrow; + + color: var(--color-text-secondary); + + .home-hero__stat--about & { + color: inherit; + } + } + + .home-hero__index { + &::after { + content: "⟫" / ""; + margin-inline: var(--size-2); + font-family: var(--font-body); + } + } + + .home-hero__tagline { + @mixin typo_heading-3; + @mixin mt auto; + } + + .home-hero__value { + @mixin mt auto; + + font-family: var(--font-header); + font-size: var(--type-7); + font-weight: var(--font-weight-heading); + line-height: 1; + } + + .home-hero__note { + @mixin typo_eyebrow; + + color: var(--color-text-quiet); + } +} diff --git a/src/styles/components/home/home-section.css b/src/styles/components/home/home-section.css new file mode 100644 index 0000000..d937221 --- /dev/null +++ b/src/styles/components/home/home-section.css @@ -0,0 +1,25 @@ +@layer components.blocks { + .home-section { + @mixin layout-wrapper; + @mixin my var(--size-14); + } + + .home-section__heading { + @mixin typo_heading-2; + @mixin mt var(--size-0); + @mixin mb var(--size-8); + @mixin pt var(--size-6); + @mixin border-t var(--border-boundary), solid, var(--color-border-strong); + + display: flex; + gap: var(--size-6); + align-items: baseline; + } + + .home-section__number { + @mixin typo_eyebrow; + + font-weight: var(--font-weight-emphasis); + color: var(--color-text-secondary); + } +} diff --git a/src/styles/components/home/index.css b/src/styles/components/home/index.css new file mode 100644 index 0000000..853879a --- /dev/null +++ b/src/styles/components/home/index.css @@ -0,0 +1,5 @@ +@import url("home-hero.css"); +@import url("home-section.css"); +@import url("changelog.css"); +@import url("work-list.css"); +@import url("activity-strip.css"); diff --git a/src/styles/components/home/work-list.css b/src/styles/components/home/work-list.css new file mode 100644 index 0000000..5e1583e --- /dev/null +++ b/src/styles/components/home/work-list.css @@ -0,0 +1,119 @@ +@layer components.blocks { + .work-list { + @mixin ma var(--size-0); + @mixin pa var(--size-0); + + list-style: none; + } + + .work-list__item { + @mixin py var(--size-8); + @mixin border-b var(--border-hairline), solid, var(--color-border-strong); + + display: grid; + grid-template-areas: + "numeral main" + "numeral meta"; + grid-template-columns: var(--size-14) minmax(0, 1fr); + gap: var(--size-6) var(--size-10); + align-items: center; + + @media (--bp-tablet-up) { + grid-template-areas: "numeral main meta"; + grid-template-columns: var(--size-16) minmax(0, 1fr) auto; + } + } + + .work-list__numeral { + grid-area: numeral; + align-self: start; + + font-family: var(--font-header); + font-size: var(--type-9); + font-weight: var(--font-weight-heading); + line-height: 1; + } + + .work-list__main { + grid-area: main; + min-width: 0; + } + + .work-list__title { + @mixin ma var(--size-0); + + font-family: var(--font-header); + font-size: clamp(var(--type-5), 3vw, var(--type-8)); + font-weight: var(--font-weight-heading); + line-height: 1; + text-transform: uppercase; + letter-spacing: var(--tracking-8); + + & a { + &:hover { + text-decoration: underline; + text-decoration-color: var(--color-primary); + text-decoration-thickness: var(--border-card); + } + + &:focus-visible { + @mixin focus-ring; + } + } + } + + .work-list__excerpt { + @mixin typo_body-small; + @mixin mt var(--size-3); + @mixin mb var(--size-0); + + color: var(--color-text-secondary); + } + + .work-list__meta { + display: flex; + grid-area: meta; + flex-direction: column; + gap: var(--size-4); + align-items: flex-start; + + @media (--bp-tablet-up) { + align-items: flex-end; + } + } + + .work-list__sections { + @mixin typo_eyebrow; + @mixin ma var(--size-0); + @mixin pa var(--size-0); + + display: flex; + flex-wrap: wrap; + gap: var(--size-2) var(--size-3); + + color: var(--color-text-secondary); + list-style: none; + + & li + li { + &::before { + content: "·" / ""; + margin-inline-end: var(--size-3); + } + } + + & a { + &:hover { + color: var(--color-text-primary); + text-decoration: underline; + } + + &:focus-visible { + @mixin focus-ring; + } + } + + @media (--bp-tablet-up) { + justify-content: flex-end; + } + } +} diff --git a/src/styles/components/index.css b/src/styles/components/index.css index 35d880c..543409f 100644 --- a/src/styles/components/index.css +++ b/src/styles/components/index.css @@ -2,3 +2,5 @@ @import url("blocks/index.css"); @import url("post/index.css"); @import url("site/index.css"); +@import url("home/index.css"); +@import url("dev/index.css");