Merge pull request 'Blog' (#1) from blog into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"css": {
|
||||
"lint": {
|
||||
"propertyIgnoredDueToDisplay": "ignore",
|
||||
"unknownAtRules": "ignore",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,13 +2,24 @@ import { defineConfig, fontProviders } from 'astro/config'
|
||||
|
||||
import keystatic from '@keystatic/astro'
|
||||
|
||||
import markdoc from '@astrojs/markdoc'
|
||||
import node from '@astrojs/node'
|
||||
import react from '@astrojs/react'
|
||||
|
||||
import { contentReload } from './src/integrations/content-reload'
|
||||
|
||||
const devOnly = (integration) => ({
|
||||
name: `${integration.name}:dev-only`,
|
||||
hooks: {
|
||||
'astro:config:setup': async (params) => {
|
||||
if (params.command !== 'dev') return
|
||||
await integration.hooks['astro:config:setup']?.(params)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://dave-dmg.de',
|
||||
integrations: [react(), markdoc(), keystatic()],
|
||||
integrations: [react(), devOnly(keystatic()), contentReload()],
|
||||
|
||||
adapter: node({
|
||||
mode: 'standalone',
|
||||
@@ -18,11 +29,13 @@ export default defineConfig({
|
||||
provider: fontProviders.google(),
|
||||
name: 'Geist',
|
||||
cssVariable: '--font-geist',
|
||||
weights: ['100 900'],
|
||||
},
|
||||
{
|
||||
provider: fontProviders.google(),
|
||||
name: 'Geist Mono',
|
||||
cssVariable: '--font-geist-mono',
|
||||
weights: ['100 900'],
|
||||
},
|
||||
{
|
||||
provider: fontProviders.google(),
|
||||
|
||||
@@ -3,6 +3,7 @@ import prettier from 'eslint-config-prettier/flat'
|
||||
import astro from 'eslint-plugin-astro'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import unusedImports from 'eslint-plugin-unused-imports'
|
||||
import globals from 'globals'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default [
|
||||
@@ -11,7 +12,10 @@ export default [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...astro.configs.recommended,
|
||||
|
||||
{
|
||||
name: 'dave-dmg/node',
|
||||
languageOptions: { globals: globals.node },
|
||||
},
|
||||
{
|
||||
name: 'dave-dmg/unused',
|
||||
files: ['**/*.{js,mjs,cjs,ts,tsx,astro}'],
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import { config } from '@keystatic/core'
|
||||
|
||||
import { contentCollections } from '@/keystatic/collections/general'
|
||||
import { siteCollections } from '@/keystatic/collections/site'
|
||||
import { ARTICLES, PAGES, RELEASES, TAGS } from '@/lib/constants/collections'
|
||||
|
||||
export default config({
|
||||
storage: {
|
||||
kind: 'local',
|
||||
},
|
||||
collections: {},
|
||||
ui: {
|
||||
navigation: {
|
||||
Content: [ARTICLES, PAGES, TAGS],
|
||||
Site: [RELEASES],
|
||||
},
|
||||
},
|
||||
collections: {
|
||||
...contentCollections,
|
||||
...siteCollections,
|
||||
},
|
||||
})
|
||||
|
||||
12
package.json
12
package.json
@@ -12,20 +12,24 @@
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/markdoc": "^2.0.1",
|
||||
"@astrojs/node": "^11.0.0",
|
||||
"@astrojs/react": "^6.0.0",
|
||||
"@keystar/ui": "^0.10.0",
|
||||
"@keystatic/astro": "^6.0.0",
|
||||
"@keystatic/core": "^0.6.7",
|
||||
"@markdoc/markdoc": "^0.4.0",
|
||||
"astro": "^7.0.4",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8"
|
||||
"react-dom": "^19.2.8",
|
||||
"sharp": "^0.35.4",
|
||||
"shiki": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.10",
|
||||
"@csstools/postcss-global-data": "^4.0.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
|
||||
"@types/node": "^22.20.4",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"eslint": "^10.11.0",
|
||||
@@ -33,6 +37,7 @@
|
||||
"eslint-plugin-astro": "^3.2.1",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.4.1",
|
||||
"globals": "^17.12.0",
|
||||
"postcss": "^8.5.28",
|
||||
"postcss-custom-media": "^12.0.1",
|
||||
"postcss-mixins": "^12.1.2",
|
||||
@@ -44,7 +49,8 @@
|
||||
"stylelint-config-html": "^2.0.0",
|
||||
"stylelint-config-standard": "^40.0.0",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.70.0"
|
||||
"typescript-eslint": "^8.70.0",
|
||||
"vite": "^8.3.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.15.0",
|
||||
"engines": {
|
||||
|
||||
173
pnpm-lock.yaml
generated
173
pnpm-lock.yaml
generated
@@ -11,30 +11,39 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@astrojs/markdoc':
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.9(@types/react@19.3.0)(astro@7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))(react@19.3.0)
|
||||
'@astrojs/node':
|
||||
specifier: ^11.0.0
|
||||
version: 11.1.6(astro@7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))
|
||||
version: 11.1.6(astro@7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@22.20.4)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))
|
||||
'@astrojs/react':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.6(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(esbuild@0.28.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
version: 6.0.6(@types/node@22.20.4)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(esbuild@0.28.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
'@keystar/ui':
|
||||
specifier: ^0.10.0
|
||||
version: 0.10.0(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0)
|
||||
'@keystatic/astro':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(@keystatic/core@0.6.9(@keystar/ui@0.10.0(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0))(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0))(astro@7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
|
||||
version: 6.0.0(@keystatic/core@0.6.9(@keystar/ui@0.10.0(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0))(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0))(astro@7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@22.20.4)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
|
||||
'@keystatic/core':
|
||||
specifier: ^0.6.7
|
||||
version: 0.6.9(@keystar/ui@0.10.0(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0))(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0)
|
||||
'@markdoc/markdoc':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(@types/react@19.3.0)(react@19.3.0)
|
||||
astro:
|
||||
specifier: ^7.0.4
|
||||
version: 7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
version: 7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@22.20.4)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
react:
|
||||
specifier: ^19.2.8
|
||||
version: 19.3.0
|
||||
react-dom:
|
||||
specifier: ^19.2.8
|
||||
version: 19.3.0(react@19.3.0)
|
||||
sharp:
|
||||
specifier: ^0.35.4
|
||||
version: 0.35.4(@types/node@22.20.4)
|
||||
shiki:
|
||||
specifier: ^4.4.3
|
||||
version: 4.4.3
|
||||
devDependencies:
|
||||
'@astrojs/check':
|
||||
specifier: ^0.9.10
|
||||
@@ -48,6 +57,9 @@ importers:
|
||||
'@ianvs/prettier-plugin-sort-imports':
|
||||
specifier: ^4.7.1
|
||||
version: 4.7.1(prettier@3.9.8)
|
||||
'@types/node':
|
||||
specifier: ^22.20.4
|
||||
version: 22.20.4
|
||||
'@types/react':
|
||||
specifier: ^19.2.18
|
||||
version: 19.3.0
|
||||
@@ -69,6 +81,9 @@ importers:
|
||||
eslint-plugin-unused-imports:
|
||||
specifier: ^4.4.1
|
||||
version: 4.4.1(@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.11.0)(typescript@6.0.3))(eslint@10.11.0)(typescript@6.0.3))(eslint@10.11.0)
|
||||
globals:
|
||||
specifier: ^17.12.0
|
||||
version: 17.12.0
|
||||
postcss:
|
||||
specifier: ^8.5.28
|
||||
version: 8.5.28
|
||||
@@ -105,6 +120,9 @@ importers:
|
||||
typescript-eslint:
|
||||
specifier: ^8.70.0
|
||||
version: 8.70.0(eslint@10.11.0)(typescript@6.0.3)
|
||||
vite:
|
||||
specifier: ^8.3.0
|
||||
version: 8.3.0(@types/node@22.20.4)(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -208,12 +226,6 @@ packages:
|
||||
prettier-plugin-astro:
|
||||
optional: true
|
||||
|
||||
'@astrojs/markdoc@2.0.9':
|
||||
resolution: {integrity: sha512-1w9Zg9czdfZXjBx9xv/eUy+65cOEr5AG6nsD36ZF3Eytir5Uip/0WpALLdOg6uN35wNbws+ZaibH5aAdC/6Jyg==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
peerDependencies:
|
||||
astro: ^7.0.0
|
||||
|
||||
'@astrojs/markdown-satteri@0.4.1':
|
||||
resolution: {integrity: sha512-EniHbFNa6SQHDih7JnpPos3NWXO6hdt4raBcOosfGmlfc3gP9CI/sESA3VOf1PgJZ7SFfewlZW9Livn0JugEZg==}
|
||||
|
||||
@@ -1001,18 +1013,6 @@ packages:
|
||||
react:
|
||||
optional: true
|
||||
|
||||
'@markdoc/markdoc@0.5.10':
|
||||
resolution: {integrity: sha512-5HwkG2cLG/ivffeJlxGmraJ9erYfsaYz9rjG1KCf46PLy+yhrBQWw26h69GewNUHbyKNW6FrXuDYgbl2kvIYdA==}
|
||||
engines: {node: '>=14.7.0'}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: '*'
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
|
||||
'@napi-rs/wasm-runtime@1.2.4':
|
||||
resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0}
|
||||
@@ -1259,6 +1259,9 @@ packages:
|
||||
'@types/nlcst@2.0.3':
|
||||
resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==}
|
||||
|
||||
'@types/node@22.20.4':
|
||||
resolution: {integrity: sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==}
|
||||
|
||||
'@types/react-dom@19.3.0':
|
||||
resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==}
|
||||
peerDependencies:
|
||||
@@ -1694,32 +1697,16 @@ packages:
|
||||
dom-serializer@2.0.0:
|
||||
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||
|
||||
dom-serializer@3.1.1:
|
||||
resolution: {integrity: sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
domelementtype@2.3.0:
|
||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||
|
||||
domelementtype@3.0.0:
|
||||
resolution: {integrity: sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
domhandler@5.0.3:
|
||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
domhandler@6.0.1:
|
||||
resolution: {integrity: sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
domutils@3.2.2:
|
||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||
|
||||
domutils@4.0.2:
|
||||
resolution: {integrity: sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
dset@3.1.4:
|
||||
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -1750,10 +1737,6 @@ packages:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
entities@8.1.0:
|
||||
resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
env-paths@2.2.1:
|
||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2073,10 +2056,6 @@ packages:
|
||||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
|
||||
htmlparser2@12.0.0:
|
||||
resolution: {integrity: sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
htmlparser2@9.1.0:
|
||||
resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==}
|
||||
|
||||
@@ -3154,6 +3133,9 @@ packages:
|
||||
uncrypto@0.1.3:
|
||||
resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
undici@8.10.2:
|
||||
resolution: {integrity: sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
@@ -3658,19 +3640,6 @@ snapshots:
|
||||
- '@emnapi/runtime'
|
||||
- typescript
|
||||
|
||||
'@astrojs/markdoc@2.0.9(@types/react@19.3.0)(astro@7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))(react@19.3.0)':
|
||||
dependencies:
|
||||
'@astrojs/internal-helpers': 0.11.0
|
||||
'@astrojs/prism': 4.0.2
|
||||
'@markdoc/markdoc': 0.5.10(@types/react@19.3.0)(react@19.3.0)
|
||||
astro: 7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
esbuild: 0.28.2
|
||||
github-slugger: 2.0.0
|
||||
htmlparser2: 12.0.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- react
|
||||
|
||||
'@astrojs/markdown-satteri@0.4.1':
|
||||
dependencies:
|
||||
'@astrojs/internal-helpers': 0.11.0
|
||||
@@ -3678,10 +3647,10 @@ snapshots:
|
||||
github-slugger: 2.0.0
|
||||
satteri: 0.10.5
|
||||
|
||||
'@astrojs/node@11.1.6(astro@7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))':
|
||||
'@astrojs/node@11.1.6(astro@7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@22.20.4)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))':
|
||||
dependencies:
|
||||
'@astrojs/internal-helpers': 0.11.0
|
||||
astro: 7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
astro: 7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@22.20.4)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
send: 1.2.1
|
||||
server-destroy: 1.0.1
|
||||
transitivePeerDependencies:
|
||||
@@ -3691,17 +3660,17 @@ snapshots:
|
||||
dependencies:
|
||||
prismjs: 1.30.0
|
||||
|
||||
'@astrojs/react@6.0.6(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(esbuild@0.28.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)':
|
||||
'@astrojs/react@6.0.6(@types/node@22.20.4)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(esbuild@0.28.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)':
|
||||
dependencies:
|
||||
'@astrojs/internal-helpers': 0.11.0
|
||||
'@types/react': 19.3.0
|
||||
'@types/react-dom': 19.3.0(@types/react@19.3.0)
|
||||
'@vitejs/plugin-react': 5.2.0(vite@8.3.0(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))
|
||||
'@vitejs/plugin-react': 5.2.0(vite@8.3.0(@types/node@22.20.4)(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))
|
||||
devalue: 5.9.4
|
||||
react: 19.3.0
|
||||
react-dom: 19.3.0(react@19.3.0)
|
||||
ultrahtml: 1.7.0
|
||||
vite: 8.3.0(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
vite: 8.3.0(@types/node@22.20.4)(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- '@vitejs/devtools'
|
||||
@@ -4190,8 +4159,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
optional: true
|
||||
'@img/colour@1.1.0': {}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.35.4':
|
||||
optionalDependencies:
|
||||
@@ -4350,12 +4318,12 @@ snapshots:
|
||||
react-dom: 19.3.0(react@19.3.0)
|
||||
react-stately: 3.48.0(react@19.3.0)
|
||||
|
||||
'@keystatic/astro@6.0.0(@keystatic/core@0.6.9(@keystar/ui@0.10.0(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0))(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0))(astro@7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
|
||||
'@keystatic/astro@6.0.0(@keystatic/core@0.6.9(@keystar/ui@0.10.0(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0))(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0))(astro@7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@22.20.4)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.7
|
||||
'@keystatic/core': 0.6.9(@keystar/ui@0.10.0(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0))(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0)
|
||||
'@types/react': 19.3.0
|
||||
astro: 7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
astro: 7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@22.20.4)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
react: 19.3.0
|
||||
react-dom: 19.3.0(react@19.3.0)
|
||||
|
||||
@@ -4442,13 +4410,6 @@ snapshots:
|
||||
'@types/react': 19.3.0
|
||||
react: 19.3.0
|
||||
|
||||
'@markdoc/markdoc@0.5.10(@types/react@19.3.0)(react@19.3.0)':
|
||||
optionalDependencies:
|
||||
'@types/linkify-it': 3.0.5
|
||||
'@types/markdown-it': 12.2.3
|
||||
'@types/react': 19.3.0
|
||||
react: 19.3.0
|
||||
|
||||
'@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.11.1
|
||||
@@ -4673,6 +4634,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
'@types/node@22.20.4':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/react-dom@19.3.0(@types/react@19.3.0)':
|
||||
dependencies:
|
||||
'@types/react': 19.3.0
|
||||
@@ -4803,7 +4768,7 @@ snapshots:
|
||||
'@urql/core': 5.2.0(graphql@16.14.2)
|
||||
wonka: 6.3.6
|
||||
|
||||
'@vitejs/plugin-react@5.2.0(vite@8.3.0(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))':
|
||||
'@vitejs/plugin-react@5.2.0(vite@8.3.0(@types/node@22.20.4)(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7)
|
||||
@@ -4811,7 +4776,7 @@ snapshots:
|
||||
'@rolldown/pluginutils': 1.0.0-rc.3
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.18.0
|
||||
vite: 8.3.0(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
vite: 8.3.0(@types/node@22.20.4)(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -4938,7 +4903,7 @@ snapshots:
|
||||
- '@emnapi/runtime'
|
||||
- supports-color
|
||||
|
||||
astro@7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1):
|
||||
astro@7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@22.20.4)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1):
|
||||
dependencies:
|
||||
'@astrojs/compiler-rs': 0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)
|
||||
'@astrojs/internal-helpers': 0.11.0
|
||||
@@ -4988,13 +4953,13 @@ snapshots:
|
||||
unifont: 0.7.5
|
||||
unstorage: 1.17.5(idb-keyval@6.3.0)
|
||||
verkit: 0.4.1
|
||||
vite: 8.3.0(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
vitefu: 1.1.3(vite@8.3.0(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))
|
||||
vite: 8.3.0(@types/node@22.20.4)(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
vitefu: 1.1.3(vite@8.3.0(@types/node@22.20.4)(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))
|
||||
xxhash-wasm: 1.1.0
|
||||
yargs-parser: 22.0.0
|
||||
zod: 4.6.5
|
||||
optionalDependencies:
|
||||
sharp: 0.35.4
|
||||
sharp: 0.35.4(@types/node@22.20.4)
|
||||
transitivePeerDependencies:
|
||||
- '@azure/app-configuration'
|
||||
- '@azure/cosmos'
|
||||
@@ -5212,36 +5177,18 @@ snapshots:
|
||||
domhandler: 5.0.3
|
||||
entities: 4.5.0
|
||||
|
||||
dom-serializer@3.1.1:
|
||||
dependencies:
|
||||
domelementtype: 3.0.0
|
||||
domhandler: 6.0.1
|
||||
entities: 8.1.0
|
||||
|
||||
domelementtype@2.3.0: {}
|
||||
|
||||
domelementtype@3.0.0: {}
|
||||
|
||||
domhandler@5.0.3:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
domhandler@6.0.1:
|
||||
dependencies:
|
||||
domelementtype: 3.0.0
|
||||
|
||||
domutils@3.2.2:
|
||||
dependencies:
|
||||
dom-serializer: 2.0.0
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
|
||||
domutils@4.0.2:
|
||||
dependencies:
|
||||
dom-serializer: 3.1.1
|
||||
domelementtype: 3.0.0
|
||||
domhandler: 6.0.1
|
||||
|
||||
dset@3.1.4: {}
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
@@ -5263,8 +5210,6 @@ snapshots:
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
entities@8.1.0: {}
|
||||
|
||||
env-paths@2.2.1: {}
|
||||
|
||||
error-ex@1.3.4:
|
||||
@@ -5613,13 +5558,6 @@ snapshots:
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
htmlparser2@12.0.0:
|
||||
dependencies:
|
||||
domelementtype: 3.0.0
|
||||
domhandler: 6.0.1
|
||||
domutils: 4.0.2
|
||||
entities: 8.1.0
|
||||
|
||||
htmlparser2@9.1.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
@@ -6630,7 +6568,7 @@ snapshots:
|
||||
|
||||
setprototypeof@1.2.0: {}
|
||||
|
||||
sharp@0.35.4:
|
||||
sharp@0.35.4(@types/node@22.20.4):
|
||||
dependencies:
|
||||
'@img/colour': 1.1.0
|
||||
detect-libc: 2.1.2
|
||||
@@ -6661,7 +6599,7 @@ snapshots:
|
||||
'@img/sharp-win32-arm64': 0.35.4
|
||||
'@img/sharp-win32-ia32': 0.35.4
|
||||
'@img/sharp-win32-x64': 0.35.4
|
||||
optional: true
|
||||
'@types/node': 22.20.4
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
@@ -6939,6 +6877,8 @@ snapshots:
|
||||
|
||||
uncrypto@0.1.3: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
undici@8.10.2: {}
|
||||
|
||||
unicorn-magic@0.4.0: {}
|
||||
@@ -7033,7 +6973,7 @@ snapshots:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite@8.3.0(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1):
|
||||
vite@8.3.0(@types/node@22.20.4)(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1):
|
||||
dependencies:
|
||||
lightningcss: 1.33.0
|
||||
picomatch: 4.0.7
|
||||
@@ -7041,14 +6981,15 @@ snapshots:
|
||||
rolldown: 1.2.9
|
||||
tinyglobby: 0.2.17
|
||||
optionalDependencies:
|
||||
'@types/node': 22.20.4
|
||||
esbuild: 0.28.2
|
||||
fsevents: 2.3.3
|
||||
sugarss: 5.0.1(postcss@8.5.28)
|
||||
yaml: 2.9.1
|
||||
|
||||
vitefu@1.1.3(vite@8.3.0(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)):
|
||||
vitefu@1.1.3(vite@8.3.0(@types/node@22.20.4)(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)):
|
||||
optionalDependencies:
|
||||
vite: 8.3.0(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
vite: 8.3.0(@types/node@22.20.4)(esbuild@0.28.2)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
|
||||
volar-service-css@0.0.71(@volar/language-service@2.4.28):
|
||||
dependencies:
|
||||
|
||||
4
public/robots.txt
Normal file
4
public/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://dave-dmg.de/sitemap.xml
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.4 MiB |
54
src/components/dev/DevBanner.astro
Normal file
54
src/components/dev/DevBanner.astro
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
import type { Status } from '@/lib/constants/status'
|
||||
import { forEntry, formatDiagnostics } from '@/lib/diagnostics/report'
|
||||
import type { Diagnostic } from '@/lib/diagnostics/types'
|
||||
import { getSite } from '@/lib/site'
|
||||
|
||||
type Props = {
|
||||
collection: string
|
||||
id: string
|
||||
status?: Status
|
||||
diagnostics?: Diagnostic[]
|
||||
}
|
||||
|
||||
const { collection, id, status, diagnostics = [] } = Astro.props
|
||||
|
||||
const NOTICE: Partial<Record<Status, string>> = {
|
||||
draft: 'Draft – not built in production',
|
||||
unlisted: 'Unlisted – has page, but appears in no list',
|
||||
}
|
||||
|
||||
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) && (
|
||||
<details class="dev-banner dark" open>
|
||||
<summary class="dev-banner__bar">
|
||||
Dev{label && <span class="dev-banner__crumb">{label}</span>}
|
||||
</summary>
|
||||
<div class="dev-banner__body">
|
||||
{notice && <p class="dev-banner__notice">{notice}</p>}
|
||||
{here.length > 0 && (
|
||||
<ul class="dev-banner__list" role="list">
|
||||
{here.map((d) => (
|
||||
<li class:list={['dev-banner__item', `dev-banner__item--${d.level}`]}>
|
||||
{d.message}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{elsewhere.length > 0 && (
|
||||
<details class="dev-banner__elsewhere">
|
||||
<summary class="dev-banner__toggle">{elsewhere.length} elsewhere</summary>
|
||||
<pre class="dev-banner__log">{formatDiagnostics(elsewhere)}</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
31
src/components/home/ActivityStrip.astro
Normal file
31
src/components/home/ActivityStrip.astro
Normal file
@@ -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 && (
|
||||
<p class="activity-strip">
|
||||
<span class="activity-strip__summary">{summary}</span>
|
||||
<span class="activity-strip__cells" aria-hidden="true">
|
||||
{activity.map((cell, i) => (
|
||||
<span
|
||||
class:list={[
|
||||
'activity-strip__cell',
|
||||
cell.touched && 'activity-strip__cell--touched',
|
||||
cell.touched && i === total - 1 && 'activity-strip__cell--latest',
|
||||
]}
|
||||
title={`v${cell.release.version}`}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
47
src/components/home/Changelog.astro
Normal file
47
src/components/home/Changelog.astro
Normal file
@@ -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')
|
||||
---
|
||||
|
||||
<ol class="changelog" role="list">
|
||||
{releases.map((release) => {
|
||||
const more = release.changes.length - LIMIT
|
||||
return (
|
||||
<li class="changelog__release">
|
||||
<div class="changelog__head">
|
||||
<h3 class="changelog__version">v{release.version}</h3>
|
||||
<time class="changelog__date" datetime={release.date.toISOString()}>
|
||||
{toMilitaryDTG(release.date)}
|
||||
</time>
|
||||
</div>
|
||||
<ul class="changelog__changes" role="list">
|
||||
{release.changes.slice(0, LIMIT).map((change) => (
|
||||
<li class="changelog__change">
|
||||
<span class="changelog__kind">{labelOf(change.kind)}</span>
|
||||
<span class="changelog__work">{workOf(change)}</span>
|
||||
<p class="changelog__text">
|
||||
{change.article ? <a href={change.article.path}>{change.text}</a> : change.text}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{more > 0 && <p class="changelog__more">+{more} more</p>}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
67
src/components/home/HomeHero.astro
Normal file
67
src/components/home/HomeHero.astro
Normal file
@@ -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
|
||||
---
|
||||
|
||||
<header class="home-hero dark">
|
||||
<div class="home-hero__inner">
|
||||
<p class="home-hero__eyebrow">Index</p>
|
||||
<h1 class="home-hero__title">{SITE.title}</h1>
|
||||
<dl class="home-hero__stats">
|
||||
<div class="home-hero__stat home-hero__stat--about">
|
||||
<dt class="home-hero__label">
|
||||
<span class="home-hero__index" aria-hidden="true">
|
||||
01
|
||||
</span>
|
||||
About
|
||||
</dt>
|
||||
<dd class="home-hero__tagline">{SITE.tagline}</dd>
|
||||
</div>
|
||||
<div class="home-hero__stat">
|
||||
<dt class="home-hero__label">
|
||||
<span class="home-hero__index" aria-hidden="true">
|
||||
02
|
||||
</span>
|
||||
Works
|
||||
</dt>
|
||||
<dd class="home-hero__value">{works}</dd>
|
||||
<dd class="home-hero__note">Top-level</dd>
|
||||
</div>
|
||||
<div class="home-hero__stat">
|
||||
<dt class="home-hero__label">
|
||||
<span class="home-hero__index" aria-hidden="true">
|
||||
03
|
||||
</span>
|
||||
Entries
|
||||
</dt>
|
||||
<dd class="home-hero__value">{entries}</dd>
|
||||
<dd class="home-hero__note">Articles</dd>
|
||||
</div>
|
||||
<div class="home-hero__stat">
|
||||
<dt class="home-hero__label">
|
||||
<span class="home-hero__index" aria-hidden="true">
|
||||
04
|
||||
</span>
|
||||
Latest release
|
||||
</dt>
|
||||
<dd class="home-hero__value">{release ? `v${release.version}` : '—'}</dd>
|
||||
<dd class="home-hero__note">
|
||||
{release ? (
|
||||
<time datetime={release.date.toISOString()}>{toMilitaryDTG(release.date)}</time>
|
||||
) : (
|
||||
'None yet'
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</header>
|
||||
21
src/components/home/HomeSection.astro
Normal file
21
src/components/home/HomeSection.astro
Normal file
@@ -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}`
|
||||
---
|
||||
|
||||
<section class="home-section" aria-labelledby={id}>
|
||||
<h2 id={id} class="home-section__heading">
|
||||
<span class="home-section__number" aria-hidden="true">
|
||||
§ {toRoman(index)}
|
||||
</span>
|
||||
{title}
|
||||
</h2>
|
||||
<slot />
|
||||
</section>
|
||||
44
src/components/home/WorkList.astro
Normal file
44
src/components/home/WorkList.astro
Normal file
@@ -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
|
||||
---
|
||||
|
||||
<ol class="work-list" role="list">
|
||||
{works.map((work, i) => (
|
||||
<li class="work-list__item">
|
||||
<span class="work-list__numeral" aria-hidden="true">
|
||||
{toRoman(i + 1)}
|
||||
</span>
|
||||
<div class="work-list__main">
|
||||
<h3 class="work-list__title">
|
||||
<a href={work.path}>{work.title}</a>
|
||||
</h3>
|
||||
{work.summary.excerpt && <p class="work-list__excerpt">{work.summary.excerpt}</p>}
|
||||
</div>
|
||||
<div class="work-list__meta">
|
||||
{work.children.length > 0 ? (
|
||||
<ul class="work-list__sections" role="list">
|
||||
{work.children.map((section) => (
|
||||
<li>
|
||||
<a href={section.path}>{section.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p class="work-list__sections">—</p>
|
||||
)}
|
||||
<ActivityStrip activity={activityOf(work, releases)} />
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
47
src/components/markdoc/Node.astro
Normal file
47
src/components/markdoc/Node.astro
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
import type { AstroComponentFactory } from 'astro/runtime/server/index.js'
|
||||
|
||||
import Markdoc, { type RenderableTreeNode } from '@markdoc/markdoc'
|
||||
|
||||
import ContentImage from '@/components/media/ContentImage.astro'
|
||||
|
||||
import Figure from '../media/Figure.astro'
|
||||
import Callout from './blocks/general/Callout.astro'
|
||||
import Code from './blocks/general/Code.astro'
|
||||
import Definition from './blocks/general/Definitions/Definition.astro'
|
||||
import DefinitionList from './blocks/general/Definitions/DefinitionList.astro'
|
||||
import Lead from './blocks/general/Lead.astro'
|
||||
import Quote from './blocks/general/Quote.astro'
|
||||
import Reveal from './blocks/general/Reveal.astro'
|
||||
import TableWrapper from './blocks/general/TableWrapper.astro'
|
||||
|
||||
type Props = { node: RenderableTreeNode | RenderableTreeNode[] }
|
||||
|
||||
const COMPONENTS: Record<string, AstroComponentFactory> = {
|
||||
Callout,
|
||||
Reveal,
|
||||
Code,
|
||||
Figure,
|
||||
TableWrapper,
|
||||
Quote,
|
||||
Definition,
|
||||
DefinitionList,
|
||||
Lead,
|
||||
Image: ContentImage,
|
||||
}
|
||||
const { node } = Astro.props
|
||||
const isTag = Markdoc.Tag.isTag(node)
|
||||
const Element = isTag ? (COMPONENTS[node.name] ?? node.name) : null
|
||||
---
|
||||
|
||||
{Array.isArray(node) ? (
|
||||
node.map((child) => <Astro.self node={child} />)
|
||||
) : isTag && Element ? (
|
||||
<Element {...node.attributes}>
|
||||
{node.children.map((child) => (
|
||||
<Astro.self node={child} />
|
||||
))}
|
||||
</Element>
|
||||
) : typeof node === 'string' || typeof node === 'number' ? (
|
||||
node
|
||||
) : null}
|
||||
21
src/components/markdoc/blocks/general/Callout.astro
Normal file
21
src/components/markdoc/blocks/general/Callout.astro
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
import { NOTE_TYPES, type CalloutType } from '@/lib/constants/blocks/notes'
|
||||
|
||||
type Props = {
|
||||
type?: CalloutType
|
||||
title?: string
|
||||
}
|
||||
|
||||
const { type = 'default', title } = Astro.props
|
||||
|
||||
const kind = NOTE_TYPES.find((option) => option.value === type)?.label ?? type
|
||||
---
|
||||
|
||||
<aside class:list={['callout', `callout--${type}`]}>
|
||||
<div class:list={['callout__kind', type !== 'optional' && 'dark']}>{kind}</div>
|
||||
|
||||
<div class="callout__body">
|
||||
{title && <p class="callout__title">{title}</p>}
|
||||
<slot />
|
||||
</div>
|
||||
</aside>
|
||||
14
src/components/markdoc/blocks/general/Code.astro
Normal file
14
src/components/markdoc/blocks/general/Code.astro
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
type Props = {
|
||||
code: string
|
||||
lang?: string
|
||||
html?: string
|
||||
}
|
||||
|
||||
const { code, lang, html } = Astro.props
|
||||
---
|
||||
|
||||
<div class="code" data-language={lang}>
|
||||
{lang && <span class="code__lang">{lang}</span>}
|
||||
{html ? <Fragment set:html={html} /> : <pre tabindex="0"><code>{code}</code></pre>}
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
import TermEntry, { type SeeLink } from '@/components/terms/TermEntry.astro'
|
||||
import type { TermKind } from '@/lib/constants/terms'
|
||||
import { slugify } from '@/lib/content/slug'
|
||||
|
||||
type Props = {
|
||||
term: string
|
||||
kind?: TermKind
|
||||
see?: string
|
||||
}
|
||||
|
||||
const { term, kind = 'entry', see = '' } = Astro.props
|
||||
|
||||
const links: SeeLink[] = see
|
||||
.split(',')
|
||||
.map((name) => name.trim())
|
||||
.filter(Boolean)
|
||||
.map((name) => ({ title: name, href: `#${slugify(name)}` }))
|
||||
---
|
||||
|
||||
<TermEntry id={slugify(term)} title={term} kind={kind} see={links} ink>
|
||||
<slot />
|
||||
</TermEntry>
|
||||
@@ -0,0 +1,3 @@
|
||||
<dl class="terms terms--cross">
|
||||
<slot />
|
||||
</dl>
|
||||
25
src/components/markdoc/blocks/general/FootnoteList.astro
Normal file
25
src/components/markdoc/blocks/general/FootnoteList.astro
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
import Node from '@/components/markdoc/Node.astro'
|
||||
import type { Footnote } from '@/lib/markdoc/types'
|
||||
|
||||
type Props = { footnotes: Footnote[] }
|
||||
const { footnotes } = Astro.props
|
||||
---
|
||||
|
||||
{footnotes.length > 0 && (
|
||||
<section class="footnotes">
|
||||
<h2 class="footnotes__title">Notes</h2>
|
||||
<ol>
|
||||
{footnotes.map((footnote) => (
|
||||
<li id={`fn-${footnote.n}`}>
|
||||
<p class="footnotes__note">
|
||||
<Node node={footnote.children} />{' '}
|
||||
<a href={`#fnref-${footnote.n}`} class="footnotes__back" aria-label="Back to text">
|
||||
↩
|
||||
</a>
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
)}
|
||||
3
src/components/markdoc/blocks/general/Lead.astro
Normal file
3
src/components/markdoc/blocks/general/Lead.astro
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="lead">
|
||||
<slot />
|
||||
</div>
|
||||
22
src/components/markdoc/blocks/general/Quote.astro
Normal file
22
src/components/markdoc/blocks/general/Quote.astro
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
type Props = {
|
||||
cite?: string
|
||||
source?: string
|
||||
href?: string
|
||||
}
|
||||
|
||||
const { cite, source, href } = Astro.props
|
||||
const label = source || href
|
||||
---
|
||||
|
||||
<figure class="quote">
|
||||
<blockquote cite={href}>
|
||||
<slot />
|
||||
</blockquote>
|
||||
{(cite || label) && (
|
||||
<figcaption class="quote__citation">
|
||||
{cite && <span class="quote__who">{cite}</span>}
|
||||
{label && <cite class="quote__source">{href ? <a href={href}>{label}</a> : label}</cite>}
|
||||
</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
20
src/components/markdoc/blocks/general/Reveal.astro
Normal file
20
src/components/markdoc/blocks/general/Reveal.astro
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
type Props = { summary?: string }
|
||||
|
||||
const { summary = 'Spoiler' } = Astro.props
|
||||
---
|
||||
|
||||
<details class="reveal">
|
||||
<summary class="reveal__summary">
|
||||
<span class="reveal__kind dark">Spoiler</span>
|
||||
|
||||
<span class="reveal__head">
|
||||
<span class="reveal__title">{summary}</span>
|
||||
<span class="reveal__action">Reveal</span>
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
<div class="reveal__body">
|
||||
<slot />
|
||||
</div>
|
||||
</details>
|
||||
15
src/components/markdoc/blocks/general/TableWrapper.astro
Normal file
15
src/components/markdoc/blocks/general/TableWrapper.astro
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
import type { TableVariant } from '@/lib/constants/blocks/table'
|
||||
|
||||
type Props = {
|
||||
variant?: TableVariant
|
||||
caption?: string
|
||||
}
|
||||
|
||||
const { variant = 'default', caption } = Astro.props
|
||||
---
|
||||
|
||||
<figure class:list={['table-wrapper', `table-wrapper--${variant}`]}>
|
||||
<slot />
|
||||
{caption && <figcaption>{caption}</figcaption>}
|
||||
</figure>
|
||||
32
src/components/media/ContentImage.astro
Normal file
32
src/components/media/ContentImage.astro
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
import { Image } from 'astro:assets'
|
||||
|
||||
import { resolveImage } from '@/lib/images/resolve'
|
||||
|
||||
type Props = {
|
||||
src: string
|
||||
alt: string
|
||||
eager?: boolean
|
||||
widths?: number[]
|
||||
sizes?: string
|
||||
fit?: boolean
|
||||
}
|
||||
|
||||
const { src, alt, eager, widths, sizes, fit } = Astro.props
|
||||
const image = resolveImage(src)
|
||||
if (!image && import.meta.env.PROD) throw new Error(`Image not found: ${src}`)
|
||||
---
|
||||
|
||||
{image ? (
|
||||
<Image
|
||||
src={image}
|
||||
alt={alt}
|
||||
widths={widths}
|
||||
sizes={sizes}
|
||||
class:list={[fit && 'fit']}
|
||||
loading={eager ? 'eager' : 'lazy'}
|
||||
fetchpriority={eager ? 'high' : undefined}
|
||||
/>
|
||||
) : (
|
||||
<span class="missing-image">Missing image: {src}</span>
|
||||
)}
|
||||
47
src/components/media/Figure.astro
Normal file
47
src/components/media/Figure.astro
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
import type { AspectRatio, FigureSize, ImageEffect } from '@/lib/constants/figure'
|
||||
|
||||
import ContentImage from './ContentImage.astro'
|
||||
|
||||
type Props = {
|
||||
image: string
|
||||
alt: string
|
||||
caption?: string
|
||||
credit?: string
|
||||
size?: FigureSize
|
||||
effect?: ImageEffect
|
||||
ratio?: AspectRatio
|
||||
}
|
||||
|
||||
const LAYOUT: Record<FigureSize, { widths: number[]; sizes: string }> = {
|
||||
inline: { widths: [400, 800, 1200], sizes: '(min-width: 80rem) 37rem, 100vw' },
|
||||
wide: { widths: [600, 1200, 1800], sizes: '(min-width: 80rem) 59rem, 100vw' },
|
||||
}
|
||||
|
||||
const {
|
||||
image,
|
||||
alt,
|
||||
caption,
|
||||
credit,
|
||||
size = 'inline',
|
||||
effect = 'none',
|
||||
ratio = 'original',
|
||||
} = Astro.props
|
||||
---
|
||||
|
||||
<figure
|
||||
class:list={[
|
||||
'figure',
|
||||
`figure--${size}`,
|
||||
effect !== 'none' && `effect--${effect}`,
|
||||
ratio !== 'original' && `ratio--${ratio}`,
|
||||
]}
|
||||
>
|
||||
<ContentImage src={image} alt={alt} {...LAYOUT[size]} />
|
||||
{(caption || credit) && (
|
||||
<figcaption class:list={['figure__caption', size !== 'inline' && 'dark']}>
|
||||
{caption && <span class="figure__text">{caption}</span>}
|
||||
{credit && <span class="figure__credit">{credit}</span>}
|
||||
</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
26
src/components/post/ArticleCard/ArticleCardGrid.astro
Normal file
26
src/components/post/ArticleCard/ArticleCardGrid.astro
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import type { Article } from '@/lib/site/types'
|
||||
|
||||
import ArticleCardItem from './ArticleCardItem.astro'
|
||||
|
||||
type Props = {
|
||||
articles: Article[]
|
||||
title?: string
|
||||
}
|
||||
|
||||
const { articles, title } = Astro.props
|
||||
const level = title ? 3 : 2
|
||||
---
|
||||
|
||||
{articles.length > 0 && (
|
||||
<section class="article-card-grid">
|
||||
{title && <h2 class="article-card-grid__title">{title}</h2>}
|
||||
<ul class="article-card-grid__list" role="list">
|
||||
{articles.map((article) => (
|
||||
<li class="article-card-grid__item">
|
||||
<ArticleCardItem article={article} level={level} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
41
src/components/post/ArticleCard/ArticleCardItem.astro
Normal file
41
src/components/post/ArticleCard/ArticleCardItem.astro
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
import ContentImage from '@/components/media/ContentImage.astro'
|
||||
import { toMilitaryDTG } from '@/lib/content/dates'
|
||||
import type { Article } from '@/lib/site/types'
|
||||
|
||||
type Props = {
|
||||
article: Article
|
||||
level?: 2 | 3
|
||||
}
|
||||
|
||||
const { article, level = 2 } = Astro.props
|
||||
const { cover, excerpt } = article.summary
|
||||
const date = article.publishedAt
|
||||
const Heading = level === 2 ? 'h2' : 'h3'
|
||||
---
|
||||
|
||||
<article class:list={['article-card', cover.effect !== 'none' && `effect--${cover.effect}`]}>
|
||||
{cover.image && (
|
||||
<div class="article-card__cover">
|
||||
<ContentImage
|
||||
src={cover.image}
|
||||
alt=""
|
||||
widths={[400, 800, 1200]}
|
||||
sizes="(min-width: 80rem) 24rem, (min-width: 48rem) 50vw, 100vw"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div class="article-card__body">
|
||||
<Heading class="article-card__title">
|
||||
<a class="article-card__link" href={article.path}>
|
||||
{article.title}
|
||||
</a>
|
||||
</Heading>
|
||||
{excerpt && <p class="article-card__excerpt">{excerpt}</p>}
|
||||
{date && (
|
||||
<time class="article-card__date" datetime={date.toISOString()}>
|
||||
{toMilitaryDTG(date)}
|
||||
</time>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
22
src/components/post/Footer/ArticleFooter.astro
Normal file
22
src/components/post/Footer/ArticleFooter.astro
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
import type { Sequence } from '@/lib/site/navigation'
|
||||
import type { Article } from '@/lib/site/types'
|
||||
|
||||
import RevisionCard from './RevisionCard.astro'
|
||||
import SequenceNav from './SequenceNav.astro'
|
||||
|
||||
type Props = {
|
||||
article: Article
|
||||
sequence: Sequence | null
|
||||
}
|
||||
|
||||
const { article, sequence } = Astro.props
|
||||
const { title, changes } = article
|
||||
---
|
||||
|
||||
{(changes.length > 0 || sequence) && (
|
||||
<footer class="article-footer">
|
||||
{changes.length > 0 && <RevisionCard title={title} changes={changes} />}
|
||||
{sequence && <SequenceNav sequence={sequence} />}
|
||||
</footer>
|
||||
)}
|
||||
38
src/components/post/Footer/RevisionCard.astro
Normal file
38
src/components/post/Footer/RevisionCard.astro
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
import { CHANGE_TYPES } from '@/lib/constants/changelog'
|
||||
import { toMilitaryDTG } from '@/lib/content/dates'
|
||||
import type { Change } from '@/lib/site/types'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
changes: Change[]
|
||||
}
|
||||
|
||||
const LIMIT = 5
|
||||
|
||||
const { title, changes } = Astro.props
|
||||
|
||||
const labelOf = (kind: Change['kind']) =>
|
||||
CHANGE_TYPES.find((option) => option.value === kind)?.label ?? kind
|
||||
---
|
||||
|
||||
<section class="revision-card" aria-labelledby="revisions-heading">
|
||||
<div class="revision-card__head">
|
||||
<h2 id="revisions-heading" class="revision-card__heading">
|
||||
Revisions
|
||||
</h2>
|
||||
<p class="revision-card__subject">{title}</p>
|
||||
</div>
|
||||
<ol class="revision-card__list">
|
||||
{changes.slice(0, LIMIT).map(({ kind, text, release }) => (
|
||||
<li class="revision-card__row">
|
||||
<span class="revision-card__version">v{release.version}</span>
|
||||
<time class="revision-card__date" datetime={release.date.toISOString()}>
|
||||
{toMilitaryDTG(release.date)}
|
||||
</time>
|
||||
<span class="revision-card__kind">{labelOf(kind)}</span>
|
||||
<p class="revision-card__text">{text}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
66
src/components/post/Footer/SequenceNav.astro
Normal file
66
src/components/post/Footer/SequenceNav.astro
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
import { toRoman } from '@/lib/content/numerals'
|
||||
import type { Sequence } from '@/lib/site/navigation'
|
||||
|
||||
type Props = {
|
||||
sequence: Sequence
|
||||
}
|
||||
|
||||
const { parent, siblings, position, previous, next } = Astro.props.sequence
|
||||
---
|
||||
|
||||
<nav class="sequence-nav" aria-label={`${parent.title} sequence`}>
|
||||
<div class="sequence-nav__step sequence-nav__step--previous">
|
||||
{previous && (
|
||||
<a class="sequence-nav__link" href={previous.path} rel="prev">
|
||||
<span class="sequence-nav__label">
|
||||
Previous
|
||||
<span class="sequence-nav__numeral" aria-hidden="true">
|
||||
{toRoman(position)}
|
||||
</span>
|
||||
</span>
|
||||
<span class="sequence-nav__title">{previous.title}</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="sequence-nav__overview">
|
||||
<p class="sequence-nav__label">
|
||||
<a class="sequence-nav__parent" href={parent.path}>
|
||||
{parent.title}
|
||||
</a>
|
||||
<span class="sequence-nav__count">
|
||||
{toRoman(position + 1)} of {toRoman(siblings.length)}
|
||||
</span>
|
||||
</p>
|
||||
<ol class="sequence-nav__squares">
|
||||
{siblings.map((sibling, i) => (
|
||||
<li>
|
||||
<a
|
||||
class="sequence-nav__square"
|
||||
href={sibling.path}
|
||||
aria-label={sibling.title}
|
||||
title={sibling.title}
|
||||
aria-current={i === position ? 'page' : undefined}
|
||||
>
|
||||
{toRoman(i + 1)}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="sequence-nav__step sequence-nav__step--next">
|
||||
{next && (
|
||||
<a class="sequence-nav__link" href={next.path} rel="next">
|
||||
<span class="sequence-nav__label">
|
||||
<span class="sequence-nav__numeral" aria-hidden="true">
|
||||
{toRoman(position + 2)}
|
||||
</span>
|
||||
Next
|
||||
</span>
|
||||
<span class="sequence-nav__title">{next.title}</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
32
src/components/post/Header/ArticleHeader.astro
Normal file
32
src/components/post/Header/ArticleHeader.astro
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
import { toMilitaryDTG } from '@/lib/content/dates'
|
||||
import { trailOf } from '@/lib/site/navigation'
|
||||
import type { Article } from '@/lib/site/types'
|
||||
|
||||
import Cover from './Cover.astro'
|
||||
import Meta from './Meta.astro'
|
||||
import Overline from './Overline.astro'
|
||||
import Title from './Title.astro'
|
||||
|
||||
type Props = {
|
||||
article: Article
|
||||
}
|
||||
|
||||
const { article } = Astro.props
|
||||
|
||||
const crumbs = trailOf(article)
|
||||
const published = article.publishedAt
|
||||
---
|
||||
|
||||
<header class="article-header">
|
||||
<Overline
|
||||
crumbs={crumbs}
|
||||
stamp={published ? toMilitaryDTG(published) : 'No publication date set'}
|
||||
datetime={published?.toISOString()}
|
||||
/>
|
||||
<div class="article-header__panel">
|
||||
<Title title={article.title} subtitle={article.subtitle} />
|
||||
<Meta article={article} />
|
||||
<Cover cover={article.summary.cover} />
|
||||
</div>
|
||||
</header>
|
||||
34
src/components/post/Header/Cover.astro
Normal file
34
src/components/post/Header/Cover.astro
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
import ContentImage from '@/components/media/ContentImage.astro'
|
||||
import type { Article } from '@/lib/site/types'
|
||||
|
||||
type Props = {
|
||||
cover: Article['summary']['cover']
|
||||
}
|
||||
|
||||
const { image, alt, caption, credit, effect, ratio } = Astro.props.cover
|
||||
---
|
||||
|
||||
{image && (
|
||||
<figure
|
||||
class:list={[
|
||||
'article-header__cover',
|
||||
effect !== 'none' && `effect--${effect}`,
|
||||
ratio !== 'original' && `ratio--${ratio}`,
|
||||
]}
|
||||
>
|
||||
<ContentImage
|
||||
src={image}
|
||||
alt={alt}
|
||||
widths={[800, 1600, 2400]}
|
||||
sizes="(min-width: 80rem) 74rem, 100vw"
|
||||
eager
|
||||
/>
|
||||
{(caption || credit) && (
|
||||
<figcaption class="article-header__caption dark">
|
||||
{caption && <span>{caption}</span>}
|
||||
{credit && <span class="article-header__credit">{credit}</span>}
|
||||
</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
)}
|
||||
75
src/components/post/Header/Meta.astro
Normal file
75
src/components/post/Header/Meta.astro
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
import StageBadge from '@/components/post/Header/StageBadge.astro'
|
||||
import { toMilitaryDTG } from '@/lib/content/dates'
|
||||
import { getSite } from '@/lib/site'
|
||||
import type { Article } from '@/lib/site/types'
|
||||
|
||||
type Props = {
|
||||
article: Article
|
||||
}
|
||||
|
||||
const { article } = Astro.props
|
||||
const site = await getSite()
|
||||
const tags = article.tags.map((id) => site.tags.get(id)).filter((tag) => tag !== undefined)
|
||||
const latest = article.changes[0]?.release.date
|
||||
const updated = latest && article.publishedAt && latest > article.publishedAt ? latest : null
|
||||
const date = updated ?? article.publishedAt
|
||||
const dateLabel = updated ? 'Updated' : 'Published'
|
||||
---
|
||||
|
||||
<dl class="article-header__meta">
|
||||
<div class="article-header__cell">
|
||||
<dt class="article-header__label">
|
||||
<span class="article-header__index" aria-hidden="true">
|
||||
02
|
||||
</span>
|
||||
State
|
||||
</dt>
|
||||
<dd>
|
||||
<StageBadge stage={article.stage} />
|
||||
</dd>
|
||||
</div>
|
||||
<div class="article-header__cell">
|
||||
<dt class="article-header__label">
|
||||
<span class="article-header__index" aria-hidden="true">
|
||||
03
|
||||
</span>
|
||||
{dateLabel}
|
||||
</dt>
|
||||
<dd class="article-header__value">
|
||||
{date ? <time datetime={date.toISOString()}>{toMilitaryDTG(date)}</time> : 'Undated'}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="article-header__cell">
|
||||
<dt class="article-header__label">
|
||||
<span class="article-header__index" aria-hidden="true">
|
||||
04
|
||||
</span>
|
||||
Author
|
||||
</dt>
|
||||
<dd class="article-header__value">Dave Damage</dd>
|
||||
</div>
|
||||
<div class="article-header__cell">
|
||||
<dt class="article-header__label">
|
||||
<span class="article-header__index" aria-hidden="true">
|
||||
05
|
||||
</span>
|
||||
Tags
|
||||
</dt>
|
||||
<dd class="article-header__value">
|
||||
{tags.length > 0 ? (
|
||||
<ul class="article-header__tags">
|
||||
{tags.map((tag) => (
|
||||
<li>
|
||||
<a class="article-header__tag" href={tag.path} rel="tag">
|
||||
{tag.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
'None'
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
95
src/components/post/Header/Overline.astro
Normal file
95
src/components/post/Header/Overline.astro
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
export type Crumb = {
|
||||
title: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
crumbs: Crumb[]
|
||||
stamp?: string
|
||||
datetime?: string
|
||||
}
|
||||
|
||||
const { crumbs, stamp, datetime } = Astro.props
|
||||
---
|
||||
|
||||
<div class="overline dark">
|
||||
<div class="wrapper">
|
||||
{crumbs.length > 1 && (
|
||||
<nav aria-label="Breadcrumb" class="breadcrumbs">
|
||||
<ol>
|
||||
{crumbs.map((crumb, index) => (
|
||||
<li>
|
||||
{crumb.path && index < crumbs.length - 1 ? (
|
||||
<a href={crumb.path}>{crumb.title}</a>
|
||||
) : (
|
||||
<span aria-current="page">{crumb.title}</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
)}
|
||||
{datetime ? (
|
||||
<time class="stamp" datetime={datetime}>
|
||||
<slot name="stamp">{stamp}</slot>
|
||||
</time>
|
||||
) : (
|
||||
<span class="stamp">
|
||||
<slot name="stamp">{stamp}</slot>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.overline {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--type--1);
|
||||
line-height: 1;
|
||||
color: var(--color-text-secondary);
|
||||
letter-spacing: var(--tracking-0);
|
||||
|
||||
background-color: var(--color-surface-elevated-2);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@mixin layout-wrapper;
|
||||
@mixin py var(--size-4);
|
||||
|
||||
display: flex;
|
||||
gap: var(--size-4);
|
||||
align-items: baseline;
|
||||
justify-content: space-between; /* breadcrumbs left, stamp right */
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
& ol {
|
||||
display: flex;
|
||||
gap: var(--size-2);
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
list-style: none;
|
||||
|
||||
& li {
|
||||
&:not(:last-child) {
|
||||
&::after {
|
||||
content: '⟫' / '';
|
||||
margin-inline-start: var(--size-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stamp {
|
||||
font-weight: 700;
|
||||
|
||||
&::before {
|
||||
content: '⟫' / '';
|
||||
margin-inline-end: var(--size-2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
28
src/components/post/Header/SimpleHeader.astro
Normal file
28
src/components/post/Header/SimpleHeader.astro
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
type Props = {
|
||||
title: string
|
||||
eyebrow?: string
|
||||
standfirst?: string
|
||||
count?: number
|
||||
}
|
||||
|
||||
const { title, eyebrow, standfirst, count } = Astro.props
|
||||
---
|
||||
|
||||
<header class="simple-header dark">
|
||||
<div class="simple-header__inner">
|
||||
<div class="simple-header__text">
|
||||
{eyebrow && <p class="simple-header__eyebrow">{eyebrow}</p>}
|
||||
<h1 class="simple-header__title">{title}</h1>
|
||||
{standfirst && <p class="simple-header__standfirst">{standfirst}</p>}
|
||||
</div>
|
||||
{count !== undefined && (
|
||||
<p class="simple-header__count">
|
||||
<span aria-hidden="true">{String(count).padStart(2, '0')}</span>
|
||||
<span class="simple-header__hidden">
|
||||
{count} {count === 1 ? 'entry' : 'entries'}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
13
src/components/post/Header/StageBadge.astro
Normal file
13
src/components/post/Header/StageBadge.astro
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
import { STAGE_OPTIONS, type Stage } from '@/lib/constants/stage'
|
||||
|
||||
type Props = {
|
||||
stage: Stage
|
||||
}
|
||||
|
||||
const { stage } = Astro.props
|
||||
|
||||
const label = STAGE_OPTIONS.find((option) => option.value === stage)?.label ?? stage
|
||||
---
|
||||
|
||||
<span class:list={['stage-badge', `stage-badge--${stage}`]}>{label}</span>
|
||||
17
src/components/post/Header/Title.astro
Normal file
17
src/components/post/Header/Title.astro
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
type Props = {
|
||||
title: string
|
||||
subtitle?: string
|
||||
}
|
||||
|
||||
const { title, subtitle } = Astro.props
|
||||
---
|
||||
|
||||
<div class="article-header__designation">
|
||||
<p class="article-header__label" aria-hidden="true">
|
||||
<span class="article-header__index">01</span>
|
||||
Designation
|
||||
</p>
|
||||
<h1 class="article-header__title">{title}</h1>
|
||||
{subtitle && <p class="article-header__subtitle">{subtitle}</p>}
|
||||
</div>
|
||||
99
src/components/post/ToC.astro
Normal file
99
src/components/post/ToC.astro
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
import type { Heading } from '@/lib/markdoc/types'
|
||||
|
||||
type Props = {
|
||||
headings: Heading[]
|
||||
min?: number
|
||||
level?: number
|
||||
}
|
||||
|
||||
const { headings, min = 3, level = 3 } = Astro.props
|
||||
const entries = headings.filter((heading) => heading.level <= level)
|
||||
---
|
||||
|
||||
{entries.length >= min && (
|
||||
<nav class="toc" aria-labelledby="toc-title">
|
||||
<h2 class="title" id="toc-title">
|
||||
Contents
|
||||
</h2>
|
||||
<ol>
|
||||
{entries.map((heading) => (
|
||||
<li class:list={['entry', `entry--${heading.level}`]}>
|
||||
<a href={`#${heading.id}`}>
|
||||
<span class="n" aria-hidden="true" />
|
||||
<span class="label">{heading.text}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
)}
|
||||
|
||||
<style>
|
||||
.toc {
|
||||
@mixin border-t var(--border-card), solid, var(--color-border-strong);
|
||||
counter-reset: toc;
|
||||
position: sticky;
|
||||
top: calc(var(--layout-header-height) + var(--size-10));
|
||||
}
|
||||
|
||||
.title {
|
||||
@mixin typo_label;
|
||||
@mixin my var(--size-0);
|
||||
@mixin py var(--size-4);
|
||||
@mixin border-b var(--border-hairline), solid, var(--color-border-normal);
|
||||
font-size: var(--type--2);
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
a {
|
||||
@mixin py var(--size-3);
|
||||
@mixin border-b var(--border-hairline), solid, var(--color-border-subtle);
|
||||
display: grid;
|
||||
grid-template-columns: 4ch minmax(0, 1fr);
|
||||
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--type--1);
|
||||
line-height: var(--leading-2);
|
||||
color: var(--color-text-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background-color: var(--color-surface-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.n {
|
||||
color: var(--color-text-quiet);
|
||||
}
|
||||
|
||||
.entry--2 {
|
||||
counter-increment: toc;
|
||||
|
||||
& .n {
|
||||
&::before {
|
||||
content: counter(toc, upper-roman);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.entry--3 {
|
||||
& a {
|
||||
@mixin ps var(--size-8);
|
||||
}
|
||||
|
||||
& .label {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
& .n {
|
||||
&::before {
|
||||
content: '⟫';
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
140
src/components/site/Finder.astro
Normal file
140
src/components/site/Finder.astro
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
import { COLLECTION_LABELS } from '@/lib/constants/collections'
|
||||
import { toRoman } from '@/lib/content/numerals'
|
||||
import { getSite } from '@/lib/site'
|
||||
import { contextOf } from '@/lib/site/navigation'
|
||||
import type { Article } from '@/lib/site/types'
|
||||
|
||||
type Props = {
|
||||
article?: Article
|
||||
}
|
||||
|
||||
const { article } = Astro.props
|
||||
const site = await getSite()
|
||||
const groups = contextOf(article ?? null, site.roots)
|
||||
const count = groups.reduce((sum, group) => sum + group.entries.length, 0)
|
||||
---
|
||||
|
||||
<site-finder class="finder">
|
||||
<button
|
||||
class="finder__trigger light"
|
||||
type="button"
|
||||
commandfor="finder"
|
||||
command="show-modal"
|
||||
aria-haspopup="dialog"
|
||||
>
|
||||
<span class="finder__prompt" aria-hidden="true">
|
||||
❯
|
||||
</span>
|
||||
<span class="finder__placeholder">Navigate…</span>
|
||||
<kbd class="finder__key" data-finder-shortcut>
|
||||
⌘K
|
||||
</kbd>
|
||||
</button>
|
||||
|
||||
<dialog id="finder" class="finder__dialog" aria-labelledby="finder-title" closedby="any">
|
||||
<div class="finder__bar">
|
||||
<h2 id="finder-title" class="finder__title">
|
||||
Register<span class="finder__crumb">Query</span>
|
||||
</h2>
|
||||
<button
|
||||
class="finder__close"
|
||||
type="button"
|
||||
commandfor="finder"
|
||||
command="close"
|
||||
aria-label="Close"
|
||||
>
|
||||
Esc
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="finder__field">
|
||||
<span class="finder__prompt" aria-hidden="true">
|
||||
❯
|
||||
</span>
|
||||
<input
|
||||
class="finder__input"
|
||||
type="text"
|
||||
role="combobox"
|
||||
aria-label="Search articles"
|
||||
aria-controls="finder-results"
|
||||
aria-expanded="true"
|
||||
aria-autocomplete="list"
|
||||
placeholder="Where do you want to go?"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
data-finder-input
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="finder__status">
|
||||
<p class="finder__scope" data-finder-scope>
|
||||
<span data-finder-mode>Context</span>
|
||||
<span class="finder__crumb" data-finder-subject>
|
||||
{article?.title ?? 'Index'}
|
||||
</span>
|
||||
</p>
|
||||
<p class="finder__count" data-finder-count>
|
||||
{String(count).padStart(2, '0')} entries
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="finder-results" class="finder__results" role="listbox" aria-label="Articles">
|
||||
<div data-finder-context>
|
||||
{groups.map((group, g) => {
|
||||
const heading = group.heading?.title ?? 'Index'
|
||||
return (
|
||||
<div class="finder__group" role="group" aria-label={heading}>
|
||||
<p class="finder__heading" aria-hidden="true">
|
||||
<span class="finder__section">§ {toRoman(g + 1)}</span>
|
||||
{heading}
|
||||
</p>
|
||||
{group.entries.map((entry, i) => (
|
||||
<a
|
||||
id={`finder-option-${g}-${i}`}
|
||||
class="finder__row"
|
||||
href={entry.path}
|
||||
role="option"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span class="finder__type">{COLLECTION_LABELS[entry.collection]}</span>
|
||||
<span class="finder__label">{entry.title}</span>
|
||||
<span class="finder__path">{entry.path}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div data-finder-matches hidden></div>
|
||||
<p class="finder__empty" data-finder-empty hidden>
|
||||
No entries found
|
||||
</p>
|
||||
|
||||
<template data-finder-row>
|
||||
<a class="finder__row" role="option" tabindex="-1">
|
||||
<span class="finder__type" data-field="type"></span>
|
||||
<span class="finder__label" data-field="label"></span>
|
||||
<span class="finder__path" data-field="path"></span>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="finder__footer" aria-hidden="true">
|
||||
<span class="finder__hint">
|
||||
<kbd>↑</kbd>
|
||||
<kbd>↓</kbd> Move
|
||||
</span>
|
||||
<span class="finder__hint">
|
||||
<kbd>↵</kbd> Open
|
||||
</span>
|
||||
<span class="finder__hint">
|
||||
<kbd data-finder-shortcut>⌘K</kbd> Toggle
|
||||
</span>
|
||||
</div>
|
||||
</dialog>
|
||||
</site-finder>
|
||||
<script>
|
||||
import '@/lib/finder/element'
|
||||
</script>
|
||||
39
src/components/site/SiteFooter.astro
Normal file
39
src/components/site/SiteFooter.astro
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
import { toMilitaryDTG } from '@/lib/content/dates'
|
||||
import { getSite } from '@/lib/site'
|
||||
|
||||
const LINKED_PAGES = ['imprint', 'privacy-policy', 'colophon']
|
||||
|
||||
const site = await getSite()
|
||||
const release = site.releases[0]
|
||||
const pages = LINKED_PAGES.map((id) => site.pages.get(id)).filter((page) => page !== undefined)
|
||||
const year = new Date().getUTCFullYear()
|
||||
---
|
||||
|
||||
<footer class="site-footer dark">
|
||||
<div class="site-footer__inner">
|
||||
<p class="site-footer__copyright">© {year} Dave Damage</p>
|
||||
{release && (
|
||||
<p class="site-footer__release">
|
||||
<span class="site-footer__version">v{release.version}</span>
|
||||
<time datetime={release.date.toISOString()}>{toMilitaryDTG(release.date)}</time>
|
||||
</p>
|
||||
)}
|
||||
<nav class="site-footer__nav" aria-label="Site">
|
||||
<ul class="site-footer__links">
|
||||
{pages.map((page) => (
|
||||
<li>
|
||||
<a class="site-footer__link" href={page.path}>
|
||||
{page.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<a class="site-footer__link site-footer__top" href="#top">
|
||||
Top
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
26
src/components/site/SiteHeader.astro
Normal file
26
src/components/site/SiteHeader.astro
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import type { Article } from '@/lib/site/types'
|
||||
|
||||
import Finder from './Finder.astro'
|
||||
|
||||
type Props = {
|
||||
article?: Article
|
||||
}
|
||||
|
||||
const { article } = Astro.props
|
||||
---
|
||||
|
||||
<header class="site-header dark">
|
||||
<div class="site-header__inner">
|
||||
<a class="site-header__home" href="/" aria-label="dave dmg, home">
|
||||
<span class="site-header__logo" aria-hidden="true">
|
||||
◬
|
||||
</span>
|
||||
<span class="site-header__name">
|
||||
dave<span class="site-header__bracket">[</span>dmg
|
||||
<span class="site-header__bracket">]</span>
|
||||
</span>
|
||||
</a>
|
||||
<Finder article={article} />
|
||||
</div>
|
||||
</header>
|
||||
42
src/components/terms/TermEntry.astro
Normal file
42
src/components/terms/TermEntry.astro
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
import { TERM_KINDS, type TermKind } from '@/lib/constants/terms'
|
||||
|
||||
export type SeeLink = {
|
||||
title: string
|
||||
href: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
id: string
|
||||
title: string
|
||||
kind: TermKind
|
||||
see?: SeeLink[]
|
||||
ink?: boolean
|
||||
}
|
||||
|
||||
const { id, title, kind, see = [], ink } = Astro.props
|
||||
|
||||
const label = TERM_KINDS.find((option) => option.value === kind)?.label ?? kind
|
||||
---
|
||||
|
||||
<dt class:list={['term', ink && 'dark']} id={id}>
|
||||
<span class="term__name">{title}</span>
|
||||
<span class="term__kind">{label}</span>
|
||||
</dt>
|
||||
|
||||
<dd class="term__body">
|
||||
<p class="term__definition">
|
||||
<slot />
|
||||
</p>
|
||||
|
||||
{see.length > 0 && (
|
||||
<p class="term__see">
|
||||
<span class="term__see-label">See</span>
|
||||
{see.map((related) => (
|
||||
<a class="term__link" href={related.href}>
|
||||
{related.title}
|
||||
</a>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
</dd>
|
||||
@@ -1 +0,0 @@
|
||||
export const collections = {}
|
||||
15
src/content/general/articles/awq.mdoc
Normal file
15
src/content/general/articles/awq.mdoc
Normal file
@@ -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: []
|
||||
---
|
||||
20
src/content/general/articles/calcination.mdoc
Normal file
20
src/content/general/articles/calcination.mdoc
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Calcination
|
||||
subtitle: Where we burn the people
|
||||
status: draft
|
||||
noindex: false
|
||||
stage: stub
|
||||
route:
|
||||
parent: stages
|
||||
index: 2
|
||||
sequence: false
|
||||
summary:
|
||||
cover:
|
||||
effect: none
|
||||
ratio: original
|
||||
published: 2026-09-30T12:27:00.000Z
|
||||
tags:
|
||||
- alchemical-materialism
|
||||
- genesis
|
||||
- the-crucible
|
||||
---
|
||||
16
src/content/general/articles/chainbreaker.mdoc
Normal file
16
src/content/general/articles/chainbreaker.mdoc
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: Chainbreaker
|
||||
subtitle: We're gonna break down the walls
|
||||
status: draft
|
||||
noindex: false
|
||||
stage: stub
|
||||
route:
|
||||
index: 2
|
||||
sequence: false
|
||||
summary:
|
||||
cover:
|
||||
effect: none
|
||||
ratio: original
|
||||
published: 2026-09-30T14:27:00.000Z
|
||||
tags: []
|
||||
---
|
||||
20
src/content/general/articles/coagulation.mdoc
Normal file
20
src/content/general/articles/coagulation.mdoc
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Coagulation
|
||||
subtitle: Where we burn the politicians
|
||||
status: draft
|
||||
noindex: false
|
||||
stage: stub
|
||||
route:
|
||||
parent: stages
|
||||
index: 6
|
||||
sequence: false
|
||||
summary:
|
||||
cover:
|
||||
effect: none
|
||||
ratio: original
|
||||
published: 2026-09-30T12:27:00.000Z
|
||||
tags:
|
||||
- alchemical-materialism
|
||||
- genesis
|
||||
- the-crucible
|
||||
---
|
||||
20
src/content/general/articles/conjunction.mdoc
Normal file
20
src/content/general/articles/conjunction.mdoc
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Conjunction
|
||||
subtitle: Where we burn the state
|
||||
status: draft
|
||||
noindex: false
|
||||
stage: stub
|
||||
route:
|
||||
parent: stages
|
||||
index: 5
|
||||
sequence: false
|
||||
summary:
|
||||
cover:
|
||||
effect: none
|
||||
ratio: original
|
||||
published: 2026-09-30T12:27:00.000Z
|
||||
tags:
|
||||
- alchemical-materialism
|
||||
- genesis
|
||||
- the-crucible
|
||||
---
|
||||
20
src/content/general/articles/dissolution.mdoc
Normal file
20
src/content/general/articles/dissolution.mdoc
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Dissolution
|
||||
subtitle: Where we act
|
||||
status: draft
|
||||
noindex: false
|
||||
stage: stub
|
||||
route:
|
||||
parent: stages
|
||||
index: 7
|
||||
sequence: false
|
||||
summary:
|
||||
cover:
|
||||
effect: none
|
||||
ratio: original
|
||||
published: 2026-09-30T12:27:00.000Z
|
||||
tags:
|
||||
- alchemical-materialism
|
||||
- genesis
|
||||
- the-crucible
|
||||
---
|
||||
20
src/content/general/articles/fermentation.mdoc
Normal file
20
src/content/general/articles/fermentation.mdoc
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Fermentation
|
||||
subtitle: Where we burn the priests
|
||||
status: draft
|
||||
noindex: false
|
||||
stage: stub
|
||||
route:
|
||||
parent: stages
|
||||
index: 3
|
||||
sequence: false
|
||||
summary:
|
||||
cover:
|
||||
effect: none
|
||||
ratio: original
|
||||
published: 2026-09-30T12:27:00.000Z
|
||||
tags:
|
||||
- alchemical-materialism
|
||||
- genesis
|
||||
- the-crucible
|
||||
---
|
||||
212
src/content/general/articles/prima-materia.mdoc
Normal file
212
src/content/general/articles/prima-materia.mdoc
Normal file
@@ -0,0 +1,212 @@
|
||||
---
|
||||
title: Prima Materia
|
||||
subtitle: Where we lay the ground
|
||||
status: draft
|
||||
noindex: false
|
||||
stage: stub
|
||||
route:
|
||||
parent: stages
|
||||
index: 1
|
||||
sequence: false
|
||||
summary:
|
||||
cover:
|
||||
image: /src/assets/content/articles/prima-materia/summary/cover/image.jpg
|
||||
alt: The Great Day of His Wrath
|
||||
credit: John Martin – The Great Day of His Wrath
|
||||
effect: xeroxed
|
||||
ratio: 21-9
|
||||
published: 2026-09-21T12:35:00.000Z
|
||||
tags:
|
||||
- the-crucible
|
||||
- alchemical-materialism
|
||||
- genesis
|
||||
---
|
||||
{% lead %}
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. This first paragraph is the lead, so it should be larger and carry the drop cap. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. A sentence with *emphasis*, **strong text**, `inline code`, ~~struck text~~ and an [external link](https://example.com).
|
||||
{% /lead %}
|
||||
|
||||
## Heading two, long enough to wrap onto a second line
|
||||
|
||||
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur{% footnote note="A footnote with *emphasis* and a [link](https://example.com)." /%}. Excepteur sint occaecat cupidatat non proident{% sidenote type="default" marker="†" note="A sidenote in the margin, beside the line it belongs to." /%}.
|
||||
|
||||
### Heading three
|
||||
|
||||
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam.
|
||||
|
||||
#### Heading four
|
||||
|
||||
At vero eos et accusamus et iusto odio dignissimos ducimus.
|
||||
|
||||
## Lists
|
||||
|
||||
- First item
|
||||
- Second item, with enough text to run past the end of a line and wrap underneath the marker
|
||||
- Third item
|
||||
- Nested item
|
||||
- Another nested item
|
||||
- Third level
|
||||
|
||||
1. First step
|
||||
1. Second step
|
||||
1. Sub-step
|
||||
1. Another sub-step
|
||||
1. Third step
|
||||
|
||||
## Quotes and rules
|
||||
|
||||
> A plain blockquote, no attribution.
|
||||
>
|
||||
> With a second paragraph.
|
||||
|
||||
{% quote
|
||||
cite="Paracelsus"
|
||||
source="Die dritte Defension"
|
||||
href="https://example.com" %}
|
||||
All things are poison, and nothing is without poison; the dosage alone makes it so a thing is not a poison.
|
||||
{% /quote %}
|
||||
|
||||
---
|
||||
|
||||
## Callouts
|
||||
|
||||
{% callout type="default" title="TITA TITLE" %}
|
||||
I AM DONE WITH THIS SHIT! SO COMPLETELY AND TOTALLY DONE!
|
||||
{% /callout %}
|
||||
|
||||
{% callout type="optional" title="Note" %}
|
||||
A default callout with a [link](https://example.com) and `code` inside it.
|
||||
{% /callout %}
|
||||
|
||||
{% callout type="example" title="Worked example" %}
|
||||
An example callout containing a list:
|
||||
|
||||
- one
|
||||
- two
|
||||
{% /callout %}
|
||||
|
||||
{% reveal summary="Spoiler" %}
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. This first paragraph is the lead, so it should be larger and carry the drop cap. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. A sentence with *emphasis*, **strong text**, `inline code`, ~~struck text~~ and an [external link](https://example.com).
|
||||
{% /reveal %}
|
||||
|
||||
## Code
|
||||
|
||||
```js
|
||||
const crucible = { stage: 'prima-materia', heat: 3 }
|
||||
|
||||
for (const [key, value] of Object.entries(crucible)) {
|
||||
console.log(`${key}: ${value}`)
|
||||
}
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
{% tableWrapper
|
||||
variant="default"
|
||||
caption="Stages of the Work"
|
||||
columns=["rowheader", "key", "num", "num", "text"] %}
|
||||
{% table %}
|
||||
- Stage
|
||||
- No
|
||||
- Heat
|
||||
- Hours
|
||||
- Notes
|
||||
---
|
||||
- Prima Materia
|
||||
- I
|
||||
- 3
|
||||
- 0.25
|
||||
- The undifferentiated ground; nothing is named yet.
|
||||
---
|
||||
- Calcination
|
||||
- II
|
||||
- 7
|
||||
- 1.50
|
||||
- Burned down to what will not burn.
|
||||
---
|
||||
- Solutio
|
||||
- III
|
||||
- 5
|
||||
- 12
|
||||
- Dissolved back into flow; forms loosen.
|
||||
---
|
||||
- Tincture
|
||||
- VII
|
||||
- 10
|
||||
- 120.75
|
||||
- The finished colour, carried into everything it touches.
|
||||
{% /table %}
|
||||
{% /tableWrapper %}
|
||||
|
||||
## Images
|
||||
|
||||
{% figure
|
||||
image="/src/assets/content/articles/prima-materia/john_martin_-_the_great_day_of_his_wrath_-_google_art_project.jpg"
|
||||
alt="A placeholder image"
|
||||
caption="An inline figure with a caption"
|
||||
credit="Dave Damage"
|
||||
effect="xeroxed"
|
||||
ratio="16-9"
|
||||
size="inline" /%}
|
||||
|
||||
Aliquam erat volutpat. Fusce sed velit at arcu tincidunt vulputate{% sidenote type="example" marker="‡" note="A second sidenote, typed as an example." /%}. Nam vitae lacus a nisl ultrices dictum.
|
||||
|
||||
Finally, a link to another article: {% xref reference="the-crucible" %}The Crucible{% /xref %}.
|
||||
|
||||
## Code
|
||||
|
||||
```ts
|
||||
const crucible = { stage: 'prima-materia', heat: 3 }
|
||||
|
||||
function temper(work: typeof crucible): 'calcined' | 'raw' {
|
||||
return work.heat > 5 ? 'calcined' : 'raw'
|
||||
}
|
||||
```
|
||||
|
||||
A fence with no language, which should render unhighlighted and with no label:
|
||||
|
||||
```
|
||||
Prima materia — the undifferentiated ground.
|
||||
Nothing is named yet.
|
||||
```
|
||||
|
||||
A long line, which should scroll inside the block rather than widen the article:
|
||||
|
||||
```css
|
||||
.prose :is(h2, h3, h4) + .table-wrapper thead th { border-block-end: var(--border-card) solid var(--color-border-strong); color: inherit; background-color: transparent; }
|
||||
```
|
||||
|
||||
An unknown language, which should render plain and raise a warning in the dev banner:
|
||||
|
||||
```brainfuck
|
||||
+[----->+++<]>+.
|
||||
```
|
||||
|
||||
{% callout type="example" title="Code inside a block" %}
|
||||
The frame should sit inside the callout body, not fight it:
|
||||
|
||||
```js
|
||||
export const isTag = (node) => Markdoc.Tag.isTag(node)
|
||||
```
|
||||
{% /callout %}
|
||||
|
||||
## Spoiler
|
||||
|
||||
Duis aute irure dolor in reprehenderit, and then {% spoiler %}the prima materia was the vessel all along{% /spoiler %}, which changes the reading of everything above.
|
||||
|
||||
A spoiler long enough to wrap across a line break: {% spoiler %}Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua{% /spoiler %}. And text after it.
|
||||
|
||||
## Definitions
|
||||
|
||||
{% definitions %}
|
||||
{% definition term="Prima Materia" kind="prime" see="Calcination, Solutio" %}
|
||||
The undifferentiated ground. Nothing is named yet, which is precisely what makes it workable.
|
||||
{% /definition %}
|
||||
|
||||
{% definition term="Calcination" kind="stage" see="Prima Materia" %}
|
||||
Burned down to what will not burn.
|
||||
{% /definition %}
|
||||
|
||||
{% definition term="Solutio" kind="stage" %}
|
||||
Dissolved back into flow, so that forms loosen.
|
||||
{% /definition %}
|
||||
{% /definitions %}
|
||||
15
src/content/general/articles/stages.mdoc
Normal file
15
src/content/general/articles/stages.mdoc
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Stages
|
||||
status: draft
|
||||
noindex: false
|
||||
stage: stub
|
||||
route:
|
||||
parent: the-crucible
|
||||
sequence: true
|
||||
summary:
|
||||
cover:
|
||||
effect: none
|
||||
ratio: original
|
||||
published: 2026-09-21T12:34:00.000Z
|
||||
tags: []
|
||||
---
|
||||
20
src/content/general/articles/sublimation.mdoc
Normal file
20
src/content/general/articles/sublimation.mdoc
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Sublimation
|
||||
subtitle: Where we burn the witches
|
||||
status: draft
|
||||
noindex: false
|
||||
stage: stub
|
||||
route:
|
||||
parent: stages
|
||||
index: 4
|
||||
sequence: false
|
||||
summary:
|
||||
cover:
|
||||
effect: none
|
||||
ratio: original
|
||||
published: 2026-09-30T12:27:00.000Z
|
||||
tags:
|
||||
- alchemical-materialism
|
||||
- genesis
|
||||
- the-crucible
|
||||
---
|
||||
20
src/content/general/articles/the-crucible.mdoc
Normal file
20
src/content/general/articles/the-crucible.mdoc
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: The Crucible
|
||||
status: draft
|
||||
noindex: false
|
||||
stage: stub
|
||||
route:
|
||||
index: 1
|
||||
sequence: false
|
||||
summary:
|
||||
excerpt: >-
|
||||
Where we explore the crucible – a materialistic alchemist Worldbuilding
|
||||
Framework
|
||||
cover:
|
||||
effect: none
|
||||
ratio: original
|
||||
published: 2026-09-21T12:33:00.000Z
|
||||
tags:
|
||||
- the-crucible
|
||||
---
|
||||
hmm, do you even sharp?
|
||||
7
src/content/general/pages/colophon.mdoc
Normal file
7
src/content/general/pages/colophon.mdoc
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Colophon
|
||||
status: draft
|
||||
noindex: false
|
||||
route: {}
|
||||
summary: {}
|
||||
---
|
||||
8
src/content/general/pages/imprint.mdoc
Normal file
8
src/content/general/pages/imprint.mdoc
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Imprint
|
||||
status: draft
|
||||
noindex: false
|
||||
route: {}
|
||||
summary: {}
|
||||
---
|
||||
Set in Blaka{% footnote note="Jdajhdasljkh" /%}.
|
||||
7
src/content/general/pages/privacy-policy.mdoc
Normal file
7
src/content/general/pages/privacy-policy.mdoc
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Privacy Policy
|
||||
status: draft
|
||||
noindex: false
|
||||
route: {}
|
||||
summary: {}
|
||||
---
|
||||
3
src/content/general/tags/alchemical-materialism.json
Normal file
3
src/content/general/tags/alchemical-materialism.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"title": "Alchemical Materialism"
|
||||
}
|
||||
3
src/content/general/tags/genesis.json
Normal file
3
src/content/general/tags/genesis.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"title": "Genesis"
|
||||
}
|
||||
4
src/content/general/tags/the-crucible.json
Normal file
4
src/content/general/tags/the-crucible.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "The Crucible",
|
||||
"description": "I AM JUST A ITSY-BITSY TEENY-WEENIE TEST"
|
||||
}
|
||||
6
src/content/site/releases/v0-12.yaml
Normal file
6
src/content/site/releases/v0-12.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
version: '0.12'
|
||||
date: 2026-09-30T13:00:00.000Z
|
||||
changes:
|
||||
- kind: added
|
||||
article: prima-materia
|
||||
text: Section on the ground state; first table of correspondences
|
||||
12
src/content/site/releases/v0-13.yaml
Normal file
12
src/content/site/releases/v0-13.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
version: '0.13'
|
||||
date: 2026-09-30T13:00:00.000Z
|
||||
changes:
|
||||
- kind: added
|
||||
article: calcination
|
||||
text: Test
|
||||
- kind: added
|
||||
article: fermentation
|
||||
text: Big test with a longer sentence
|
||||
- kind: changed
|
||||
article: coagulation
|
||||
text: Lorem ipsum, blablabla ipbsum whorum
|
||||
19
src/content/site/releases/v0-14.yaml
Normal file
19
src/content/site/releases/v0-14.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
version: '0.14'
|
||||
date: 2026-09-30T13:00:00.000Z
|
||||
changes:
|
||||
- kind: added
|
||||
article: coagulation
|
||||
text: >-
|
||||
s the raw release exactly as Keystatic reads it, inferred from the schema
|
||||
like the other three
|
||||
- kind: changed
|
||||
article: prima-materia
|
||||
text: >-
|
||||
releases are loaded, resolved and attached to articles. Six files; nothing
|
||||
renders yet.
|
||||
- kind: fixed
|
||||
article: fermentation
|
||||
text: We don't burn the priests anymore, we burn the gods
|
||||
- kind: removed
|
||||
article: the-crucible
|
||||
text: AND EVERYTHING GOES BOOOOOOOM
|
||||
18
src/integrations/content-reload.ts
Normal file
18
src/integrations/content-reload.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { AstroIntegration } from 'astro'
|
||||
|
||||
export const contentReload = (): AstroIntegration => ({
|
||||
name: 'content-reload',
|
||||
hooks: {
|
||||
'astro:server:setup': ({ server }) => {
|
||||
server.watcher.on('all', (_event, file) => {
|
||||
if (!file.includes('/src/content')) return
|
||||
for (const name of ['ssr', 'prerender'])
|
||||
server.environments[name]?.hot.send('astro:content-changed', {})
|
||||
server.environments.client.hot.send({
|
||||
type: 'full-reload',
|
||||
path: '*',
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
19
src/keystatic/blocks/general/callout.ts
Normal file
19
src/keystatic/blocks/general/callout.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
import { wrapper } from '@keystatic/core/content-components'
|
||||
|
||||
import { NOTE_TYPES } from '@/lib/constants/blocks/notes'
|
||||
|
||||
export const callout = wrapper({
|
||||
label: 'Callout',
|
||||
description: 'An aside around any content',
|
||||
schema: {
|
||||
type: fields.select({
|
||||
label: 'Type',
|
||||
options: NOTE_TYPES,
|
||||
defaultValue: 'default',
|
||||
}),
|
||||
title: fields.text({
|
||||
label: 'Title',
|
||||
}),
|
||||
},
|
||||
})
|
||||
27
src/keystatic/blocks/general/definition-list/definition.ts
Normal file
27
src/keystatic/blocks/general/definition-list/definition.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
import { wrapper } from '@keystatic/core/content-components'
|
||||
|
||||
import { TERM_KINDS } from '@/lib/constants/terms'
|
||||
|
||||
export const definition = wrapper({
|
||||
label: 'Definition',
|
||||
description: 'One term and its meaning',
|
||||
forSpecificLocations: true,
|
||||
schema: {
|
||||
term: fields.text({
|
||||
label: 'Term',
|
||||
validation: {
|
||||
isRequired: true,
|
||||
},
|
||||
}),
|
||||
kind: fields.select({
|
||||
label: 'Kind',
|
||||
options: TERM_KINDS,
|
||||
defaultValue: 'entry',
|
||||
}),
|
||||
see: fields.text({
|
||||
label: 'See also',
|
||||
description: 'Other terms in this list, comma separated',
|
||||
}),
|
||||
},
|
||||
})
|
||||
13
src/keystatic/blocks/general/definition-list/definitions.ts
Normal file
13
src/keystatic/blocks/general/definition-list/definitions.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { repeating } from '@keystatic/core/content-components'
|
||||
|
||||
export const definitions = repeating({
|
||||
label: 'Definitions',
|
||||
description: 'A list of terms defined for this article only',
|
||||
children: ['definition'],
|
||||
validation: {
|
||||
children: {
|
||||
min: 1,
|
||||
},
|
||||
},
|
||||
schema: {},
|
||||
})
|
||||
21
src/keystatic/blocks/general/figure.ts
Normal file
21
src/keystatic/blocks/general/figure.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
import { block } from '@keystatic/core/content-components'
|
||||
|
||||
import { imageFields } from '@/keystatic/fields/content/image'
|
||||
import { FIGURE_SIZES } from '@/lib/constants/figure'
|
||||
|
||||
export const figure = (collection: string) =>
|
||||
block({
|
||||
label: 'Figure',
|
||||
description: 'An image with alt text, and optionally a caption and credit.',
|
||||
schema: {
|
||||
...imageFields(collection, {
|
||||
required: true,
|
||||
}),
|
||||
size: fields.select({
|
||||
label: 'Size',
|
||||
options: FIGURE_SIZES,
|
||||
defaultValue: 'inline',
|
||||
}),
|
||||
},
|
||||
})
|
||||
16
src/keystatic/blocks/general/footnote.ts
Normal file
16
src/keystatic/blocks/general/footnote.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
import { inline } from '@keystatic/core/content-components'
|
||||
|
||||
export const footnote = inline({
|
||||
label: 'Footnote',
|
||||
description: 'Numbered automatically; the note is collected at the end of the article',
|
||||
schema: {
|
||||
note: fields.text({
|
||||
label: 'Note',
|
||||
multiline: true,
|
||||
validation: {
|
||||
isRequired: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
27
src/keystatic/blocks/general/index.ts
Normal file
27
src/keystatic/blocks/general/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { callout } from './callout'
|
||||
import { definition } from './definition-list/definition'
|
||||
import { definitions } from './definition-list/definitions'
|
||||
import { figure } from './figure'
|
||||
import { footnote } from './footnote'
|
||||
import { lead } from './lead'
|
||||
import { quote } from './quote'
|
||||
import { reveal } from './reveal'
|
||||
import { sidenote } from './sidenote'
|
||||
import { spoiler } from './spoiler'
|
||||
import { tableWrapper } from './table-wrapper'
|
||||
import { xref } from './xref'
|
||||
|
||||
export const generalBlocks = (collection: string) => ({
|
||||
figure: figure(collection),
|
||||
callout,
|
||||
definition,
|
||||
definitions,
|
||||
lead,
|
||||
footnote,
|
||||
sidenote,
|
||||
tableWrapper,
|
||||
quote,
|
||||
reveal,
|
||||
spoiler,
|
||||
xref: xref(),
|
||||
})
|
||||
7
src/keystatic/blocks/general/lead.ts
Normal file
7
src/keystatic/blocks/general/lead.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { wrapper } from '@keystatic/core/content-components'
|
||||
|
||||
export const lead = wrapper({
|
||||
label: 'Lead',
|
||||
description: 'The opening paragraph, set larger and with the drop cap',
|
||||
schema: {},
|
||||
})
|
||||
17
src/keystatic/blocks/general/quote.ts
Normal file
17
src/keystatic/blocks/general/quote.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
import { wrapper } from '@keystatic/core/content-components'
|
||||
|
||||
export const quote = wrapper({
|
||||
label: 'Quote',
|
||||
schema: {
|
||||
cite: fields.text({
|
||||
label: 'Cite',
|
||||
}),
|
||||
source: fields.text({
|
||||
label: 'Source',
|
||||
}),
|
||||
href: fields.url({
|
||||
label: 'Source Link',
|
||||
}),
|
||||
},
|
||||
})
|
||||
14
src/keystatic/blocks/general/reveal.ts
Normal file
14
src/keystatic/blocks/general/reveal.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
import { wrapper } from '@keystatic/core/content-components'
|
||||
|
||||
export const reveal = wrapper({
|
||||
label: 'Reveal',
|
||||
description: 'A spoiler around any content, closed until the reader opens it',
|
||||
schema: {
|
||||
summary: fields.text({
|
||||
label: 'Label',
|
||||
description: 'What the reader sees before opening.',
|
||||
defaultValue: 'Spoiler',
|
||||
}),
|
||||
},
|
||||
})
|
||||
33
src/keystatic/blocks/general/sidenote.ts
Normal file
33
src/keystatic/blocks/general/sidenote.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
import { inline } from '@keystatic/core/content-components'
|
||||
|
||||
import { NOTE_TYPES } from '@/lib/constants/blocks/notes'
|
||||
|
||||
export const sidenote = inline({
|
||||
label: 'Sidenote',
|
||||
schema: {
|
||||
type: fields.select({
|
||||
label: 'Type',
|
||||
options: NOTE_TYPES,
|
||||
defaultValue: 'default',
|
||||
}),
|
||||
marker: fields.text({
|
||||
label: 'Marker',
|
||||
description: 'One or two characters, e.g. * † ‡ a.',
|
||||
defaultValue: '▾',
|
||||
validation: {
|
||||
isRequired: true,
|
||||
length: {
|
||||
max: 3,
|
||||
},
|
||||
},
|
||||
}),
|
||||
note: fields.text({
|
||||
label: 'Note',
|
||||
multiline: true,
|
||||
validation: {
|
||||
isRequired: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
15
src/keystatic/blocks/general/spoiler.ts
Normal file
15
src/keystatic/blocks/general/spoiler.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { mark } from '@keystatic/core/content-components'
|
||||
|
||||
import { eyeOffIcon } from '@keystar/ui/icon/icons/eyeOffIcon'
|
||||
|
||||
export const spoiler = mark({
|
||||
label: 'Spoiler',
|
||||
icon: eyeOffIcon,
|
||||
tag: 'span',
|
||||
schema: {},
|
||||
style: {
|
||||
backgroundColor: '#2A2A2A',
|
||||
color: '#F5F5F5',
|
||||
padding: '0 2px',
|
||||
},
|
||||
})
|
||||
31
src/keystatic/blocks/general/table-wrapper.ts
Normal file
31
src/keystatic/blocks/general/table-wrapper.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
import { wrapper } from '@keystatic/core/content-components'
|
||||
|
||||
import { COLUMN_TYPES, TABLE_VARIANTS } from '@/lib/constants/blocks/table'
|
||||
|
||||
export const tableWrapper = wrapper({
|
||||
label: 'Table Wrapper',
|
||||
description: 'Styling and caption for one table',
|
||||
schema: {
|
||||
variant: fields.select({
|
||||
label: 'Variant',
|
||||
options: TABLE_VARIANTS,
|
||||
defaultValue: 'default',
|
||||
}),
|
||||
caption: fields.text({
|
||||
label: 'Caption',
|
||||
}),
|
||||
columns: fields.array(
|
||||
fields.select({
|
||||
label: 'Type',
|
||||
options: COLUMN_TYPES,
|
||||
defaultValue: 'default',
|
||||
}),
|
||||
{
|
||||
label: 'Columns',
|
||||
description: 'One per column, left to right.',
|
||||
itemLabel: (item) => item.value,
|
||||
},
|
||||
),
|
||||
},
|
||||
})
|
||||
23
src/keystatic/blocks/general/xref.ts
Normal file
23
src/keystatic/blocks/general/xref.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
import { mark } from '@keystatic/core/content-components'
|
||||
|
||||
import { linkIcon } from '@keystar/ui/icon/icons/linkIcon'
|
||||
|
||||
import { ARTICLES } from '@/lib/constants/collections'
|
||||
|
||||
export const xref = (collection: string = ARTICLES) =>
|
||||
mark({
|
||||
label: 'Internal link',
|
||||
icon: linkIcon,
|
||||
tag: 'a',
|
||||
schema: {
|
||||
reference: fields.relationship({
|
||||
label: 'Reference',
|
||||
collection,
|
||||
validation: { isRequired: true },
|
||||
}),
|
||||
anchor: fields.text({
|
||||
label: 'Heading id',
|
||||
}),
|
||||
},
|
||||
})
|
||||
30
src/keystatic/collections/general/articles.ts
Normal file
30
src/keystatic/collections/general/articles.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { collection, fields } from '@keystatic/core'
|
||||
|
||||
import { base, published, stage } from '@/keystatic/fields/content/base'
|
||||
import { content } from '@/keystatic/fields/content/content'
|
||||
import { articleRoute } from '@/keystatic/fields/content/route'
|
||||
import { articleSummary } from '@/keystatic/fields/content/summary'
|
||||
import { ARTICLES, TAGS } from '@/lib/constants/collections'
|
||||
|
||||
export const articles = collection({
|
||||
label: 'Articles',
|
||||
slugField: 'title',
|
||||
path: `src/content/general/${ARTICLES}/*`,
|
||||
format: {
|
||||
contentField: 'content',
|
||||
},
|
||||
entryLayout: 'content',
|
||||
columns: ['title', 'status', 'stage'],
|
||||
schema: {
|
||||
...base,
|
||||
stage,
|
||||
route: articleRoute,
|
||||
summary: articleSummary(ARTICLES),
|
||||
published,
|
||||
tags: fields.multiRelationship({
|
||||
label: 'Tags',
|
||||
collection: TAGS,
|
||||
}),
|
||||
content: content(ARTICLES),
|
||||
},
|
||||
})
|
||||
11
src/keystatic/collections/general/index.ts
Normal file
11
src/keystatic/collections/general/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ARTICLES, PAGES, TAGS } from '@/lib/constants/collections'
|
||||
|
||||
import { articles } from './articles'
|
||||
import { pages } from './pages'
|
||||
import { tags } from './tags'
|
||||
|
||||
export const contentCollections = {
|
||||
[ARTICLES]: articles,
|
||||
[PAGES]: pages,
|
||||
[TAGS]: tags,
|
||||
}
|
||||
23
src/keystatic/collections/general/pages.ts
Normal file
23
src/keystatic/collections/general/pages.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { collection } from '@keystatic/core'
|
||||
|
||||
import { base } from '@/keystatic/fields/content/base'
|
||||
import { content } from '@/keystatic/fields/content/content'
|
||||
import { pageRoute } from '@/keystatic/fields/content/route'
|
||||
import { pageSummary } from '@/keystatic/fields/content/summary'
|
||||
import { PAGES } from '@/lib/constants/collections'
|
||||
|
||||
export const pages = collection({
|
||||
label: 'Pages',
|
||||
slugField: 'title',
|
||||
path: `src/content/general/${PAGES}/*`,
|
||||
format: {
|
||||
contentField: 'content',
|
||||
},
|
||||
columns: ['title', 'status'],
|
||||
schema: {
|
||||
...base,
|
||||
route: pageRoute,
|
||||
summary: pageSummary,
|
||||
content: content(PAGES),
|
||||
},
|
||||
})
|
||||
21
src/keystatic/collections/general/tags.ts
Normal file
21
src/keystatic/collections/general/tags.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { collection, fields } from '@keystatic/core'
|
||||
|
||||
import { title } from '@/keystatic/fields/content/base'
|
||||
import { TAGS } from '@/lib/constants/collections'
|
||||
|
||||
export const tags = collection({
|
||||
label: 'Tags',
|
||||
slugField: 'title',
|
||||
path: `src/content/general/${TAGS}/*`,
|
||||
format: {
|
||||
data: 'json',
|
||||
},
|
||||
columns: ['title'],
|
||||
schema: {
|
||||
title,
|
||||
description: fields.text({
|
||||
label: 'Description',
|
||||
multiline: true,
|
||||
}),
|
||||
},
|
||||
})
|
||||
7
src/keystatic/collections/site/index.ts
Normal file
7
src/keystatic/collections/site/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { RELEASES } from '@/lib/constants/collections'
|
||||
|
||||
import { releases } from './releases'
|
||||
|
||||
export const siteCollections = {
|
||||
[RELEASES]: releases,
|
||||
}
|
||||
66
src/keystatic/collections/site/releases.ts
Normal file
66
src/keystatic/collections/site/releases.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { collection, fields } from '@keystatic/core'
|
||||
|
||||
import { CHANGE_TYPES } from '@/lib/constants/changelog'
|
||||
import { ARTICLES, RELEASES } from '@/lib/constants/collections'
|
||||
|
||||
const VERSION = /^\d+\.\d+(\.\d+)?$/
|
||||
|
||||
export const releases = collection({
|
||||
label: 'Releases',
|
||||
slugField: 'version',
|
||||
path: `src/content/site/${RELEASES}/*`,
|
||||
columns: ['version', 'date'],
|
||||
schema: {
|
||||
version: fields.slug({
|
||||
name: {
|
||||
label: 'Version',
|
||||
description: 'Without the v: 0.12, 1.0, 1.0.3',
|
||||
validation: {
|
||||
isRequired: true,
|
||||
pattern: {
|
||||
regex: VERSION,
|
||||
message: 'Numbers separated by dots, like 0.12 or 1.0.3',
|
||||
},
|
||||
},
|
||||
},
|
||||
slug: {
|
||||
label: 'File ID',
|
||||
generate: (version) => `v${version.replaceAll('.', '-')}`,
|
||||
},
|
||||
}),
|
||||
date: fields.datetime({
|
||||
label: 'Date',
|
||||
defaultValue: {
|
||||
kind: 'now',
|
||||
},
|
||||
validation: {
|
||||
isRequired: true,
|
||||
},
|
||||
}),
|
||||
changes: fields.array(
|
||||
fields.object({
|
||||
kind: fields.select({
|
||||
label: 'Kind',
|
||||
options: CHANGE_TYPES,
|
||||
defaultValue: 'added',
|
||||
}),
|
||||
article: fields.relationship({
|
||||
label: 'Article',
|
||||
description: 'Empty for site-wide changes',
|
||||
collection: ARTICLES,
|
||||
}),
|
||||
text: fields.text({
|
||||
label: 'Text',
|
||||
validation: {
|
||||
isRequired: true,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
{
|
||||
label: 'Changes',
|
||||
itemLabel: (props) =>
|
||||
`${props.fields.kind.value} · ${props.fields.article.value ?? 'site'} · ${props.fields.text.value}`,
|
||||
},
|
||||
),
|
||||
},
|
||||
})
|
||||
60
src/keystatic/fields/content/base.ts
Normal file
60
src/keystatic/fields/content/base.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
|
||||
import { STAGE_OPTIONS } from '@/lib/constants/stage'
|
||||
import { STATUS_OPTIONS } from '@/lib/constants/status'
|
||||
import { SEGMENT, SEGMENT_MESSAGE, slugify } from '@/lib/content/slug'
|
||||
|
||||
export const title = fields.slug({
|
||||
name: {
|
||||
label: 'Title',
|
||||
validation: {
|
||||
isRequired: true,
|
||||
},
|
||||
},
|
||||
slug: {
|
||||
label: 'File ID',
|
||||
description: 'Internal and unique across the collection.',
|
||||
generate: slugify,
|
||||
validation: {
|
||||
pattern: {
|
||||
regex: SEGMENT,
|
||||
message: SEGMENT_MESSAGE,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const subtitle = fields.text({
|
||||
label: 'Subtitle',
|
||||
})
|
||||
|
||||
export const status = fields.select({
|
||||
label: 'Status',
|
||||
options: STATUS_OPTIONS,
|
||||
defaultValue: 'draft',
|
||||
})
|
||||
|
||||
export const stage = fields.select({
|
||||
label: 'Stage',
|
||||
description: 'How finished the text is. Separate from status, which controls visibility',
|
||||
options: STAGE_OPTIONS,
|
||||
defaultValue: 'stub',
|
||||
})
|
||||
|
||||
export const noindex = fields.checkbox({
|
||||
label: 'Hide from search engines',
|
||||
})
|
||||
|
||||
export const published = fields.datetime({
|
||||
label: 'Published',
|
||||
defaultValue: {
|
||||
kind: 'now',
|
||||
},
|
||||
})
|
||||
|
||||
export const base = {
|
||||
title,
|
||||
subtitle,
|
||||
status,
|
||||
noindex,
|
||||
}
|
||||
14
src/keystatic/fields/content/content.ts
Normal file
14
src/keystatic/fields/content/content.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
|
||||
import { generalBlocks } from '@/keystatic/blocks/general'
|
||||
import { assets } from '@/lib/images/storage'
|
||||
|
||||
export const content = (collection: string) =>
|
||||
fields.markdoc({
|
||||
label: 'Content',
|
||||
options: {
|
||||
heading: [2, 3, 4],
|
||||
image: assets(collection),
|
||||
},
|
||||
components: generalBlocks(collection),
|
||||
})
|
||||
6
src/keystatic/fields/content/cover.ts
Normal file
6
src/keystatic/fields/content/cover.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
|
||||
import { imageFields } from './image'
|
||||
|
||||
export const cover = (collection: string) =>
|
||||
fields.object(imageFields(collection), { label: 'Cover' })
|
||||
36
src/keystatic/fields/content/image.ts
Normal file
36
src/keystatic/fields/content/image.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
|
||||
import { ASPECT_RATIOS, IMAGE_EFFECTS } from '@/lib/constants/figure'
|
||||
import { assets } from '@/lib/images/storage'
|
||||
|
||||
export const imageFields = (collection: string, { required = false } = {}) => ({
|
||||
image: fields.image({
|
||||
label: 'Image',
|
||||
...assets(collection),
|
||||
validation: {
|
||||
isRequired: required,
|
||||
},
|
||||
}),
|
||||
alt: fields.text({
|
||||
label: 'Alt text',
|
||||
validation: {
|
||||
isRequired: required,
|
||||
},
|
||||
}),
|
||||
caption: fields.text({
|
||||
label: 'Caption',
|
||||
}),
|
||||
credit: fields.text({
|
||||
label: 'Credit',
|
||||
}),
|
||||
effect: fields.select({
|
||||
label: 'Effect',
|
||||
options: IMAGE_EFFECTS,
|
||||
defaultValue: 'none',
|
||||
}),
|
||||
ratio: fields.select({
|
||||
label: 'Crop',
|
||||
options: ASPECT_RATIOS,
|
||||
defaultValue: 'original',
|
||||
}),
|
||||
})
|
||||
47
src/keystatic/fields/content/route.ts
Normal file
47
src/keystatic/fields/content/route.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
|
||||
import { ARTICLES } from '@/lib/constants/collections'
|
||||
import { OPTIONAL_SEGMENT, SEGMENT_MESSAGE } from '@/lib/content/slug'
|
||||
|
||||
const segment = fields.text({
|
||||
label: 'URL segment',
|
||||
description: 'Optional- Leave empty to use the slugified title',
|
||||
validation: {
|
||||
pattern: {
|
||||
regex: OPTIONAL_SEGMENT,
|
||||
message: SEGMENT_MESSAGE,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const pageRoute = fields.object(
|
||||
{
|
||||
segment,
|
||||
},
|
||||
{
|
||||
label: 'Route',
|
||||
},
|
||||
)
|
||||
|
||||
export const articleRoute = fields.object(
|
||||
{
|
||||
segment,
|
||||
parent: fields.relationship({
|
||||
label: 'Parent',
|
||||
description: 'Empty makes this a root',
|
||||
collection: ARTICLES,
|
||||
}),
|
||||
index: fields.integer({
|
||||
label: 'Index',
|
||||
description: 'Sorts among siblings. Empty sorts by date, newest first',
|
||||
}),
|
||||
sequence: fields.checkbox({
|
||||
label: 'Sequence',
|
||||
description:
|
||||
'Children form a numbered series with previous/next navigation. Give each child an index',
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Route',
|
||||
},
|
||||
)
|
||||
33
src/keystatic/fields/content/summary.ts
Normal file
33
src/keystatic/fields/content/summary.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
|
||||
import { cover } from './cover'
|
||||
|
||||
const excerpt = fields.text({
|
||||
label: 'Excerpt',
|
||||
multiline: true,
|
||||
validation: {
|
||||
length: {
|
||||
max: 160,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const pageSummary = fields.object(
|
||||
{
|
||||
excerpt,
|
||||
},
|
||||
{
|
||||
label: 'Summary',
|
||||
},
|
||||
)
|
||||
|
||||
export const articleSummary = (collection: string) =>
|
||||
fields.object(
|
||||
{
|
||||
excerpt,
|
||||
cover: cover(collection),
|
||||
},
|
||||
{
|
||||
label: 'Summary',
|
||||
},
|
||||
)
|
||||
@@ -1,14 +1,26 @@
|
||||
---
|
||||
import { Font } from 'astro:assets'
|
||||
|
||||
import type { Article } from '@/lib/site/types'
|
||||
|
||||
import '@/styles/global.css'
|
||||
|
||||
import SiteFooter from '@/components/site/SiteFooter.astro'
|
||||
import SiteHeader from '@/components/site/SiteHeader.astro'
|
||||
import { resolveImage } from '@/lib/images/resolve'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
description?: string
|
||||
image?: string
|
||||
noindex?: boolean
|
||||
article?: Article
|
||||
}
|
||||
|
||||
const { title, description } = Astro.props
|
||||
const { title, description, image, noindex, article } = Astro.props
|
||||
const canonical = new URL(Astro.url.pathname, Astro.site)
|
||||
const preview = image ? resolveImage(image) : undefined
|
||||
const previewUrl = preview ? new URL(preview.src, Astro.site) : undefined
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
@@ -20,14 +32,30 @@ const { title, description } = Astro.props
|
||||
|
||||
<title>{title}</title>
|
||||
{description && <meta name="description" content={description} />}
|
||||
{noindex && <meta name="robots" content="noindex" />}
|
||||
<link rel="canonical" href={canonical} />
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:url" content={canonical} />
|
||||
{description && <meta property="og:description" content={description} />}
|
||||
{previewUrl && <meta property="og:image" content={previewUrl} />}
|
||||
<meta name="twitter:card" content={previewUrl ? 'summary_large_image' : 'summary'} />
|
||||
<slot name="head" />
|
||||
<Font cssVariable="--font-geist" preload />
|
||||
<Font cssVariable="--font-geist-mono" preload />
|
||||
<Font cssVariable="--font-blaka" preload />
|
||||
<Font cssVariable="--font-julia-mono" preload />
|
||||
<Font cssVariable="--font-unigrim" />
|
||||
<style is:inline>
|
||||
@layer reset, tokens, base, components.prose, components.blocks, utilities;
|
||||
</style>
|
||||
</head>
|
||||
<body class="ui">
|
||||
<slot />
|
||||
<SiteHeader article={article} />
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
<SiteFooter />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
104
src/layouts/general/ArticleLayout.astro
Normal file
104
src/layouts/general/ArticleLayout.astro
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
import DevBanner from '@/components/dev/DevBanner.astro'
|
||||
import FootnoteList from '@/components/markdoc/blocks/general/FootnoteList.astro'
|
||||
import Node from '@/components/markdoc/Node.astro'
|
||||
import ArticleFooter from '@/components/post/Footer/ArticleFooter.astro'
|
||||
import ArticleHeader from '@/components/post/Header/ArticleHeader.astro'
|
||||
import ToC from '@/components/post/ToC.astro'
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro'
|
||||
import type { Rendered } from '@/lib/markdoc/types'
|
||||
import { metaOf } from '@/lib/site/meta'
|
||||
import { sequenceOf } from '@/lib/site/navigation'
|
||||
import type { Article } from '@/lib/site/types'
|
||||
|
||||
type Props = {
|
||||
article: Article
|
||||
rendered: Rendered
|
||||
}
|
||||
|
||||
const { article, rendered } = Astro.props
|
||||
const meta = metaOf(article)
|
||||
const sequence = sequenceOf(article)
|
||||
---
|
||||
|
||||
<BaseLayout {...meta} article={article}>
|
||||
<article class="article">
|
||||
<ArticleHeader article={article} />
|
||||
<div class="frame">
|
||||
<div class="rail">
|
||||
<ToC headings={rendered.headings} />
|
||||
</div>
|
||||
<div class="body content prose">
|
||||
<Node node={rendered.tree} />
|
||||
</div>
|
||||
{Astro.slots.has('margin') && (
|
||||
<aside class="margin">
|
||||
<slot name="margin" />
|
||||
</aside>
|
||||
)}
|
||||
{rendered.footnotes.length > 0 && (
|
||||
<div class="notes content">
|
||||
<FootnoteList footnotes={rendered.footnotes} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ArticleFooter sequence={sequence} article={article} />
|
||||
</article>
|
||||
<DevBanner
|
||||
collection={article.collection}
|
||||
id={article.id}
|
||||
status={article.status}
|
||||
diagnostics={rendered.diagnostics}
|
||||
/>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.frame {
|
||||
@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;
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rail,
|
||||
.body,
|
||||
.margin,
|
||||
.notes {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (--bp-desktop) {
|
||||
.rail {
|
||||
grid-column: rail-start / rail-end;
|
||||
grid-row: 1 / 3;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.body {
|
||||
grid-column: body-start / body-end;
|
||||
}
|
||||
|
||||
.margin {
|
||||
grid-column: margin-start / margin-end;
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
|
||||
.notes {
|
||||
grid-column: body-start / body-end;
|
||||
margin-top: var(--layout-gutter);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
71
src/layouts/general/PageLayout.astro
Normal file
71
src/layouts/general/PageLayout.astro
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
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'
|
||||
import type { Rendered } from '@/lib/markdoc/types'
|
||||
import { metaOf } from '@/lib/site/meta'
|
||||
import type { Page } from '@/lib/site/types'
|
||||
|
||||
type Props = {
|
||||
page: Page
|
||||
rendered: Rendered
|
||||
}
|
||||
|
||||
const { page, rendered } = Astro.props
|
||||
const meta = metaOf(page)
|
||||
---
|
||||
|
||||
<BaseLayout {...meta}>
|
||||
<DevBanner
|
||||
collection={page.collection}
|
||||
id={page.id}
|
||||
status={page.status}
|
||||
diagnostics={rendered.diagnostics}
|
||||
/>
|
||||
<article class="page">
|
||||
<SimpleHeader eyebrow="Page" title={page.title} standfirst={page.subtitle || undefined} />
|
||||
<div class="frame">
|
||||
<div class="body content prose">
|
||||
<Node node={rendered.tree} />
|
||||
</div>
|
||||
{rendered.footnotes.length > 0 && (
|
||||
<div class="notes content">
|
||||
<FootnoteList footnotes={rendered.footnotes} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.frame {
|
||||
@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) {
|
||||
grid-column: body-start / body-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
22
src/layouts/general/TagLayout.astro
Normal file
22
src/layouts/general/TagLayout.astro
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
import DevBanner from '@/components/dev/DevBanner.astro'
|
||||
import ArticleCardGrid from '@/components/post/ArticleCard/ArticleCardGrid.astro'
|
||||
import SimpleHeader from '@/components/post/Header/SimpleHeader.astro'
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro'
|
||||
import type { Tag } from '@/lib/site/types'
|
||||
|
||||
type Props = { tag: Tag }
|
||||
|
||||
const { tag } = Astro.props
|
||||
---
|
||||
|
||||
<BaseLayout title={`Tagged: ${tag.title}`} description={tag.description || undefined}>
|
||||
<DevBanner collection={tag.collection} id={tag.id} />
|
||||
<SimpleHeader
|
||||
eyebrow="Tag"
|
||||
title={tag.title}
|
||||
standfirst={tag.description || undefined}
|
||||
count={tag.articles.length}
|
||||
/>
|
||||
<ArticleCardGrid articles={tag.articles} />
|
||||
</BaseLayout>
|
||||
16
src/lib/constants/blocks/notes.ts
Normal file
16
src/lib/constants/blocks/notes.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const NOTE_TYPES = [
|
||||
{
|
||||
label: 'Note',
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
label: 'Example',
|
||||
value: 'example',
|
||||
},
|
||||
{
|
||||
label: 'Optional',
|
||||
value: 'optional',
|
||||
},
|
||||
] as const
|
||||
|
||||
export type CalloutType = (typeof NOTE_TYPES)[number]['value']
|
||||
37
src/lib/constants/blocks/table.ts
Normal file
37
src/lib/constants/blocks/table.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export const TABLE_VARIANTS = [
|
||||
{
|
||||
label: 'Default',
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
label: 'Matrix',
|
||||
value: 'matrix',
|
||||
},
|
||||
] as const
|
||||
|
||||
export type TableVariant = (typeof TABLE_VARIANTS)[number]['value']
|
||||
|
||||
export const COLUMN_TYPES = [
|
||||
{
|
||||
label: 'Default',
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
label: 'Number',
|
||||
value: 'num',
|
||||
},
|
||||
{
|
||||
label: 'Key',
|
||||
value: 'key',
|
||||
},
|
||||
{
|
||||
label: 'Sentence',
|
||||
value: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Row header',
|
||||
value: 'rowheader',
|
||||
},
|
||||
] as const
|
||||
|
||||
export type ColumnType = (typeof COLUMN_TYPES)[number]['value']
|
||||
20
src/lib/constants/changelog.ts
Normal file
20
src/lib/constants/changelog.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const CHANGE_TYPES = [
|
||||
{
|
||||
label: 'Added',
|
||||
value: 'added',
|
||||
},
|
||||
{
|
||||
label: 'Changed',
|
||||
value: 'changed',
|
||||
},
|
||||
{
|
||||
label: 'Fixed',
|
||||
value: 'fixed',
|
||||
},
|
||||
{
|
||||
label: 'Removed',
|
||||
value: 'removed',
|
||||
},
|
||||
] as const
|
||||
|
||||
export type ChangeType = (typeof CHANGE_TYPES)[number]['value']
|
||||
14
src/lib/constants/collections.ts
Normal file
14
src/lib/constants/collections.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/* === CONTENT COLLECTIONS === */
|
||||
|
||||
export const ARTICLES = 'articles'
|
||||
export const PAGES = 'pages'
|
||||
export const TAGS = 'tags'
|
||||
|
||||
export const COLLECTION_LABELS = {
|
||||
[ARTICLES]: 'Article',
|
||||
[PAGES]: 'Page',
|
||||
[TAGS]: 'Tag',
|
||||
} as const
|
||||
|
||||
/* === SITE COLLECTIONS === */
|
||||
export const RELEASES = 'releases'
|
||||
23
src/lib/constants/figure.ts
Normal file
23
src/lib/constants/figure.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export const FIGURE_SIZES = [
|
||||
{ label: 'Inline', value: 'inline' },
|
||||
{ label: 'Wide', value: 'wide' },
|
||||
] as const
|
||||
|
||||
export const IMAGE_EFFECTS = [
|
||||
{ label: 'None', value: 'none' },
|
||||
{ label: 'Xeroxed', value: 'xeroxed' },
|
||||
] as const
|
||||
|
||||
export const ASPECT_RATIOS = [
|
||||
{ label: 'Original', value: 'original' },
|
||||
{ label: 'Panorama (21:9)', value: '21-9' },
|
||||
{ label: 'Wide (16:9)', value: '16-9' },
|
||||
{ label: 'Photo (3:2)', value: '3-2' },
|
||||
{ label: 'Classic (4:3)', value: '4-3' },
|
||||
{ label: 'Square (1:1)', value: '1-1' },
|
||||
{ label: 'Portrait (4:5)', value: '4-5' },
|
||||
] as const
|
||||
|
||||
export type FigureSize = (typeof FIGURE_SIZES)[number]['value']
|
||||
export type ImageEffect = (typeof IMAGE_EFFECTS)[number]['value']
|
||||
export type AspectRatio = (typeof ASPECT_RATIOS)[number]['value']
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user