diff --git a/astro.config.mjs b/astro.config.mjs index 47c92c4..36f401f 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -4,14 +4,22 @@ import markdoc from '@astrojs/markdoc'; import keystatic from '@keystatic/astro'; import node from '@astrojs/node'; - +import preact from '@astrojs/preact'; // https://astro.build/config export default defineConfig({ output: 'static', - integrations: [react(), markdoc(), keystatic()], + integrations: [react(), markdoc(), keystatic(), preact()], adapter: node({ mode: 'standalone', }), -}); + vite: { + ssr: { + noExternal: ['lodash'], + }, + optimizeDeps: { + include: ['react', 'react-dom', 'react/jsx-runtime'], + }, + }, +}); \ No newline at end of file diff --git a/keystatic.config.ts b/keystatic.config.ts index c902ebc..7fcb3af 100644 --- a/keystatic.config.ts +++ b/keystatic.config.ts @@ -1,13 +1,21 @@ import { config } from '@keystatic/core'; import { articles } from './src/keystatic/collections/articles'; import { pages } from './src/keystatic/collections/pages'; +import crucibleCollections from './src/keystatic/collections/crucible'; export default config({ storage: { kind: 'local', }, + ui: { + navigation: { + General: ['articles', 'pages'], + Crucible: ['cr_elements'], + }, + }, collections: { articles, pages, + ...crucibleCollections, }, }); diff --git a/markdoc.config.mjs b/markdoc.config.mjs new file mode 100644 index 0000000..faa11db --- /dev/null +++ b/markdoc.config.mjs @@ -0,0 +1,14 @@ +import { defineMarkdocConfig, component } from '@astrojs/markdoc/config'; + +export default defineMarkdocConfig({ + tags: { + ElementSymbol: { + render: component('./src/components/content/ElementSymbol.astro'), + attributes: { + element: { type: String }, + size: { type: String }, + color: { type: String }, + }, + }, + }, +}); diff --git a/package-lock.json b/package-lock.json index d556e0c..53b5d4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,15 +11,18 @@ "dependencies": { "@astrojs/markdoc": "^0.12.9", "@astrojs/node": "^9.5.4", + "@astrojs/preact": "^4.1.3", "@astrojs/react": "^4.2.0", "@fontsource-variable/geist": "^5.2.8", "@fontsource-variable/geist-mono": "^5.2.7", "@fontsource/blaka": "^5.2.7", + "@keystar/ui": "^0.7.19", "@keystatic/astro": "^5.0.6", "@keystatic/core": "^0.5.48", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "astro": "^5.2.5", + "preact": "^10.28.4", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -29,11 +32,16 @@ "@typescript-eslint/parser": "^8.56.0", "eslint": "^10.0.0", "eslint-plugin-astro": "^1.6.0", + "postcss-html": "^1.8.1", "postcss-import": "^16.1.1", + "postcss-mixins": "^12.1.2", "postcss-preset-env": "^11.1.3", "prettier": "^3.8.1", "prettier-plugin-astro": "^0.14.1", "stylelint": "^17.3.0", + "stylelint-config-astro": "^2.0.0", + "stylelint-config-clean-order": "^8.0.1", + "stylelint-config-html": "^1.1.0", "stylelint-config-standard": "^40.0.0", "stylelint-order": "^7.0.1" } @@ -134,6 +142,24 @@ "integrity": "sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA==", "license": "MIT" }, + "node_modules/@astrojs/preact": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@astrojs/preact/-/preact-4.1.3.tgz", + "integrity": "sha512-Ph416wbgyumkmYr7erZ83l/d+LXdZethlHRRCbgoKEn8wo3Rkq13shKFp0QYXYSDYxVaA6UBdkdimeowy/lMLQ==", + "license": "MIT", + "dependencies": { + "@preact/preset-vite": "^2.10.2", + "@preact/signals": "^2.3.1", + "preact-render-to-string": "^6.6.1", + "vite": "^6.4.1" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + }, + "peerDependencies": { + "preact": "^10.6.5" + } + }, "node_modules/@astrojs/prism": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.2.0.tgz", @@ -259,6 +285,18 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", @@ -387,6 +425,55 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-jsx": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", @@ -3667,6 +3754,121 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@preact/preset-vite": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.3.tgz", + "integrity": "sha512-1SiS+vFItpkNdBs7q585PSAIln0wBeBdcpJYbzPs1qipsb/FssnkUioNXuRsb8ZnU8YEQHr+3v8+/mzWSnTQmg==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@prefresh/vite": "^2.4.11", + "@rollup/pluginutils": "^5.0.0", + "babel-plugin-transform-hook-names": "^1.0.2", + "debug": "^4.4.3", + "picocolors": "^1.1.1", + "vite-prerender-plugin": "^0.5.8" + }, + "peerDependencies": { + "@babel/core": "7.x", + "vite": "2.x || 3.x || 4.x || 5.x || 6.x || 7.x" + } + }, + "node_modules/@preact/signals": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/@preact/signals/-/signals-2.8.1.tgz", + "integrity": "sha512-wX6U0SpcCukZTJBs5ChljvBZb3XmYzA5gd4vKHgX8wZZKaQCo2WHtmThdLx+mcVvlBa5u3XShC7ffbETJD4BiQ==", + "license": "MIT", + "dependencies": { + "@preact/signals-core": "^1.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact": ">= 10.25.0 || >=11.0.0-0" + } + }, + "node_modules/@preact/signals-core": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.13.0.tgz", + "integrity": "sha512-slT6XeTCAbdql61GVLlGU4x7XHI7kCZV5Um5uhE4zLX4ApgiiXc0UYFvVOKq06xcovzp7p+61l68oPi563ARKg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@prefresh/babel-plugin": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@prefresh/babel-plugin/-/babel-plugin-0.5.3.tgz", + "integrity": "sha512-57LX2SHs4BX2s1IwCjNzTE2OJeEepRCNf1VTEpbNcUyHfMO68eeOWGDIt4ob9aYlW6PEWZ1SuwNikuoIXANDtQ==", + "license": "MIT" + }, + "node_modules/@prefresh/core": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/@prefresh/core/-/core-1.5.9.tgz", + "integrity": "sha512-IKBKCPaz34OFVC+adiQ2qaTF5qdztO2/4ZPf4KsRTgjKosWqxVXmEbxCiUydYZRY8GVie+DQlKzQr9gt6HQ+EQ==", + "license": "MIT", + "peerDependencies": { + "preact": "^10.0.0 || ^11.0.0-0" + } + }, + "node_modules/@prefresh/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@prefresh/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==", + "license": "MIT" + }, + "node_modules/@prefresh/vite": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@prefresh/vite/-/vite-2.4.12.tgz", + "integrity": "sha512-FY1fzXpUjiuosznMV0YM7XAOPZjB5FIdWS0W24+XnlxYkt9hNAwwsiKYn+cuTEoMtD/ZVazS5QVssBr9YhpCQA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.22.1", + "@prefresh/babel-plugin": "^0.5.2", + "@prefresh/core": "^1.5.0", + "@prefresh/utils": "^1.2.0", + "@rollup/pluginutils": "^4.2.1" + }, + "peerDependencies": { + "preact": "^10.4.0 || ^11.0.0-0", + "vite": ">=2.0.0" + } + }, + "node_modules/@prefresh/vite/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@prefresh/vite/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@prefresh/vite/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@react-aria/actiongroup": { "version": "3.7.23", "resolved": "https://registry.npmjs.org/@react-aria/actiongroup/-/actiongroup-3.7.23.tgz", @@ -7369,6 +7571,15 @@ "npm": ">=6" } }, + "node_modules/babel-plugin-transform-hook-names": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz", + "integrity": "sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==", + "license": "MIT", + "peerDependencies": { + "@babel/core": "^7.12.10" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -7528,6 +7739,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001770", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", @@ -9333,6 +9554,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/hookified": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", @@ -9832,6 +10062,12 @@ "dev": true, "license": "MIT" }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -11170,6 +11406,16 @@ "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", "license": "MIT" }, + "node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, "node_modules/node-mock-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", @@ -11923,6 +12169,79 @@ "postcss": "^8.4" } }, + "node_modules/postcss-html": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.8.1.tgz", + "integrity": "sha512-OLF6P7qctfAWayOhLpcVnTGqVeJzu2W3WpIYelfz2+JV5oGxfkcEvweN9U4XpeqE0P98dcD9ssusGwlF0TK0uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "htmlparser2": "^8.0.0", + "js-tokens": "^9.0.0", + "postcss": "^8.5.0", + "postcss-safe-parser": "^6.0.0" + }, + "engines": { + "node": "^12 || >=14" + } + }, + "node_modules/postcss-html/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/postcss-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/postcss-html/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-html/node_modules/postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, "node_modules/postcss-image-set-function": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-8.0.0.tgz", @@ -11968,6 +12287,32 @@ "postcss": "^8.0.0" } }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, "node_modules/postcss-lab-function": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-8.0.1.tgz", @@ -12024,6 +12369,35 @@ "postcss": "^8.4" } }, + "node_modules/postcss-mixins": { + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/postcss-mixins/-/postcss-mixins-12.1.2.tgz", + "integrity": "sha512-90pSxmZVfbX9e5xCv7tI5RV1mnjdf16y89CJKbf/hD7GyOz1FCxcYMl8ZYA8Hc56dbApTKKmU9HfvgfWdCxlwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-js": "^4.0.1", + "postcss-simple-vars": "^7.0.1", + "sugarss": "^5.0.0", + "tinyglobby": "^0.2.14" + }, + "engines": { + "node": "^20.0 || ^22.0 || >=24.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, "node_modules/postcss-nesting": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-14.0.0.tgz", @@ -12337,6 +12711,23 @@ "node": ">=4" } }, + "node_modules/postcss-simple-vars": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-simple-vars/-/postcss-simple-vars-7.0.1.tgz", + "integrity": "sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.1" + } + }, "node_modules/postcss-sorting": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-9.1.0.tgz", @@ -12372,6 +12763,25 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/preact": { + "version": "10.28.4", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz", + "integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.6.6.tgz", + "integrity": "sha512-EfqZJytnjJldV+YaaqhthU2oXsEf5e+6rDv957p+zxAvNfFLQOPfvBOTncscQ+akzu6Wrl7s3Pa0LjUQmWJsGQ==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10 || >= 11.0.0-0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -13205,6 +13615,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-code-frame": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/simple-code-frame/-/simple-code-frame-1.3.0.tgz", + "integrity": "sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==", + "license": "MIT", + "dependencies": { + "kolorist": "^1.6.0" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -13364,6 +13783,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stack-trace": { + "version": "1.0.0-pre2", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0-pre2.tgz", + "integrity": "sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -13483,6 +13911,48 @@ "node": ">=20.19.0" } }, + "node_modules/stylelint-config-astro": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-astro/-/stylelint-config-astro-2.0.0.tgz", + "integrity": "sha512-BSl+wNEa3h1+GhHAfI3WO/fPylcVoePLIMd+JX1hz1Pt2cnqRswjfA4EqD6Wy2DqrariqYJE1xXZCnuJNrjb8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "typescript": "^5.9.3" + }, + "peerDependencies": { + "postcss-html": "^1.0.0", + "stylelint": ">=14.0.0" + } + }, + "node_modules/stylelint-config-clean-order": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/stylelint-config-clean-order/-/stylelint-config-clean-order-8.0.1.tgz", + "integrity": "sha512-zKjp7BiINXRZOG9m0fE/6UKoM6clPekL+LoAiHMCiQU2hgirKL5G0mKc5Z0ygIhQXfb1+DTRDM0mu6Ecdv4q8g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "stylelint": ">=16", + "stylelint-order": ">=6" + } + }, + "node_modules/stylelint-config-html": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stylelint-config-html/-/stylelint-config-html-1.1.0.tgz", + "integrity": "sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12 || >=14" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "postcss-html": "^1.0.0", + "stylelint": ">=14.0.0" + } + }, "node_modules/stylelint-config-recommended": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-18.0.0.tgz", @@ -13651,6 +14121,29 @@ "s.color": "0.0.15" } }, + "node_modules/sugarss": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-5.0.1.tgz", + "integrity": "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw==", + "devOptional": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, "node_modules/superstruct": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz", @@ -13995,7 +14488,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14492,6 +14984,32 @@ } } }, + "node_modules/vite-prerender-plugin": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/vite-prerender-plugin/-/vite-prerender-plugin-0.5.12.tgz", + "integrity": "sha512-EiwhbMn+flg14EysbLTmZSzq8NGTxhytgK3bf4aGRF1evWLGwZiHiUJ1KZDvbxgKbMf2pG6fJWGEa3UZXOnR1g==", + "license": "MIT", + "dependencies": { + "kolorist": "^1.8.0", + "magic-string": "0.x >= 0.26.0", + "node-html-parser": "^6.1.12", + "simple-code-frame": "^1.3.0", + "source-map": "^0.7.4", + "stack-trace": "^1.0.0-pre2" + }, + "peerDependencies": { + "vite": "5.x || 6.x || 7.x" + } + }, + "node_modules/vite-prerender-plugin/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/vitefu": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", diff --git a/package.json b/package.json index af45212..b0d7fee 100644 --- a/package.json +++ b/package.json @@ -10,15 +10,18 @@ "dependencies": { "@astrojs/markdoc": "^0.12.9", "@astrojs/node": "^9.5.4", + "@astrojs/preact": "^4.1.3", "@astrojs/react": "^4.2.0", "@fontsource-variable/geist": "^5.2.8", "@fontsource-variable/geist-mono": "^5.2.7", "@fontsource/blaka": "^5.2.7", + "@keystar/ui": "^0.7.19", "@keystatic/astro": "^5.0.6", "@keystatic/core": "^0.5.48", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "astro": "^5.2.5", + "preact": "^10.28.4", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -35,11 +38,16 @@ "@typescript-eslint/parser": "^8.56.0", "eslint": "^10.0.0", "eslint-plugin-astro": "^1.6.0", + "postcss-html": "^1.8.1", "postcss-import": "^16.1.1", + "postcss-mixins": "^12.1.2", "postcss-preset-env": "^11.1.3", "prettier": "^3.8.1", "prettier-plugin-astro": "^0.14.1", "stylelint": "^17.3.0", + "stylelint-config-astro": "^2.0.0", + "stylelint-config-clean-order": "^8.0.1", + "stylelint-config-html": "^1.1.0", "stylelint-config-standard": "^40.0.0", "stylelint-order": "^7.0.1" } diff --git a/postcss.config.mjs b/postcss.config.mjs index 6a5a932..f62a830 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,11 +1,15 @@ import postcssGlobalData from '@csstools/postcss-global-data'; import postcssImport from 'postcss-import'; import postcssPresetEnv from 'postcss-preset-env'; +import postcssMixins from 'postcss-mixins'; export default { plugins: [ postcssGlobalData({ - files: ['./src/styles/custom-media.css'], + files: ['./src/styles/base/custom-media.css'], + }), + postcssMixins({ + mixinsDir: './src/styles/mixins/', }), postcssImport(), postcssPresetEnv({ diff --git a/src/components/content/ElementSymbol.astro b/src/components/content/ElementSymbol.astro new file mode 100644 index 0000000..160a42c --- /dev/null +++ b/src/components/content/ElementSymbol.astro @@ -0,0 +1,72 @@ +--- +import { getEntry } from 'astro:content'; + +interface Props { + element: string; + size: string; + color: string; +} + +interface FontSymbol { + discriminant: 'font'; + value: { + family: string; + character: string; + }; +} + +interface SVGSymbol { + discriminant: 'svg'; + value: string; +} + +type Symbol = FontSymbol | SVGSymbol; + +const { element, size, color } = Astro.props; +const entry = await getEntry('elements', element); + +if (!entry) { + console.warn(`Element not found: ${element}`); +} + +const symbol = entry?.data.symbol as Symbol | undefined; +--- + +{ + symbol?.discriminant === 'font' && ( + + {(symbol as FontSymbol).value.character} + + ) +} + +{ + symbol?.discriminant === 'svg' && ( + + ) +} + + diff --git a/src/components/layout/CMDPalette/CMDPalette.module.css b/src/components/layout/CMDPalette/CMDPalette.module.css new file mode 100644 index 0000000..994fce2 --- /dev/null +++ b/src/components/layout/CMDPalette/CMDPalette.module.css @@ -0,0 +1,211 @@ +.trigger { + @mixin ml auto; + + cursor: pointer; + + display: flex; + gap:var(--ui-spacing-comfortable); + align-items: center; + + padding: var(--ui-spacing-snug) var(--ui-spacing-spacious); + border: var(--size-px) solid var(--color-border-normal); + + font-family: var(--font-mono); + font-size: var(--ui-typo-size-md); + color: var(--text-color-disabled); + + background: var(--color-palette-charcoal-gray); + + transition: border-color 0.15s, color 0.15s; + + &:hover { + border-color: var(--color-text-inverse); + color: var(--color-text-inverse); + } + + & .kbd { + padding: var(--ui-spacing-hairline) var( --ui-spacing-cozy); + border: var(--size-px) solid var(--color-border-normal); + border-radius: var(--size-05); + + font-family: var(--font-mono); + font-size: var(--ui-typo-size-xs); + + background: var(--color-surface-inverse); + } +} + +.backdrop { + position: fixed; + z-index: 99; + inset: 0; + background: var(--color-overlay-heavy); +} + +.palette { + position: fixed; + z-index: 100; + top: 20%; + left: 50%; + transform: translateX(-50%); + + width: min(var(--size-128), 90vw); + padding: var(--ui-spacing-tight); + border: var(--size-05) solid var(--color-primary); + + background: var(--color-surface-inverse); + box-shadow: + 0 0 0 var(--size-px) var(--color-border-strong), + var(--size-1) var(--size-1) 0 var(--color-surface-inverse); +} + +.header { + @mixin px var(--ui-spacing-generous); + @mixin border-b var(--size-px), solid, var(--color-border-strong); + + display: flex; + gap: var(--ui-spacing-comfortable); + align-items: center; + + & .icon { + font-family: var(--font-mono); + font-size: var(--ui-typo-size-lg); + font-weight: 900; + color: var(--color-text-disabled); + } + + & .input { + @mixin py var(--ui-spacing-generous); + + flex: 1; + + border: none; + + font-family: var(--font-mono); + font-size: var(--ui-typo-size-lg); + color: var(--color-text-inverse); + + background: transparent; + outline: none; + + &::placeholder { + color: var(--color-text-disabled); + } + } + + & .esc { + font-family: var(--font-mono); + font-size: var(--typo-size-xs); + color: var(--color-text-disabled); + } +} + +.results { + overflow-y: auto; + max-height: var(--size-96); + + & .groupLabel { + padding: var(--ui-spacing-relaxed) var(--ui-spacing-generous) var(--ui-spacing-snug); + + font-size: var(--ui-typo-size-2xs); + font-weight: 700; + color: var(--color-text-disabled); + text-transform: uppercase; + letter-spacing: var(--spacing-loosest); + } + + & .result { + cursor: pointer; + + display: flex; + gap: var(--ui-spacing-relaxed); + align-items: center; + + padding: var(--ui-spacing-comfortable) var(--ui-spacing-generous); + + font-family: var(--font-mono); + font-size: var(--ui-typo-size-md); + color: var(--color-text-inverse); + text-decoration: none; + text-transform: uppercase; + letter-spacing: var(--typo-spacing-relaxed); + + transition: background 0.8s; + + & .type { + min-width: var(--size-16); + + font-size: var(--ui-typo-size-2xs); + color: var(--color-text-disabled); + text-align: right; + letter-spacing: var(--typo-spacing-looser); + } + + & .label { + flex: 1; + } + + & .path { + font-size: var(--ui-typo-size-2xs); + color: var(--color-text-disabled); + text-transform: none; + letter-spacing: 0; + } + + & .arrow { + font-size: var(--ui-typo-size-xs); + color: var(--color-text-disabled); + opacity: 0; + transition: opacity 0.1s; + } + + &:hover, + &.selected { + background: var(--color-border-strong); + + & .arrow { + opacity: 1; + } + } + } +} + +.empty { + padding: var(--ui-spacing-luxurious) var(--ui-spacing-generous); + + font-size: var(--ui-typo-size-sm); + color: var(--color-text-disabled); + text-align: center; + text-transform: uppercase; + letter-spacing: var(--typo-spacing-comfortable); +} + +.footer { + @mixin border-t var(--size-px), solid, var(--color-border-strong); + + display: flex; + align-items: center; + justify-content: space-between; + + padding: var(--ui-spacing-comfortable) var(--ui-spacing-generous); + + font-size: var(--ui-typo-size-xs); + color: var(--color-text-disabled); + + & .group { + display: flex; + gap: var(--ui-spacing-relaxed); + align-items: center; + } + + & kbd { + @mixin mx var(--ui-spacing-tight); + + padding: var(--ui-spacing-tight) var(--ui-spacing-snug); + border: var(--size-px) solid var(--color-border-normal); + border-radius: var(--size-05); + + font-family: var(--font-mono); + font-size: var(--ui-typo-size-2xs); + } +} diff --git a/src/components/layout/CMDPalette/index.tsx b/src/components/layout/CMDPalette/index.tsx new file mode 100644 index 0000000..2e57d23 --- /dev/null +++ b/src/components/layout/CMDPalette/index.tsx @@ -0,0 +1,287 @@ +import { + useState, + useEffect, + useRef, + useCallback, + useMemo, +} from 'preact/hooks'; +import type { PaletteEntry, ContentType } from '@lib/types/content'; +import styles from './CMDPalette.module.css'; + +/* CONSTANTS */ +const MAX_DEFAULT = 20; +const MAX_SEARCH = 30; + +const TYPE_LABELS: Record = { + article: 'Article', + element: 'Element', + page: 'Page', +}; + +/* INDEX ENTRY */ +interface IndexedEntry extends PaletteEntry { + _label: string; + _parent: string; + _type: string; + _path: string; +} + +const normalize = (str: string): string => + str.toLowerCase().replace(/[^a-z0-9]/g, ''); + +const indexEntry = (entry: PaletteEntry): IndexedEntry => ({ + ...entry, + _label: normalize(entry.label), + _parent: normalize(entry.parent ?? ''), + _type: normalize(entry.type), + _path: normalize(entry.path), +}); + +/* SCORING */ +const scoreEntry = (entry: IndexedEntry, q: string): number => { + let score = 0; + + if (entry._label === q) score += 100; + else if (entry._label.startsWith(q)) score += 80; + else if (entry._label.includes(q)) score += 60; + if (entry._parent.includes(q)) score += 30; + if (entry._type.includes(q)) score += 20; + if (entry._path.includes(q)) score += 10; + + return score; +}; + +/* RENDERABLE ROW – single-pass from filtered entries */ +interface RenderRow { + kind: 'label' | 'result'; + key: string; + group?: string; + entry?: PaletteEntry; + index?: number; +} + +const buildRows = (entries: PaletteEntry[]): RenderRow[] => { + const rows: RenderRow[] = []; + let currentGroup = ''; + let idx = 0; + + for (const entry of entries) { + const group = entry.parent ?? TYPE_LABELS[entry.type] ?? 'Other'; + + if (group !== currentGroup) { + currentGroup = group; + rows.push({ kind: 'label', key: `label-${group}`, group }); + } + rows.push({ + kind: 'result', + key: entry.path, + entry, + index: idx++, + }); + } + return rows; +}; + +/* COMPONENT */ +export default function CommandPalette() { + const [isOpen, setIsOpen] = useState(false); + const [query, setQuery] = useState(''); + const [index, setIndex] = useState([]); + const [selectedIndex, setSelectedIndex] = useState(-1); + + const selectedRef = useRef(-1); + const loadingRef = useRef(false); + const inputRef = useRef(null); + const resultsRef = useRef(null); + + /* Keep ref in sync for stable access in callbacks */ + useEffect(() => { + selectedRef.current = selectedIndex; + }, [selectedIndex]); + + /* Load index (ref-guarded, no race conditions) */ + const loadIndex = useCallback(async () => { + if (index.length > 0 || loadingRef.current) return; + loadingRef.current = true; + + try { + const res = await fetch('/palette-index.json'); + const data: PaletteEntry[] = await res.json(); + setIndex(data.map(indexEntry)); + } catch (err) { + console.error('Failed to load palette index:', err); + loadingRef.current = false; + } + }, [index.length]); + + /* OPEN / CLOSE */ + const open = useCallback(async () => { + await loadIndex(); + setIsOpen(true); + setQuery(''); + setSelectedIndex(-1); + }, [loadIndex]); + + const close = useCallback(() => { + setIsOpen(false); + setSelectedIndex(-1); + }, []); + + /* GLOBAL KEYBOARD SHORTCUTS */ + useEffect(() => { + const onKeydown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault(); + isOpen ? close() : open(); + } + if (e.key === 'Escape' && isOpen) { + e.preventDefault(); + close(); + } + }; + document.addEventListener('keydown', onKeydown); + return () => document.removeEventListener('keydown', onKeydown); + }, [isOpen, open, close]); + + /* FOCUS INPUT WHEN OPENING*/ + useEffect(() => { + if (isOpen) requestAnimationFrame(() => inputRef.current?.focus()); + }, [isOpen]); + + /* FILTERED ENTRIES -> RENDER ROWS */ + const filtered = useMemo(() => { + if (!query.trim()) { + return index.filter((e) => e.depth <= 2).slice(0, MAX_DEFAULT); + } + const q = normalize(query); + return index + .map((entry) => ({ entry, score: scoreEntry(entry, q) })) + .filter((r) => r.score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, MAX_SEARCH) + .map((r) => r.entry); + }, [index, query]); + + const rows = useMemo(() => buildRows(filtered), [filtered]); + + /* COUNT OF NAVIGABLE RESULTS (excludes labels) */ + const resultCount = useMemo( + () => rows.filter((r) => r.kind === 'result').length, + [rows], + ); + + /* RESET SELECTION ON QUERY CHANGE */ + useEffect(() => { + setSelectedIndex(-1); + }, [query]); + + /*SCROLL SELECTED INTO VIEW */ + useEffect(() => { + if (selectedIndex < 0) return; + resultsRef.current + ?.querySelector(`[data-index="${selectedIndex}"]`) + ?.scrollIntoView({ block: 'nearest' }); + }, [selectedIndex]); + + /* INPUT KEYBOARD NAVIGATION */ + const onInputKeydown = useCallback( + (e: KeyboardEvent) => { + if (!resultCount) return; + if (e.key === 'ArrowDown') { + e.preventDefault(); + setSelectedIndex((i) => (i + 1) % resultCount); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setSelectedIndex((i) => (i <= 0 ? resultCount - 1 : i - 1)); + } else if (e.key === 'Enter') { + e.preventDefault(); + const idx = selectedRef.current; + const target = rows.find((r) => r.kind === 'result' && r.index === idx); + if (target?.entry) { + window.location.href = target.entry.path; + } + } + }, + [resultCount, rows], + ); + + /* RENDER */ + return ( + <> + + {isOpen &&
} + {isOpen && ( + + )} + + ); +} diff --git a/src/components/layout/MastHead.astro b/src/components/layout/MastHead.astro new file mode 100644 index 0000000..769b405 --- /dev/null +++ b/src/components/layout/MastHead.astro @@ -0,0 +1,129 @@ +--- +import CommandPalette from "./CMDPalette" +--- + + + + diff --git a/src/components/layout/PostHeader/Cover.astro b/src/components/layout/PostHeader/Cover.astro new file mode 100644 index 0000000..5cb5a19 --- /dev/null +++ b/src/components/layout/PostHeader/Cover.astro @@ -0,0 +1,75 @@ +--- +import { Image } from 'astro:assets'; +interface Props { + src: string; + alt: string; + caption?: string; +} +const { src, alt, caption } = Astro.props; + +const images = import.meta.glob<{ default: ImageMetadata }>( + '/src/content/**/cover/*.{png,jpg,jpeg,webp,avif}', + { eager: true }, +); + +const imagePath = `/src${src}`; +const image = images[imagePath]?.default; +--- + +
+ { + image ? ( + {alt} + ) : ( + {alt} + ) + } +
+
+ { + caption ? ( + {caption} + ) : ( + {src} + ) + } +
+
+
+ + diff --git a/src/components/layout/PostHeader/Meta.astro b/src/components/layout/PostHeader/Meta.astro new file mode 100644 index 0000000..2ec58a2 --- /dev/null +++ b/src/components/layout/PostHeader/Meta.astro @@ -0,0 +1,78 @@ +--- +interface Props { + tags: string[]; + updateDate?: string; +} + +const { tags, updateDate } = Astro.props; +--- + +
+
+ Author + Dave Damage +
+ { + tags && tags.length > 0 && ( +
+ Tags +
    + {tags.map((tag) => ( +
  • {tag}
  • + ))} +
+
+ ) + } + { + updateDate && ( +
+ Last Update + +
+ ) + } +
+ diff --git a/src/components/layout/PostHeader/Overline.astro b/src/components/layout/PostHeader/Overline.astro new file mode 100644 index 0000000..2b22d05 --- /dev/null +++ b/src/components/layout/PostHeader/Overline.astro @@ -0,0 +1,68 @@ +--- +import type { BreadcrumbSegment } from '@lib/types/content'; + +interface Props { + breadcrumbs: BreadcrumbSegment[]; + publicationDate: string; +} + +const { breadcrumbs, publicationDate } = Astro.props; +--- + +
+
+ + +
+
+ + diff --git a/src/components/layout/PostHeader/Title.astro b/src/components/layout/PostHeader/Title.astro new file mode 100644 index 0000000..04f918e --- /dev/null +++ b/src/components/layout/PostHeader/Title.astro @@ -0,0 +1,38 @@ +--- +interface Props { + title: string; + subtitle?: string; +} + +const { title, subtitle } = Astro.props; + +console.log(subtitle); +--- + +
+

{title}

+ {subtitle &&

{subtitle}

} +
+ + diff --git a/src/components/layout/PostHeader/index.astro b/src/components/layout/PostHeader/index.astro new file mode 100644 index 0000000..6ff3807 --- /dev/null +++ b/src/components/layout/PostHeader/index.astro @@ -0,0 +1,46 @@ +--- +import type { BreadcrumbSegment } from '@lib/types/content'; +import Overline from './Overline.astro'; +import Title from './Title.astro'; +import Meta from './Meta.astro'; +import Cover from './Cover.astro'; + +interface Props { + title: string; + cover?: { + src: string; + alt: string; + caption: string; + showInHeader: boolean; + }; + subtitle?: string; + tags: string[]; + publishDate: string; + updateDate?: string; + breadcrumbs: BreadcrumbSegment[]; +} + +const { title, cover, subtitle, tags, publishDate, updateDate, breadcrumbs } = + Astro.props; +const showCover = cover?.src && cover?.showInHeader; + +console.log(Astro.props); +--- + +
+ + + { + showCover && ( + <Cover alt={cover.alt} src={cover.src} caption={cover?.caption} /> + ) + } + <Meta tags={tags} updateDate={updateDate} /> +</header> + +<style> + .wrapper { + @mixin border-b 8px, solid, var(--color-border-strong); + background-color: var(--color-surface-base); + } +</style> diff --git a/src/content/articles/alchemical-materialism/cover/src.png b/src/content/articles/alchemical-materialism/cover/src.png new file mode 100644 index 0000000..a380b98 Binary files /dev/null and b/src/content/articles/alchemical-materialism/cover/src.png differ diff --git a/src/content/articles/alchemical-materialism/index.mdoc b/src/content/articles/alchemical-materialism/index.mdoc new file mode 100644 index 0000000..b88c106 --- /dev/null +++ b/src/content/articles/alchemical-materialism/index.mdoc @@ -0,0 +1,309 @@ +--- +title: Alchemical Materialism +subtitle: A Systematic Framework for Worldbuilding Through Elemental Combination +summary: A Systematic Framework for Worldbuilding Through Elemental Combination +cover: + src: /content/articles/alchemical-materialism/cover/src.png + alt: Man approach a volcano + caption: The Eshian God-Alchemists claimed this was the face of Monad + showInHeader: true +publishDate: 2026-02-20T11:17:00.000Z +status: published +isFeatured: false +parent: framework +tags: + - Was ist Was? + - Framework + - Worldbuilding +relatedArticles: [] +seo: + noIndex: false +--- +## Overview + +- **Foundational system** of the crucible +- Generates **entities** – cultures, religions, states, vessels, magical traditions – through **systematic elemental combination,** not arbitrary assignment +- Two interlocking element sets + - **Three Primes =>** Soul, Body, Spirit; dynamic forces in tension + - **Five Essences =>** Earth, Fire, Water, Air, Aether; multivalent elemental character +- **Elemental Pool Mechanic =>** add tokens from material conditions and history, tally, spend on capabilities, traits, and relationships +- **One Engine =>** elemental pool drives everything; capabilities, identity, and cultural character all flow from the same source + +## The Three Primes + +- *Dynamic Forces in Tension* within each entity +- Correspond to the three alchemical principles observed when substance is placed in the crucible: + - Something **burns** (Sulfur) + - Something **remains** (Salt) + - Something **transforms** (Mercury) +- Each entity contains all three; dominance determines character. +- Material conditions determine which Prime dominates; not cultural preference or collective personality +- **Interdependence:** + - Without **Soul,** nothing starts; structure sits inert, nothing transforms + - Without **Body,** nothing holds; fire has no fuel, transforming has no material + - Without **Spirit,** nothing changes; fire burns the same thing forever, structure never adapts + +{% table %} +- Prime +- Glyph +- Alchemical +- Core +- Keyword +- Organization +- Failure +--- +- [Body](/the-crucible/references/elements/body) +- {% ElementSymbol + element="body" + size="var(--typo-size-2xl)" + color="inherit" /%} +- Salt +- Structure; the form +- »It endures« +- Institutional strength +- Calcifies everything +--- +- [Soul](/the-crucible/references/elements/soul) +- {% ElementSymbol + element="soul" + size="var(--typo-size-2xl)" + color="inherit" /%} +- Sulfur +- Agency; the Spark +- »I burn« +- Individual excellence +- Consumes everything +--- +- [Spirit](/the-crucible/references/elements/spirit) +- {% ElementSymbol + element="spirit" + size="var(--typo-size-2xl)" + color="inherit" /%} +- Mercury +- Transformation; the flux +- »Nothing stays« +- Adaptive innovation +- Dissolves everything +{% /table %} + +## The Five Essences + +- Describe the **material character** of an entity +- Operates on two tiers simultaneously + - **Character =>** Material expression; how the essence manifests across any domain of society + - **Symbolic =>** Thematic resonance; enriches religion, mythology, cultural flavour +- Each essence has a **primary capability affinity** and two **secondary affinities;** connecting them to the five universal capability tracks +- Essences are multivalent → it can express across multiple domains but it has a home + +{% table %} +- Essence +- Symbol +- Properties +- IS +- Image +--- +- [Aether](/the-crucible/references/elements/aether) +- {% ElementSymbol + element="aether" + size="var(--typo-size-2xl)" + color="inherit" /%} +- Quintessence +- Transcendent, ordered, numinous +- The Stars +--- +- [Air](/the-crucible/references/elements/air) +- {% ElementSymbol element="air" size="var(--typo-size-2xl)" color="inherit" /%} +- Hot & Wet +- Invisible, expansive, permeating +- The Storm +--- +- [Earth](/the-crucible/references/elements/earth) +- {% ElementSymbol + element="earth" + size="var(--typo-size-2xl)" + color="inherit" /%} +- Cold & Dry +- Heavy, material, foundational +- The Mountain +--- +- [Fire](/the-crucible/references/elements/fire) +- {% ElementSymbol + element="fire" + size="var(--typo-size-2xl)" + color="inherit" /%} +- Hot & Dry +- Bright, consuming, refining +- The Volcano +--- +- [Water](/the-crucible/references/elements/water) +- {% ElementSymbol + element="water" + size="var(--typo-size-2xl)" + color="inherit" /%} +- Cold & Wet +- Flowing, deep, dissolving +- The River +{% /table %} + +### The Five Capability Tracks + +{% table %} +- Domain +- Covers +--- +- **Prosperity** +- Labor, production, construction, infrastructure +--- +- **Warfare** +- Armies, defense, fortifications, armaments +--- +- **Statecraft** +- Governance, administration, law, diplomacy +--- +- **Lore** +- Knowledge, education, medicine, philosophy, sciences +--- +- **Rites** +- Religion, ritual, sacred practice, arcane arts, cosmology +{% /table %} + +#### Essence-to-Capability Affinity + +{% table %} +- Essence +- Primary +- Secondary 1 +- Secondary 2 +--- +- **Aether** +- Rites +- Statecraft +- Lore +--- +- **Air** +- Lore +- Prosperity +- Warfare +--- +- **Earth** +- Prosperity +- Warfare +- Rites +--- +- **Fire** +- Warfare +- Lore +- Statecraft +--- +- **Water** +- Statecraft +- Rites +- Prosperity +{% /table %} + +### The Transformation Triangle + +- Three elements involve Transformation → distinguished by mode + - **Fire =>** Transformation is *destructive –* purification through consumption + - **Water =>** Transformation is *gradual* – erosion, dissolution, blending + - **Spirit =>** Transformation is *synthetic* – *solve et coagula,* a new thing born from recombination + +{% table %} +- Element +- What happens +- Process +- Image +- Reversible +--- +- **Fire** +- Destroys to create +- Input consumed; new thing exists +- The Forge +- No – Ore is gone, steel remains +--- +- **Water** +- Dissolves to mix +- Boundaries erode; things blend +- The Solvent +- Partially – Salt in Water is still salt-and-water +--- +- **Spirit** +- Recombines to become +- Inputs loose identity; genuinely new things emerge +- The Alembic +- No – the product is neither ingredient +{% /table %} + +## The Seven Aspects + +- Provide additional specificity +- Corresponding to the 7 classical metals and their planetary associations +- Categorise what exists and operates within the World + +{% table %} +- Aspects +- Symbol +- Metal +- Planet +- Association +--- +- *Entities* +- {% ElementSymbol + element="entities" + size="var(--typo-size-2xl)" + color="inherit" /%} +- Silver +- Moon +- Living beings; flesh, beasts, plants, spirits +--- +- *Matter* +- {% ElementSymbol + element="matter" + size="var(--typo-size-2xl)" + color="inherit" /%} +- Lead +- Saturn +- Physical substances; materials, terrain, stone, foundations +--- +- *Mind* +- {% ElementSymbol + element="mind" + size="var(--typo-size-2xl)" + color="inherit" /%} +- Quicksilver +- Mercury +- Thought, consciousness; intellect, emotion, skills +--- +- *Society* +- {% ElementSymbol + element="society" + size="var(--typo-size-2xl)" + color="inherit" /%} +- Tin +- Jupiter +- Collective organisation; governance, law, institutions +--- +- *Art* +- {% ElementSymbol element="art" size="var(--typo-size-2xl)" color="inherit" /%} +- Copper +- Venus +- Creation, expression; artistry, rituals, craft +--- +- *Mysteries* +- {% ElementSymbol + element="mysteries" + size="var(--typo-size-2xl)" + color="inherit" /%} +- Gold +- Sun +- Hidden, occult; magic, destiny, secrets, *residua* +--- +- *Forces* +- {% ElementSymbol + element="forces" + size="var(--typo-size-2xl)" + color="inherit" /%} +- Iron +- Mars +- Energies, natural laws; power, conflict, dynamics +{% /table %} diff --git a/src/content/articles/awq/index.mdoc b/src/content/articles/awq/index.mdoc new file mode 100644 index 0000000..386d651 --- /dev/null +++ b/src/content/articles/awq/index.mdoc @@ -0,0 +1,13 @@ +--- +title: Advanced Warhammer Quest +summary: A dungeoncrawler for the last millenium! +cover: + showInHeader: false +publishDate: 2026-02-27T14:39:00.000Z +status: published +isFeatured: false +tags: [] +relatedArticles: [] +seo: + noIndex: false +--- diff --git a/src/content/articles/chainbreaker/index.mdoc b/src/content/articles/chainbreaker/index.mdoc new file mode 100644 index 0000000..04905a3 --- /dev/null +++ b/src/content/articles/chainbreaker/index.mdoc @@ -0,0 +1,13 @@ +--- +title: Chainbreaker +summary: Last blood in a world gone mad +cover: + showInHeader: false +publishDate: 2026-02-27T14:40:00.000Z +status: published +isFeatured: false +tags: [] +relatedArticles: [] +seo: + noIndex: false +--- diff --git a/src/content/articles/elements/index.mdoc b/src/content/articles/elements/index.mdoc new file mode 100644 index 0000000..c9efe8d --- /dev/null +++ b/src/content/articles/elements/index.mdoc @@ -0,0 +1,14 @@ +--- +title: Elements +summary: References for all the Elements +cover: + showInHeader: false +publishDate: 2026-02-24T09:44:00.000Z +status: published +isFeatured: false +parent: references +tags: [] +relatedArticles: [] +seo: + noIndex: false +--- diff --git a/src/content/articles/framework/index.mdoc b/src/content/articles/framework/index.mdoc new file mode 100644 index 0000000..0d09b86 --- /dev/null +++ b/src/content/articles/framework/index.mdoc @@ -0,0 +1,23 @@ +--- +title: The Framework +summary: Conceptual Foundation of Crucible +cover: + showInHeader: false +publishDate: 2026-02-20T11:14:00.000Z +status: published +isFeatured: false +parent: the-crucible +tags: [] +relatedArticles: [] +seo: + noIndex: false +--- +- Alchemical Materialism + - The Primes + - The Essences + - Affinity + - Pool +- Design Principles + - Tiers + - Domains + - Sin Engine diff --git a/src/content/articles/materia/index.mdoc b/src/content/articles/materia/index.mdoc new file mode 100644 index 0000000..013f5b6 --- /dev/null +++ b/src/content/articles/materia/index.mdoc @@ -0,0 +1,20 @@ +--- +title: Materia +subtitle: The Elemental Pool +summary: Foundation of the Crucible, the elemental pool +cover: + showInHeader: false +publishDate: 2026-02-25T23:19:00.000Z +status: published +isFeatured: false +parent: framework +tags: [] +relatedArticles: [] +seo: + noIndex: false +--- +- Pool is both **identity** and **budget** + 1. **Generate Materia =>** from *material conditions* (environment, subsistence, mythology, history) + 1. **Read Identity =>** From a grid depending on the entity's nature (e.g. *Ethos, Theology, Polity)* + 1. **Spend tokens** +- Leftover tokens are not carried over diff --git a/src/content/articles/prima-materia/index.mdoc b/src/content/articles/prima-materia/index.mdoc new file mode 100644 index 0000000..f20dc93 --- /dev/null +++ b/src/content/articles/prima-materia/index.mdoc @@ -0,0 +1,17 @@ +--- +title: Prima Materia +subtitle: Where we read the earth and learn what it provides +summary: Where we read the earth and learn what it provides +cover: + showInHeader: true +publishDate: 2026-02-25T23:28:00.000Z +status: published +isFeatured: false +parent: the-crucible +tags: + - Crucible Stage + - Generation +relatedArticles: [] +seo: + noIndex: false +--- diff --git a/src/content/articles/references/index.mdoc b/src/content/articles/references/index.mdoc new file mode 100644 index 0000000..5575108 --- /dev/null +++ b/src/content/articles/references/index.mdoc @@ -0,0 +1,14 @@ +--- +title: References +summary: Where we collect all the references +cover: + showInHeader: false +publishDate: 2026-02-24T09:43:00.000Z +status: published +isFeatured: false +parent: the-crucible +tags: [] +relatedArticles: [] +seo: + noIndex: false +--- diff --git a/src/content/articles/the-crucible/index.mdoc b/src/content/articles/the-crucible/index.mdoc index e6f6f7e..98b95d0 100644 --- a/src/content/articles/the-crucible/index.mdoc +++ b/src/content/articles/the-crucible/index.mdoc @@ -29,16 +29,16 @@ seo: ## The Seven Stages -1. **Prima Materia. – Land** - - Generate biomes and resources +1. **Prima Materia. – Material Conditions** + - Generates land and kin 1. **Calcination – Kin** - - Generates kindred, heritage, and ancestry + - Generates heritage and ancestry 1. **Fermentation – Belief** - - Generates religion and belief + - Generates religion and faith 1. **Sublimation – Witchcraft** - - Generates magical traditions and crafts -1. **Coagulation – Vessels** - - Generates institutions and factions + - Generates disciplines and crafts +1. **Coagulation – Factions** + - Generates vessels 1. **Conjunction – Realms** - Generates states, tribes, and settlements 1. **Dissolution** diff --git a/src/content/config.ts b/src/content/config.ts index c4b5008..56740b1 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -1,45 +1,76 @@ import { defineCollection, z } from 'astro:content'; +import { glob } from 'astro/loaders'; + +const symbolSchema = z.discriminatedUnion('discriminant', [ + z.object({ + discriminant: z.literal('font'), + value: z.object({ + family: z.string(), + character: z.string(), + }), + }), + z.object({ + discriminant: z.literal('svg'), + value: z.string(), + }), +]); + +const seoSchema = z + .object({ + title: z.string().optional(), + description: z.string().optional(), + noIndex: z.boolean().default(false), + }) + .optional(); + +const coverSchema = z + .object({ + src: z.string().optional(), + alt: z.string().optional(), + caption: z.string().optional(), + showInHeader: z.boolean().default(false), + }) + .optional(); + +const baseArticleSchema = z.object({ + title: z.string(), + summary: z.string(), + subtitle: z.string().optional(), + cover: coverSchema, + publishDate: z.date(), + updateDate: z.date().optional(), + status: z.enum(['draft', 'published', 'archived']).default('draft'), + isFeatured: z.boolean().default(false), + parent: z.string().optional(), + tags: z.array(z.string()).default([]), + relatedArticles: z.array(z.string()).default([]), + seo: seoSchema, +}); const articles = defineCollection({ - schema: z.object({ - title: z.string(), - summary: z.string(), - cover: z - .object({ - src: z.string().optional(), - alt: z.string().optional(), - caption: z.string().optional(), - showInHeader: z.boolean().default(false), - }) - .optional(), - publishDate: z.date(), - updateDate: z.date().optional(), - status: z.enum(['draft', 'published', 'archived']).default('draft'), - isFeatured: z.boolean().default(false), - parent: z.string().optional(), - tags: z.array(z.string()).default([]), - relatedArticles: z.array(z.string()).default([]), - seo: z - .object({ - title: z.string().optional(), - description: z.string().optional(), - noIndex: z.boolean().default(false), - }) - .optional(), + loader: glob({ + pattern: '**/index.mdoc', + base: './src/content/articles', }), + schema: baseArticleSchema, }); const pages = defineCollection({ schema: z.object({ title: z.string(), - seo: z - .object({ - title: z.string().optional(), - description: z.string().optional(), - noIndex: z.boolean().default(false), - }) - .optional(), + seo: seoSchema, }), }); -export const collections = { articles, pages }; +const elements = defineCollection({ + loader: glob({ + pattern: '**/index.mdoc', + base: './src/content/crucible/elements', + }), + schema: baseArticleSchema.extend({ + category: z.enum(['prime', 'essence', 'aspect']).default('prime'), + symbol: symbolSchema, + }), +}); + +export const collections = { articles, pages, elements }; diff --git a/src/content/crucible/elements/aether/index.mdoc b/src/content/crucible/elements/aether/index.mdoc new file mode 100644 index 0000000..c5e222b --- /dev/null +++ b/src/content/crucible/elements/aether/index.mdoc @@ -0,0 +1,52 @@ +--- +title: Aether +subtitle: The Transcendent, The Ordered, The Numinous +summary: Exploring the Essence »Aether« in the Alchemical Materialism framework +cover: + showInHeader: false +publishDate: 2026-02-24T13:02:00.000Z +status: published +isFeatured: false +parent: elements +tags: + - Essences +relatedArticles: [] +seo: + noIndex: false +category: essence +symbol: + discriminant: font + value: + family: Unigrim Dee + character: D +--- +- **Alchemical Properties =>** the fifth element; beyond the four mundane; celestial, incorruptible +- **Primary Affinity =>** Rites +- **Associations** + - What lies beyond the material; the fifth thing + - Cosmic order → the pattern behind apparent chaos + - The numinous → the experience of something greater + - The bridge between mortal and divine; threshold substance + - Meaning itself; the answer to »why does this matter?« +- **Material Character** + - *Rites =>* Priesthoods, temples, divine mandate; fate, destiny, the inescapable + - *Statecraft =>* Divine legitimacy, sacred law, oaths before gods, hierarchy sanctified from above + - *Lore =>* Revelation, prophecy, mystical insight; knowledge from beyond; mystery traditions + - *Warfare =>* Holy war, divine champions, sacred weapons; morale & terror; the fear of the supernatural + - *Prosperity =>* Sacred crafts, ritual objects; things made for purposes beyond the materials; +- **Symbolic** + - Transcendence + - Order + - Mystery + - Meaning + - The Sacred + - What make mundane things matter + - The pattern behind the noise + - Incorruptible and therefore terrifying + - The divine gaze +- **Failures** + - Disconnection from reality + - Cosmic order that crushes mortal will + - Meaning so heavy it makes life unbearable + - Madness from contact with the transcendent +- **Key Image =>** the Stars diff --git a/src/content/crucible/elements/air/index.mdoc b/src/content/crucible/elements/air/index.mdoc new file mode 100644 index 0000000..dc9012c --- /dev/null +++ b/src/content/crucible/elements/air/index.mdoc @@ -0,0 +1,50 @@ +--- +title: Air +subtitle: The Invisible, The Expansive, The Permeating +summary: Exploring the Prime »Air« in the Alchemical Materialism framework +cover: + showInHeader: false +publishDate: 2026-02-24T13:01:00.000Z +status: published +isFeatured: false +parent: elements +tags: + - Essences +relatedArticles: [] +seo: + noIndex: false +category: essence +symbol: + discriminant: font + value: + family: Unigrim Dee + character: H +--- +- **Alchemical Properties =>** Hot & Wet; ascending, expanding, permeating +- **Primary Affinity =>** Lore +- **Associations** + - The invisible medium; what fills all space + - Breath literally, life itself; pneuma + - Movement, speed, freedom; what cannot be grasped + - The space between things; the medium of sound and speech + - Expansion without limit; the storm that scatters +- **Material Character** + - *Lore →* Speech, rhetoric, philosophy, abstract thought; pneuma as intellect; the word as power + - *Statecraft →* Communication, rumour, reputation on the wind; freedom from obligation: the ungovernable + - *Warfare →* Cavalry, archery, speed & mobility; hit-&-run; the charge; storms as divine wrath + - *Prosperity →* Windmills, sailing, ventilation of mines; the medium through which trade moves + - *Rites →* Sky-gods, storm deities, breath-of-life; divine voice; oracles; the heavens +- **Symbolic** + - Freedom + - Invisibility + - Speed + - Communication + - Expansion +- **Failures** + - Scattering + - Nothing holds + - All form dispersed + - Rootlessness + - The storm that destroys all structure + - Empty air → nothing there at all +- **Key Image =>** the Storm diff --git a/src/content/crucible/elements/art/index.mdoc b/src/content/crucible/elements/art/index.mdoc new file mode 100644 index 0000000..db5e575 --- /dev/null +++ b/src/content/crucible/elements/art/index.mdoc @@ -0,0 +1,23 @@ +--- +title: Art +summary: Exploring the Aspect »Art« in the Alchemical Materialism framework +cover: + showInHeader: false +publishDate: 2026-02-25T21:56:00.000Z +status: published +isFeatured: false +parent: elements +tags: [] +relatedArticles: [] +seo: + noIndex: false +category: aspect +symbol: + discriminant: font + value: + family: Unigrim Dee + character: F +--- +- **Planet =>** Venus +- **Metal =>** Copper +- Creation, expression; artistry, rituals, craft diff --git a/src/content/crucible/elements/body/index.mdoc b/src/content/crucible/elements/body/index.mdoc new file mode 100644 index 0000000..44ce9c1 --- /dev/null +++ b/src/content/crucible/elements/body/index.mdoc @@ -0,0 +1,49 @@ +--- +title: Body +summary: Exploring the Prime »Body« in the Alchemical Materialism framework +cover: + alt: Salt – »What remains« + showInHeader: false +publishDate: 2026-02-24T12:27:00.000Z +status: published +isFeatured: false +parent: elements +tags: + - Primes +relatedArticles: [] +seo: + noIndex: false +category: prime +symbol: + discriminant: font + value: + family: Unigrim Dee + character: M +--- +- **Alchemical principle =>** the fixed residue; what's left after burning; the stable principle +- **Core =>** Structure; form; persistence; what endures +- **Keyword =>** »It endures« +- **Principles** + - The principle of **structural persistence;** the substrate + - The institution that outlives its founders; the tradition that continues because it continues + - *Body* doesn't drive action: it's what's already there when action occurs + - Soul ignites, spirit transforms, Body is the **medium and the resistance** +- **Organisational expressions:** Institutional production: organised labor, standardised input, persistent infrastructure +- **Expressions** + - The bureaucracy processing forms centuries after anyone remembers why + - The kinship system determining marriage partners before birth + - The city walls that still stand + - The language that shapes though before you think + - The caste that tells you who you are before you are born + - The road network that dictates where trade flows +- **Failures** + - Calcification + - Rigidity + - the structure that cannot bend and therefore breaks + - the dead institution + - Tradition that crushes all life from what it holds +- **Quality** + - Fixed + - Crystalline + - Enduring + - Structural diff --git a/src/content/crucible/elements/earth/index.mdoc b/src/content/crucible/elements/earth/index.mdoc new file mode 100644 index 0000000..80d916e --- /dev/null +++ b/src/content/crucible/elements/earth/index.mdoc @@ -0,0 +1,50 @@ +--- +title: Earth +summary: Exploring the Prime »Earth« in the Alchemical Materialism framework +cover: + alt: The Material, The Heavy, The Foundation + showInHeader: false +publishDate: 2026-02-24T12:56:00.000Z +status: published +isFeatured: false +parent: elements +tags: + - Essences +relatedArticles: + - alchemical-materialism +seo: + noIndex: false +category: essence +symbol: + discriminant: font + value: + family: Unigrim Dee + character: X +--- +- **Alchemical Properties =>** Cold & Dry; descending, condensing, solidifying +- **Primary affinity =>** Prosperity +- **Associations** + - Weight, density, solidity; the things that's *there* + - The ground beneath; territory, boundary, the physical + - Resistance to movement; inertia; mass + - The body itself: flesh, bone, muscle + - What can be held, measured, counted, divided +- **Material Character** + - *Prosperity →* Extraction, agriculture, physical labour; working the land; masonry, road-building + - *Warfare →* Heavy infantry, siege, fortification; holding ground; crushing weight + - *Statecraft →* Territorial administration; »this is MY land«; + - *Lore →* Practical craft, material science; knowing by doing and touching + - *Rites →* Sacred ground, burial rites, fertility rituals; the body as sacred; chthonic power +- **Symbolic** + - Rootedness + - Stubbornness + - Endurance + - Possession + - Gravity + - The foundational +- **Failures** + - Immoveable + - Crushing + - Suffocating + - The weight that buries +- **Key Image:** the Mountain diff --git a/src/content/crucible/elements/entities/index.mdoc b/src/content/crucible/elements/entities/index.mdoc new file mode 100644 index 0000000..f6704c3 --- /dev/null +++ b/src/content/crucible/elements/entities/index.mdoc @@ -0,0 +1,23 @@ +--- +title: Entities +summary: Exploring the Aspect »Entities« in the Alchemical Materialism framework +cover: + showInHeader: false +publishDate: 2026-02-25T21:42:00.000Z +status: published +isFeatured: false +parent: elements +tags: [] +relatedArticles: [] +seo: + noIndex: false +category: aspect +symbol: + discriminant: font + value: + family: Unigrim Dee + character: O +--- +- **Metal =>** Silver +- **Planet =>** Moon +- Living beings; flesh, beasts, plants, spirits diff --git a/src/content/crucible/elements/fire/index.mdoc b/src/content/crucible/elements/fire/index.mdoc new file mode 100644 index 0000000..dbc5cdd --- /dev/null +++ b/src/content/crucible/elements/fire/index.mdoc @@ -0,0 +1,47 @@ +--- +title: Fire +subtitle: The Bright, The Consuming, The Refining +summary: Exploring the Prime »Fire« in the Alchemical Materialism framework +cover: + showInHeader: false +publishDate: 2026-02-24T12:58:00.000Z +status: published +isFeatured: false +parent: elements +tags: + - Essences +relatedArticles: [] +seo: + noIndex: false +category: essence +symbol: + discriminant: font + value: + family: Unigrim Dee + character: C +--- +- **Alchemical Properties =>** Hot & Dry; ascending, illuminating, consuming +- **Primary affinity =>** Warfare +- **Associations** + - Light that reveals; heat that transforms + - Consumption → Fire needs fuel and destroys what it burns + - The Forge → Destruction that creates something better + - Illuminations and blindness simultaneously + - Purification through burning away impurity +- **Material Character** + - *Warfare →* scorched earth, purges, burning the heretic; destroying to cleanse; Greek Fire; purification through destruction + - *Prosperity →* the forge, the kiln; smelting, glasswork, ceramics; refining the raw into finished + - *Statecraft →* passionate bonds, burning loyalty; consuming rivalries; bonds that burn bright and burn out + - *Lore →* revelation, illumination, mastery; orthodoxy as the »one true light« + - *Rites →* sacred flames, purification rites, trial by fire; burnt offerings; the sun as god +- **Symbolic** + - Brilliance + - Hunger + - Purification + - Consumption + - the hearth that warms AND the wildfire that destroys +- **Failures** + - Consumes all fuel + - Scorches what it meant to warm + - Purification that leaves nothing alive +- **Key Image:** the Forge diff --git a/src/content/crucible/elements/forces/index.mdoc b/src/content/crucible/elements/forces/index.mdoc new file mode 100644 index 0000000..fc73519 --- /dev/null +++ b/src/content/crucible/elements/forces/index.mdoc @@ -0,0 +1,23 @@ +--- +title: Forces +summary: Exploring the Aspect »Forces« in the Alchemical Materialism framework +cover: + showInHeader: false +publishDate: 2026-02-25T22:01:00.000Z +status: published +isFeatured: false +parent: elements +tags: [] +relatedArticles: [] +seo: + noIndex: false +category: aspect +symbol: + discriminant: font + value: + family: Unigrim Dee + character: B +--- +- **Planet =>** Mars +- **Metal =>** Iron +- Energies, natural laws; power, conflict, dynamics diff --git a/src/content/crucible/elements/matter/index.mdoc b/src/content/crucible/elements/matter/index.mdoc new file mode 100644 index 0000000..4af6342 --- /dev/null +++ b/src/content/crucible/elements/matter/index.mdoc @@ -0,0 +1,23 @@ +--- +title: Matter +summary: Exploring the Aspect »Matter« in the Alchemical Materialism framework +cover: + showInHeader: false +publishDate: 2026-02-25T21:49:00.000Z +status: published +isFeatured: false +parent: elements +tags: [] +relatedArticles: [] +seo: + noIndex: false +category: aspect +symbol: + discriminant: font + value: + family: Unigrim Dee + character: S +--- +- **Metal =>** Lead +- **Planet =>** Saturn +- Physical substances; materials, terrain, stone, foundations diff --git a/src/content/crucible/elements/mind/index.mdoc b/src/content/crucible/elements/mind/index.mdoc new file mode 100644 index 0000000..c1d3ce8 --- /dev/null +++ b/src/content/crucible/elements/mind/index.mdoc @@ -0,0 +1,23 @@ +--- +title: Mind +summary: Exploring the Aspect »Mind« in the Alchemical Materialism framework +cover: + showInHeader: false +publishDate: 2026-02-25T21:51:00.000Z +status: published +isFeatured: false +parent: elements +tags: [] +relatedArticles: [] +seo: + noIndex: false +category: aspect +symbol: + discriminant: font + value: + family: Unigrim Dee + character: I +--- +- **Metal =>** Quicksilver +- **Planet =>** Mercury +- Thought, consciousness; intellect, emotion, skills diff --git a/src/content/crucible/elements/mysteries/index.mdoc b/src/content/crucible/elements/mysteries/index.mdoc new file mode 100644 index 0000000..3206fc1 --- /dev/null +++ b/src/content/crucible/elements/mysteries/index.mdoc @@ -0,0 +1,23 @@ +--- +title: Mysteries +summary: Exploring the Aspect »Mysteries« in the Alchemical Materialism framework +cover: + showInHeader: false +publishDate: 2026-02-25T21:59:00.000Z +status: published +isFeatured: false +parent: elements +tags: [] +relatedArticles: [] +seo: + noIndex: false +category: aspect +symbol: + discriminant: font + value: + family: Unigrim Dee + character: R +--- +- **Planet =>** Sun +- **Metal =>** Gold +- Hidden, occult; magic, destiny; secrets, realms beyond diff --git a/src/content/crucible/elements/society/index.mdoc b/src/content/crucible/elements/society/index.mdoc new file mode 100644 index 0000000..96b8f22 --- /dev/null +++ b/src/content/crucible/elements/society/index.mdoc @@ -0,0 +1,23 @@ +--- +title: Society +summary: Exploring the Aspect »Society« in the Alchemical Materialism framework +cover: + showInHeader: false +publishDate: 2026-02-25T21:54:00.000Z +status: published +isFeatured: false +parent: elements +tags: [] +relatedArticles: [] +seo: + noIndex: false +category: aspect +symbol: + discriminant: font + value: + family: Unigrim Dee + character: L +--- +- **Metal =>** Tin +- **Planet =>** Jupiter +- Collective organisation; governance, institutions, law diff --git a/src/content/crucible/elements/soul/index.mdoc b/src/content/crucible/elements/soul/index.mdoc new file mode 100644 index 0000000..1dc1402 --- /dev/null +++ b/src/content/crucible/elements/soul/index.mdoc @@ -0,0 +1,46 @@ +--- +title: Soul +summary: Exploring the Prime »Soul« in the Alchemical Materialism framework +cover: + showInHeader: false +publishDate: 2026-02-24T11:26:00.000Z +status: published +isFeatured: false +parent: elements +tags: + - Primes +relatedArticles: [] +seo: + noIndex: false +category: prime +symbol: + discriminant: font + value: + family: Unigrim Dee + character: A +--- +- **Alchemical Principle =>** the combustible essence; what makes fire catch; the active principle +- **Core =>** Agency; drive; the spark that initiates action +- **Keyword =>** »I burn« +- **Principles** + - The principle of **individual agency and animation** + - Greed is one expression; so is passion, obsession, curiosity, protective fury, creative drive + - What unites Soul expressions: the fire originates in *a person,* not a structure or process +- **Organisational expression:** Individual excellence; master craftsmen, personal glory, competitive innovation +- **Expressions** + - The merchant hoarding silver + - The sculptor destroying a statue because of a imperfection only they can see + - The parent killing to protect their child + - The explorer walking into the unknown + - The warrior seeking glory + - The inventor breaking every rule +- **Failures:** + - Consumes everything + - Burns out + - Burns what it touches + - Individual drive that destroys because it cannot moderate +- **Qualities:** + - Combustible + - Animating + - Individual + - Igniting diff --git a/src/content/crucible/elements/spirit/index.mdoc b/src/content/crucible/elements/spirit/index.mdoc new file mode 100644 index 0000000..51732c6 --- /dev/null +++ b/src/content/crucible/elements/spirit/index.mdoc @@ -0,0 +1,57 @@ +--- +title: Spirit +subtitle: Mercury – What transforms +summary: Exploring the Prime »Spirit« in the Alchemical Materialism framework +cover: + showInHeader: false +publishDate: 2026-02-24T12:42:00.000Z +status: published +isFeatured: false +parent: elements +tags: + - Primes +relatedArticles: + - alchemical-materialism +seo: + noIndex: false +category: prime +symbol: + discriminant: font + value: + family: Unigrim Dee + character: E +--- +- **Alchemical Principle =>** the volatile mediator; what escapes, dissolves, and recombines; the transformation principle +- **Core =>** Transformation, synthesis; *becoming* +- **Keyword =>** »Nothing stays, everything becomes« +- **Principles** + - The principle of **transformation as a positive force;** not mere reaction or survival + - *»Solve et coagula«* → dissolve and recombine + - Mediation, adaption, and change-as-drive are all expressions of transformation + - Mercury is the *most alchemically important* of the three → the philosopher's stone ingredient +- **Organisational Expression** + - Adaptive innovation + - Cross-pollination + - Hybrid techniques + - Syncretic methods +- **Expressions** + - Syncretic cultures absorbing and remaking what they encounter + - Trade-diaspora peoples existing between cultures; transforming both + - Revolutionary movements dissolving old structures + - Liminal societies at crossroads, borders, thresholds + - Alchemists literally + - Seasonal/cyclical peoples participating in cycles of transformation + - Mediators who create new arrangements, not just broker peace +- **Failures** + - Dissolves everything + - Nothing holds + - Identity lost + - Perpetual revolution devouring its own + - Formlessness + - Mercury slipping through every grasp +- **Quality** + - Volatile + - Liminal + - Mercurial + - Syncretic + - Transformative diff --git a/src/content/crucible/elements/water/index.mdoc b/src/content/crucible/elements/water/index.mdoc new file mode 100644 index 0000000..c7d962a --- /dev/null +++ b/src/content/crucible/elements/water/index.mdoc @@ -0,0 +1,50 @@ +--- +title: Water +subtitle: The Flowing, The Deep, The Dissolving +summary: Exploring the Essence »Water« in the Alchemical Materialism framework +cover: + showInHeader: false +publishDate: 2026-02-24T12:59:00.000Z +status: published +isFeatured: false +parent: elements +tags: + - Essences +relatedArticles: [] +seo: + noIndex: false +category: essence +symbol: + discriminant: font + value: + family: Unigrim Dee + character: Q +--- +- **Alchemical Properties =>** Cold & Wet; descending, flowing, dissolving +- **Primary Affinity =>** Statecraft +- **Associations** + - The universal solvent -> what dissolves boundaries + - Flow, connection → the medium of exchange + - Depth and concealment → What's hidden beneath the surface + - Life-giving AND drowning: rain AND Flood + - Takes the shape of its container; adapt to form +- **Material Character** + - *Statecraft →* Networks, exchange, reciprocity; debts that flow; diplomacy; status as current + - *Prosperity →* Irrigation, fishing, brewing, dyeing; the river as economic artery + - *Arms →* Naval power, coastal raiding, poison; erosion/attrition warfare + - *Lore →* Hidden knowledge, mysteries of the deep; intuition over analysis; the murky and the clear + - *Rites →* Baptism, sacred rivers, purification by washing; sea-gods; the abyss +- **Symbolic** + - Fluidity + - Depth + - Concealment + - Connection + - Erosion + - What flows between + - What lies beneath + - Patience that wears stone away +- **Failures** + - Drowning + - Erosion + - Formlessness +- **Key Image:** the River diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-Bold.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-Bold.woff2 new file mode 100644 index 0000000..ac97321 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-Bold.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-BoldItalic.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-BoldItalic.woff2 new file mode 100644 index 0000000..d41d0ba Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-BoldItalic.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-BoldOblique.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-BoldOblique.woff2 new file mode 100644 index 0000000..f956cb2 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-BoldOblique.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraBold.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraBold.woff2 new file mode 100644 index 0000000..c28e70f Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraBold.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraBoldItalic.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraBoldItalic.woff2 new file mode 100644 index 0000000..7841c61 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraBoldItalic.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraBoldOblique.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraBoldOblique.woff2 new file mode 100644 index 0000000..597bea3 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraBoldOblique.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraLight.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraLight.woff2 new file mode 100644 index 0000000..166f3dd Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraLight.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraLightItalic.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraLightItalic.woff2 new file mode 100644 index 0000000..a5438fa Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraLightItalic.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraLightOblique.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraLightOblique.woff2 new file mode 100644 index 0000000..49f0be3 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraLightOblique.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-Heavy.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-Heavy.woff2 new file mode 100644 index 0000000..514a688 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-Heavy.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-HeavyItalic.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-HeavyItalic.woff2 new file mode 100644 index 0000000..1ac4539 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-HeavyItalic.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-HeavyOblique.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-HeavyOblique.woff2 new file mode 100644 index 0000000..a9cc305 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-HeavyOblique.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-Italic.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-Italic.woff2 new file mode 100644 index 0000000..90cae67 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-Italic.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-Light.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-Light.woff2 new file mode 100644 index 0000000..231e6d2 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-Light.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-LightItalic.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-LightItalic.woff2 new file mode 100644 index 0000000..da5fc35 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-LightItalic.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-LightOblique.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-LightOblique.woff2 new file mode 100644 index 0000000..3e8d977 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-LightOblique.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-Medium.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-Medium.woff2 new file mode 100644 index 0000000..e622a3a Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-Medium.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-MediumItalic.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-MediumItalic.woff2 new file mode 100644 index 0000000..fee1589 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-MediumItalic.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-MediumOblique.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-MediumOblique.woff2 new file mode 100644 index 0000000..e2c14df Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-MediumOblique.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-Oblique.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-Oblique.woff2 new file mode 100644 index 0000000..ff0dc21 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-Oblique.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-Regular.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-Regular.woff2 new file mode 100644 index 0000000..a84ba56 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-Regular.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-SemiBold.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-SemiBold.woff2 new file mode 100644 index 0000000..65807ea Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-SemiBold.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-SemiBoldItalic.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-SemiBoldItalic.woff2 new file mode 100644 index 0000000..409e704 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-SemiBoldItalic.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-SemiBoldOblique.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-SemiBoldOblique.woff2 new file mode 100644 index 0000000..ad33d14 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-SemiBoldOblique.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-Thin.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-Thin.woff2 new file mode 100644 index 0000000..d1faf65 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-Thin.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-ThinItalic.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-ThinItalic.woff2 new file mode 100644 index 0000000..735fa91 Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-ThinItalic.woff2 differ diff --git a/src/fonts/IosevkaSansMono/IosevkaSansMono-ThinOblique.woff2 b/src/fonts/IosevkaSansMono/IosevkaSansMono-ThinOblique.woff2 new file mode 100644 index 0000000..4c8c0dc Binary files /dev/null and b/src/fonts/IosevkaSansMono/IosevkaSansMono-ThinOblique.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Bold.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Bold.woff2 new file mode 100644 index 0000000..bd8b92f Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Bold.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-BoldItalic.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-BoldItalic.woff2 new file mode 100644 index 0000000..88ef906 Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-BoldItalic.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-BoldOblique.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-BoldOblique.woff2 new file mode 100644 index 0000000..31d9f5f Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-BoldOblique.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraBold.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraBold.woff2 new file mode 100644 index 0000000..047e3e5 Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraBold.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraBoldItalic.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraBoldItalic.woff2 new file mode 100644 index 0000000..70b1504 Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraBoldItalic.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraBoldOblique.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraBoldOblique.woff2 new file mode 100644 index 0000000..a34c72d Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraBoldOblique.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraLight.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraLight.woff2 new file mode 100644 index 0000000..da6e3fe Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraLight.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraLightItalic.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraLightItalic.woff2 new file mode 100644 index 0000000..1129594 Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraLightItalic.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraLightOblique.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraLightOblique.woff2 new file mode 100644 index 0000000..bd53c9f Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraLightOblique.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Heavy.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Heavy.woff2 new file mode 100644 index 0000000..936fd3d Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Heavy.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-HeavyItalic.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-HeavyItalic.woff2 new file mode 100644 index 0000000..c118c2f Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-HeavyItalic.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-HeavyOblique.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-HeavyOblique.woff2 new file mode 100644 index 0000000..3281b61 Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-HeavyOblique.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Italic.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Italic.woff2 new file mode 100644 index 0000000..0a3d287 Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Italic.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Light.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Light.woff2 new file mode 100644 index 0000000..f44915d Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Light.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-LightItalic.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-LightItalic.woff2 new file mode 100644 index 0000000..e32cfd0 Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-LightItalic.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-LightOblique.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-LightOblique.woff2 new file mode 100644 index 0000000..3864398 Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-LightOblique.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Medium.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Medium.woff2 new file mode 100644 index 0000000..8716b83 Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Medium.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-MediumItalic.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-MediumItalic.woff2 new file mode 100644 index 0000000..efefd88 Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-MediumItalic.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-MediumOblique.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-MediumOblique.woff2 new file mode 100644 index 0000000..0193004 Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-MediumOblique.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Oblique.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Oblique.woff2 new file mode 100644 index 0000000..8cf6863 Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Oblique.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Regular.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Regular.woff2 new file mode 100644 index 0000000..fed199e Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Regular.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-SemiBold.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-SemiBold.woff2 new file mode 100644 index 0000000..769f8a5 Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-SemiBold.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-SemiBoldItalic.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-SemiBoldItalic.woff2 new file mode 100644 index 0000000..314df52 Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-SemiBoldItalic.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-SemiBoldOblique.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-SemiBoldOblique.woff2 new file mode 100644 index 0000000..5b2fd3e Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-SemiBoldOblique.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Thin.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Thin.woff2 new file mode 100644 index 0000000..e298671 Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Thin.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ThinItalic.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ThinItalic.woff2 new file mode 100644 index 0000000..4f516df Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ThinItalic.woff2 differ diff --git a/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ThinOblique.woff2 b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ThinOblique.woff2 new file mode 100644 index 0000000..f9b8e8b Binary files /dev/null and b/src/fonts/IosevkaSlabQp/IosevkaSlabQp-ThinOblique.woff2 differ diff --git a/src/keystatic/collections/articles.ts b/src/keystatic/collections/articles.ts index b0d919a..e3a7036 100644 --- a/src/keystatic/collections/articles.ts +++ b/src/keystatic/collections/articles.ts @@ -1,6 +1,6 @@ import { collection } from '@keystatic/core'; -import { createBaseArticleFields } from '../fields/base-article.ts'; -import { createContentField } from '../fields/content.ts'; +import { createBaseArticleFields } from '@fields/base-article.ts'; +import { createContentField } from '@fields/content.ts'; export const articles = collection({ label: 'Articles', diff --git a/src/keystatic/collections/crucible/elements.ts b/src/keystatic/collections/crucible/elements.ts new file mode 100644 index 0000000..413e3b8 --- /dev/null +++ b/src/keystatic/collections/crucible/elements.ts @@ -0,0 +1,66 @@ +import { collection, fields } from '@keystatic/core'; +import { createBaseArticleFields } from '@fields/base-article.ts'; +import { createContentField } from '@fields/content.ts'; + +export const elements = collection({ + label: 'Elements', + slugField: 'title', + path: 'src/content/crucible/elements/*/', + columns: ['category'], + + format: { + contentField: 'body', + }, + schema: { + ...createBaseArticleFields(), + body: createContentField(), + category: fields.select({ + label: 'Category', + options: [ + { + label: 'Prime', + value: 'prime', + }, + { + label: 'Essence', + value: 'essence', + }, + { + label: 'Aspect', + value: 'aspect', + }, + ], + defaultValue: 'prime', + }), + symbol: fields.conditional( + fields.select({ + label: 'Type', + defaultValue: 'font', + options: [ + { label: 'Font', value: 'font' }, + { label: 'SVG', value: 'svg' }, + ], + }), + { + svg: fields.text({ + label: 'SVG Markup', + multiline: true, + }), + font: fields.object({ + family: fields.select({ + label: 'Font Family', + defaultValue: 'Unigrim Dee', + options: [ + { label: 'Unigrim Dee', value: 'Unigrim Dee' }, + { label: 'Unigrim Hochenheim', value: 'Unigrim Hochenheim' }, + { label: 'Unigrim Trithemius', value: 'Unigrim Trithemius' }, + ], + }), + character: fields.text({ + label: 'Character', + }), + }), + }, + ), + }, +}); diff --git a/src/keystatic/collections/crucible/index.ts b/src/keystatic/collections/crucible/index.ts new file mode 100644 index 0000000..599cdba --- /dev/null +++ b/src/keystatic/collections/crucible/index.ts @@ -0,0 +1,7 @@ +import { elements } from './elements'; + +const crucibleCollections = { + cr_elements: elements, +}; + +export default crucibleCollections; diff --git a/src/keystatic/collections/pages.ts b/src/keystatic/collections/pages.ts index 183f875..78c962c 100644 --- a/src/keystatic/collections/pages.ts +++ b/src/keystatic/collections/pages.ts @@ -1,6 +1,6 @@ import { collection, fields } from '@keystatic/core'; -import { createSEOField } from '../fields/seo.ts'; -import { createContentField } from '../fields/content.ts'; +import { createSEOField } from '@fields/seo.ts'; +import { createContentField } from '@fields/content.ts'; export const pages = collection({ label: 'Pages', diff --git a/src/keystatic/components/element.ts b/src/keystatic/components/element.ts new file mode 100644 index 0000000..6692d6e --- /dev/null +++ b/src/keystatic/components/element.ts @@ -0,0 +1,50 @@ +import { componentIcon } from '@keystar/ui/icon/icons/componentIcon'; +import { inline } from '@keystatic/core/content-components'; +import { fields } from '@keystatic/core'; + +const elementCompontent = { + ElementSymbol: inline({ + label: 'Element Symbol', + icon: componentIcon, + schema: { + element: fields.relationship({ + label: 'Element', + collection: 'cr_elements', + }), + size: fields.select({ + label: 'Size', + defaultValue: 'var(--typo-size-md)', + options: [ + { label: '2XS', value: 'var(--typo-size-2xs)' }, + { label: 'XS', value: 'var(--typo-size-xs)' }, + { label: 'SM', value: 'var(--typo-size-sm)' }, + { label: 'MD', value: 'var(--typo-size-md)' }, + { label: 'LG', value: 'var(--typo-size-lg)' }, + { label: 'XL', value: 'var(--typo-size-xl)' }, + { label: '2XL', value: 'var(--typo-size-2xl)' }, + { label: '3XL', value: 'var(--typo-size-3xl)' }, + { label: '4XL', value: 'var(--typo-size-4xl)' }, + { label: '5XL', value: 'var(--typo-size-5xl)' }, + { label: '6XL', value: 'var(--typo-size-6xl)' }, + { label: '7XL', value: 'var(--typo-size-7xl)' }, + { label: '8XL', value: 'var(--typo-size-8xl)' }, + ], + }), + color: fields.select({ + label: 'Color', + defaultValue: 'inherit', + options: [ + { label: 'Inherit', value: 'inherit' }, + { label: 'primary', value: 'var(--color-primary)' }, + { label: 'secondary', value: 'var(--color-secondary)' }, + { label: 'tertiary', value: 'var(--color-tertiary)' }, + { label: 'primary', value: 'var(--color-primary)' }, + { label: 'Text', value: 'var(--color-text-primary)' }, + { label: 'Text Inverse', value: 'var(--color-text-inverse)' }, + ], + }), + }, + }), +}; + +export default elementCompontent; diff --git a/src/keystatic/components/index.ts b/src/keystatic/components/index.ts new file mode 100644 index 0000000..63a02fc --- /dev/null +++ b/src/keystatic/components/index.ts @@ -0,0 +1,5 @@ +import elementComponent from './element.ts'; + +export const generalComponents = { + ...elementComponent, +}; diff --git a/src/keystatic/fields/base-article.ts b/src/keystatic/fields/base-article.ts index 5c87e95..6ec40c7 100644 --- a/src/keystatic/fields/base-article.ts +++ b/src/keystatic/fields/base-article.ts @@ -2,8 +2,11 @@ import { fields } from '@keystatic/core'; import { createSEOField } from './seo.ts'; import { createCoverField } from './cover.ts'; -export const createBaseArticleFields = () => ({ +export const createBaseArticleFields = ( + parentCollection: string = 'articles', +) => ({ title: fields.slug({ name: { label: 'Title' } }), + subtitle: fields.text({ label: 'Subtitle' }), summary: fields.text({ label: 'Summary', multiline: true, @@ -33,7 +36,7 @@ export const createBaseArticleFields = () => ({ }), parent: fields.relationship({ label: 'Parent', - collection: 'articles', + collection: parentCollection, }), tags: fields.array(fields.text({ label: 'Tag' }), { label: 'Tags', diff --git a/src/keystatic/fields/content.ts b/src/keystatic/fields/content.ts index 6cd9a63..bef597b 100644 --- a/src/keystatic/fields/content.ts +++ b/src/keystatic/fields/content.ts @@ -1,7 +1,9 @@ import { fields } from '@keystatic/core'; import type { ContentComponent } from '@keystatic/core/content-components'; +import { generalComponents } from '../components'; -export const sharedComponents: Record<string, ContentComponent> = {}; +export const sharedComponents: Record<string, ContentComponent> = + generalComponents; export const createContentField = ( additionalComponents?: Record<string, ContentComponent>, diff --git a/src/layouts/ArticleLayout.astro b/src/layouts/ArticleLayout.astro index 14a91b5..aff5836 100644 --- a/src/layouts/ArticleLayout.astro +++ b/src/layouts/ArticleLayout.astro @@ -1,22 +1,12 @@ --- import type { CollectionEntry } from 'astro:content'; -import Base from './Base.astro'; +import ContentLayout from './ContentLayout.astro'; interface Props { entry: CollectionEntry<'articles'>; } const { entry } = Astro.props; -const { Content } = await entry.render(); -const { title, summary, publishDate, updateDate, cover, tags, seo } = - entry.data; --- -<Base title={title} seo={seo}> - <main class="content"> - <article> - <h1>{title}</h1> - <Content /> - </article> - </main> -</Base> +<ContentLayout entry={entry} collectionName="articles" /> diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index c81219f..10befbc 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -1,5 +1,6 @@ --- -import '../styles.css'; +import '@styles/global.css'; +import MastHead from '@compontents/layout/MastHead.astro'; interface Props { title: string; @@ -20,11 +21,12 @@ const pageDescription = seo?.description || ''; <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>{title} + {pageTitle} {pageDescription && } {seo?.noIndex && } + diff --git a/src/layouts/ContentLayout.astro b/src/layouts/ContentLayout.astro new file mode 100644 index 0000000..93cc846 --- /dev/null +++ b/src/layouts/ContentLayout.astro @@ -0,0 +1,58 @@ +--- +import type { CollectionEntry } from 'astro:content'; +import { render } from 'astro:content'; +import Base from '@layouts/Base.astro'; +import Header from '@compontents/layout/PostHeader/index.astro'; +import { getBreadcrumbs } from '@lib/utils/paths'; +import { toMilitaryDTG } from '@lib/utils/date'; + +interface Props { + entry: CollectionEntry<'articles'> | CollectionEntry<'elements'>; + collectionName: string; +} + +const { entry, collectionName } = Astro.props; +const { Content } = await render(entry); +const { title, summary, subtitle, updateDate, publishDate, tags, seo } = + entry.data; + +const breadcrumbs = await getBreadcrumbs( + entry.data.parent ?? null, + collectionName, +); +const formattedPublishDate = toMilitaryDTG(publishDate); +const formattedUpdateDate = updateDate + ? toMilitaryDTG(updateDate) + : formattedPublishDate; +const rawCover = entry.data.cover; +const headerCover = + rawCover?.src && rawCover?.showInHeader + ? { + src: rawCover.src, + alt: rawCover.alt ?? '', + caption: rawCover.caption ?? '', + showInHeader: rawCover.showInHeader, + } + : undefined; +--- + + +
+
+
+
+ + + +
+
+
+ diff --git a/src/layouts/PageLayout.astro b/src/layouts/PageLayout.astro index 3b0e701..32cc551 100644 --- a/src/layouts/PageLayout.astro +++ b/src/layouts/PageLayout.astro @@ -1,6 +1,6 @@ --- import type { CollectionEntry } from 'astro:content'; -import Base from './Base.astro'; +import Base from '@layouts/Base.astro'; interface Props { entry: CollectionEntry<'pages'>; diff --git a/src/layouts/crucible/ElementLayout.astro b/src/layouts/crucible/ElementLayout.astro new file mode 100644 index 0000000..e3ec09a --- /dev/null +++ b/src/layouts/crucible/ElementLayout.astro @@ -0,0 +1,54 @@ +--- +import type { CollectionEntry } from 'astro:content'; +import { render } from 'astro:content'; +import Base from '@layouts/Base.astro'; + +interface Props { + entry: CollectionEntry<'elements'>; +} + +const { entry } = Astro.props; +const { Content } = await render(entry); +const { title, seo, symbol } = entry.data; + +const glyph = symbol.discriminant === 'font' ? symbol.value : null; +--- + + +
+
+

+ {title} + { + glyph && ( + + {glyph.character} + + ) + } +

+ +
+
+ + + diff --git a/src/lib/paths.ts b/src/lib/paths.ts deleted file mode 100644 index 38b5613..0000000 --- a/src/lib/paths.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { getCollection, type CollectionEntry } from 'astro:content'; - -export type ContentType = 'article' | 'page'; - -export interface ResolvedEntry { - type: ContentType; - path: string; - entry: CollectionEntry<'pages'> | CollectionEntry<'articles'>; -} - -function buildPath( - slug: string, - parentSlug: string | null, - articles: CollectionEntry<'articles'>[], - visited: Set = new Set(), -): string { - if (!parentSlug) return slug; - if (visited.has(parentSlug)) { - console.warn(`Circular reference detected at ${parentSlug}`); - return slug; - } - visited.add(parentSlug); - - const parent = articles.find((a) => a.slug === parentSlug); - if (!parent) return slug; - - const parentPath = buildPath( - parent.slug, - parent.data.parent ?? null, - articles, - visited, - ); - - return `${parentPath}/${slug}`; -} - -export async function resolveAllPaths(): Promise { - const articles = await getCollection('articles'); - const pages = await getCollection('pages'); - - const resolvedArticles: ResolvedEntry[] = articles - .filter((article) => article.data.status === 'published') - .map((article) => ({ - type: 'article', - path: buildPath(article.slug, article.data.parent ?? null, articles), - entry: article, - })); - - const resolvedPages: ResolvedEntry[] = pages.map((page) => ({ - type: 'page', - path: `static/${page.slug}`, - entry: page, - })); - - return [...resolvedArticles, ...resolvedPages]; -} diff --git a/src/lib/paths_legacy.ts b/src/lib/paths_legacy.ts new file mode 100644 index 0000000..8e5c9b4 --- /dev/null +++ b/src/lib/paths_legacy.ts @@ -0,0 +1,87 @@ +import { getCollection, type CollectionEntry } from 'astro:content'; + +export type ContentType = 'article' | 'page' | 'element'; + +export interface ResolvedEntry { + type: ContentType; + path: string; + entry: + | CollectionEntry<'pages'> + | CollectionEntry<'articles'> + | CollectionEntry<'elements'>; +} + +function buildPath( + slug: string, + parentSlug: string | null, + articles: CollectionEntry<'articles'>[], + visited: Set = new Set(), +): string { + if (!parentSlug) return slug; + if (visited.has(parentSlug)) { + console.warn(`Circular reference detected at ${parentSlug}`); + return slug; + } + visited.add(parentSlug); + + const parent = articles.find((a) => a.slug === parentSlug); + if (!parent) return slug; + + const parentPath = buildPath( + parent.slug, + parent.data.parent ?? null, + articles, + visited, + ); + + return `${parentPath}/${slug}`; +} + +function resolveArticles( + articles: CollectionEntry<'articles'>[], +): ResolvedEntry[] { + return articles + .filter((entry) => entry.data.status === 'published') + .map((entry) => ({ + type: 'article' as const, + path: buildPath(entry.slug, entry.data.parent ?? null, articles), + entry, + })); +} + +interface GlobEntry { + id: string; + data: { status: string; parent?: string | null }; +} + +function resolveGlobCollection( + collection: GlobEntry[], + type: ContentType, + articles: CollectionEntry<'articles'>[], +): { type: ContentType; path: string; entry: GlobEntry }[] { + return collection + .filter((entry) => entry.data.status === 'published') + .map((entry) => ({ + type, + path: buildPath(entry.id, entry.data.parent ?? null, articles), + entry, + })); +} + +export async function resolveAllPaths(): Promise { + const articles = await getCollection('articles'); + const elements = await getCollection('elements'); + const pages = await getCollection('pages'); + + const resolvedPages: ResolvedEntry[] = pages.map((page) => ({ + type: 'page' as const, + path: `static/${page.slug}`, + entry: page, + })); + + return [ + ...resolveArticles(articles), + ...resolveGlobCollection(elements, 'element', articles), + ...resolvedPages, + ] as ResolvedEntry[]; +} diff --git a/src/lib/types/content.ts b/src/lib/types/content.ts new file mode 100644 index 0000000..b3478c8 --- /dev/null +++ b/src/lib/types/content.ts @@ -0,0 +1,43 @@ +import type { CollectionEntry } from 'astro:content'; + +/** Registered Content Type identifiers **/ +export type ContentType = 'article' | 'page' | 'element'; + +/** + * Minimal common shape for everything that participates in path resoltution and parent chain walking. + * Normalizes the slug/id and parent differences across collections + **/ + +export interface Resolvable { + slug: string; + title: string; + parent: string | null; +} + +/** Collection name -> normalized entries **/ +export type CollectionPool = Record; + +/** Fully resolved content entry with computed path **/ +export interface ResolvedEntry { + type: ContentType; + path: string; + entry: + | CollectionEntry<'pages'> + | CollectionEntry<'articles'> + | CollectionEntry<'elements'>; +} + +/** Single segment in a breadcrumb trail **/ +export interface BreadcrumbSegment { + label: string; + href: string; +} + +/** Entry in the command palette search index **/ +export interface PaletteEntry { + label: string; + path: string; + type: ContentType; + parent: string | null; + depth: number; +} diff --git a/src/lib/utils/date.ts b/src/lib/utils/date.ts new file mode 100644 index 0000000..81aa56d --- /dev/null +++ b/src/lib/utils/date.ts @@ -0,0 +1,25 @@ +const MONTHS = [ + 'JAN', + 'FEB', + 'MAR', + 'APR', + 'MAY', + 'JUN', + 'JUL', + 'AUG', + 'SEP', + 'OCT', + 'NOV', + 'DEC', +] as const; + +export const toMilitaryDTG = (datestring: Date, timeZone = 'A'): string => { + const d = new Date(datestring); + const day = d.getUTCDate().toString().padStart(2, '0'); + const hours = d.getUTCHours().toString().padStart(2, '0'); + const minutes = d.getUTCMinutes().toString().padStart(2, '0'); + const month = MONTHS[d.getUTCMonth()]; + const year = d.getUTCFullYear().toString().slice(-2); + + return `${day}${hours}${minutes}${timeZone}${month}${year}`; +}; diff --git a/src/lib/utils/paths.ts b/src/lib/utils/paths.ts new file mode 100644 index 0000000..8a6506e --- /dev/null +++ b/src/lib/utils/paths.ts @@ -0,0 +1,209 @@ +import { getCollection } from 'astro:content'; +import type { + ContentType, + Resolvable, + CollectionPool, + ResolvedEntry, + BreadcrumbSegment, + PaletteEntry, +} from '@/lib/types/content'; + +/** Registry +Maps each collection to the collection its `parent` field points to. Single source of truth for parent chain resolution. +**/ +const PARENT_COLLECTION: Record = { + articles: 'articles', + elements: 'articles', +}; + +const toResolvable = ( + entries: { id: string; data: { title: string; parent?: string | null } }[], +): Resolvable[] => + entries.map((e) => ({ + slug: e.id, + title: e.data.title, + parent: e.data.parent ?? null, + })); + +/** Path Building — collection-aware **/ + +const buildPath = ( + slug: string, + parentSlug: string | null, + parentCollection: string, + pools: CollectionPool, + visited: Set = new Set(), +): string => { + if (!parentSlug) return slug; + + const key = `${parentCollection}:${parentSlug}`; + if (visited.has(key)) { + console.warn(`Circular reference detected at ${key}`); + return slug; + } + visited.add(key); + + const pool = pools[parentCollection]; + if (!pool) return slug; + + const parent = pool.find((e) => e.slug === parentSlug); + if (!parent) return slug; + + const grandparentCollection = + PARENT_COLLECTION[parentCollection] ?? parentCollection; + + const parentPath = buildPath( + parent.slug, + parent.parent, + grandparentCollection, + pools, + visited, + ); + + return `${parentPath}/${slug}`; +}; + +/* Breadcrumbs +Same parent-chain walk as buildPath, but collects +{ label, href } segments. Excludes the current entry. +*/ +const buildBreadcrumbs = ( + parentSlug: string | null, + parentCollection: string, + pools: CollectionPool, + visited: Set = new Set(), +): BreadcrumbSegment[] => { + if (!parentSlug) return []; + + const key = `${parentCollection}:${parentSlug}`; + if (visited.has(key)) { + console.warn(`Circular reference detected at ${key}`); + return []; + } + visited.add(key); + + const pool = pools[parentCollection]; + if (!pool) return []; + + const parent = pool.find((e) => e.slug === parentSlug); + if (!parent) return []; + + const grandparentCollection = + PARENT_COLLECTION[parentCollection] ?? parentCollection; + + const ancestors = buildBreadcrumbs( + parent.parent, + grandparentCollection, + pools, + visited, + ); + + const href = `/${buildPath( + parent.slug, + parent.parent, + grandparentCollection, + pools, + )}`; + + return [...ancestors, { label: parent.title, href }]; +}; + +/** Pool Builder**/ + +export const buildPools = async (): Promise => { + const articles = await getCollection('articles'); + const elements = await getCollection('elements'); + + return { + articles: toResolvable(articles), + elements: toResolvable(elements), + }; +}; + +/* Public Breadcrumb API */ + +export const getBreadcrumbs = async ( + parentSlug: string | null, + collectionName: string, +): Promise => { + const pools = await buildPools(); + const parentCol = PARENT_COLLECTION[collectionName] ?? collectionName; + return buildBreadcrumbs(parentSlug, parentCol, pools); +}; + +/* Collection Resolution */ + +const resolveCollection = ( + entries: { + id: string; + data: { status: string; parent?: string | null; title: string }; + }[], + type: ContentType, + collectionName: string, + pools: CollectionPool, +): ResolvedEntry[] => { + const parentCol = PARENT_COLLECTION[collectionName] ?? collectionName; + + return entries + .filter((entry) => entry.data.status === 'published') + .map((entry) => ({ + type, + path: buildPath(entry.id, entry.data.parent ?? null, parentCol, pools), + entry, + })) as ResolvedEntry[]; +}; + +/** Palette Index + * Transforms resolve entries into a flat, seriallizable array for the command palette. + * Runs at build time, output gets inlined as JSON for client-side filtering + **/ +export const buildPaletteIndex = ( + resolved: ResolvedEntry[], + pools: CollectionPool, +): PaletteEntry[] => + resolved.map((r) => { + const depth = r.path.split('/').length; + const parentSlug = + 'parent' in r.entry.data ? (r.entry.data.parent ?? null) : null; + const parentTitle = parentSlug + ? (Object.values(pools) + .flat() + .find((e) => e.slug === parentSlug)?.title ?? null) + : null; + + return { + label: r.entry.data.title, + path: `/${r.path}`, + type: r.type, + parent: parentTitle, + depth, + }; + }); +/* Main Resolver */ + +export async function resolveAllPaths(): Promise { + const pools = await buildPools(); + const pages = await getCollection('pages'); + + const resolvedPages: ResolvedEntry[] = pages.map((page) => ({ + type: 'page' as const, + path: `static/${page.id}`, + entry: page, + })); + + return [ + ...resolveCollection( + await getCollection('articles'), + 'article', + 'articles', + pools, + ), + ...resolveCollection( + await getCollection('elements'), + 'element', + 'elements', + pools, + ), + ...resolvedPages, + ]; +} diff --git a/src/pages/[...path].astro b/src/pages/[...path].astro index 51d007a..04d9b58 100644 --- a/src/pages/[...path].astro +++ b/src/pages/[...path].astro @@ -1,8 +1,9 @@ --- -import { resolveAllPaths, type ResolvedEntry } from '../lib/paths'; -import type { CollectionEntry } from 'astro:content'; -import ArticleLayout from '../layouts/ArticleLayout.astro'; -import PageLayout from '../layouts/PageLayout.astro'; +import { resolveAllPaths } from '../lib/utils/paths'; +import type { ResolvedEntry } from '@lib/types/content'; +import ArticleLayout from '@layouts/ArticleLayout.astro'; +import PageLayout from '@layouts/PageLayout.astro'; +import ElementLayout from '@layouts/crucible/ElementLayout.astro'; export async function getStaticPaths() { const entries = await resolveAllPaths(); @@ -11,18 +12,15 @@ export async function getStaticPaths() { props: entry, })); } -const { type, entry } = Astro.props as ResolvedEntry; -const props = Astro.props as ResolvedEntry; +const layoutMap: Record = { + article: ArticleLayout, + page: PageLayout, + element: ElementLayout, +} as const; + +const { type, entry } = Astro.props as ResolvedEntry; +const Layout = layoutMap[type]; --- -{ - type === 'article' && ( - } /> - ) -} -{ - type === 'page' && ( - } /> - ) -} + diff --git a/src/pages/index.astro b/src/pages/index.astro index 12f3b3c..740dc8d 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,5 +1,5 @@ --- -import Base from '../layouts/Base.astro'; +import Base from '@layouts/Base.astro'; --- diff --git a/src/pages/palette-index.json.ts b/src/pages/palette-index.json.ts new file mode 100644 index 0000000..d448e2d --- /dev/null +++ b/src/pages/palette-index.json.ts @@ -0,0 +1,18 @@ +import type { APIRoute } from 'astro'; +import { + resolveAllPaths, + buildPools, + buildPaletteIndex, +} from '@lib/utils/paths'; + +export const GET: APIRoute = async () => { + const [resolved, pools] = await Promise.all([ + resolveAllPaths(), + buildPools(), + ]); + const index = buildPaletteIndex(resolved, pools); + + return new Response(JSON.stringify(index), { + headers: { 'Content-Type': 'application/json' }, + }); +}; diff --git a/src/styles.css b/src/styles.css deleted file mode 100644 index 7163989..0000000 --- a/src/styles.css +++ /dev/null @@ -1,8 +0,0 @@ -@import 'styles/fonts.css'; -@import 'styles/foundation.css'; -@import 'styles/colors.css'; -@import 'styles/dimensions.css'; -@import 'styles/typography.css'; -@import 'styles/elements.css'; -@import 'styles/custom-media.css'; -@import 'styles/content.css'; diff --git a/src/styles/colors.css b/src/styles/base/colors.css similarity index 100% rename from src/styles/colors.css rename to src/styles/base/colors.css diff --git a/src/styles/content.css b/src/styles/base/content.css similarity index 97% rename from src/styles/content.css rename to src/styles/base/content.css index cf063e7..231cfce 100644 --- a/src/styles/content.css +++ b/src/styles/base/content.css @@ -1,11 +1,7 @@ .content { - max-width: clamp(60ch, 90vw, 90ch); - margin: 0 auto; - padding: 0 var(--spacing-comfortable); - font-size: var(--typo-size-responsive); + @mixin responsive-wrapper; font-family: var(--font-body); /* === Headings === */ - & h1 { margin-block: var(--el-h1-vspace-top) var(--el-h1-vspace-bottom); padding-bottom: var(--spacing-snug); @@ -13,8 +9,8 @@ font-family: var(--el-h1-font-family), serif; font-size: var(--el-h1-font-size); - font-weight: 900; - line-height: 1.125; + font-weight: 400; + line-height: var(--typo-leading-tight); color: var(--el-h1-color); text-transform: uppercase; letter-spacing: -0.0137em; @@ -261,7 +257,7 @@ padding-left: var(--spacing-cozy); &::marker { - content: '⯃'; + content: '⎊'; color: var(--color-text-primary); } } @@ -272,7 +268,7 @@ } ul li::marker { - content: '⯁'; + content: '⋊'; color: var(--color-text-primary); } @@ -285,7 +281,7 @@ ul ul li::marker, ol ul li::marker { - content: '⯆'; + content: '◆'; color: var(--color-text-primary); } @@ -411,7 +407,7 @@ margin-block: var(--spacing-tight) var(--spacing-tight); border: var(--size-2) solid var(--color-text-primary); - font-size: var(--typo-size-sm); + font-size: var(--typo-size-md); line-height: 1.2; & thead th, @@ -514,13 +510,8 @@ em, i { - padding: 0.1em 0.2em; - - font-style: normal; - text-transform: uppercase; + font-style: italic; letter-spacing: 0.025em; - - background: var(--color-surface-elevated-1); } strong, diff --git a/src/styles/custom-media.css b/src/styles/base/custom-media.css similarity index 100% rename from src/styles/custom-media.css rename to src/styles/base/custom-media.css diff --git a/src/styles/base/dimensions.css b/src/styles/base/dimensions.css new file mode 100644 index 0000000..0e135f5 --- /dev/null +++ b/src/styles/base/dimensions.css @@ -0,0 +1,99 @@ +@layer tokens { + :root { + /* === DIMENSIONS === */ + + /* == Base Size Units == */ + --size-0: 0; + --size-px: 1px; + --size-05: 0.125rem; + --size-1: 0.25rem; + --size-2: 0.5rem; + --size-3: 0.75rem; + --size-4: 1rem; + --size-5: 1.25rem; + --size-6: 1.5rem; + --size-8: 2rem; + --size-10: 2.5rem; + --size-12: 3rem; + --size-16: 4rem; + --size-20: 5rem; + --size-24: 6rem; + --size-32: 8rem; + --size-40: 10rem; + --size-48: 12rem; + --size-64: 16rem; + --size-80: 20rem; + --size-96: 24rem; + --size-128: 32rem; + --size-160: 40rem; + --size-192: 48rem; + --size-256: 64rem; + --size-320: 80rem; + --size-360: 90rem; + --size-384: 96rem; + --size-400: 100rem; + --size-480: 120rem; + + /* == Flexible Dimensions == */ + --dim-full: 100%; + --dim-1-2: 50%; + --dim-1-3: 33.3333%; + --dim-2-3: 66.6667%; + --dim-1-4: 25%; + --dim-2-4: var(--dim-1-2); + --dim-3-4: 75%; + --dim-1-5: 20%; + --dim-2-5: 40%; + --dim-3-5: 60%; + --dim-4-5: 80%; + --dim-1-6: 16.6667%; + --dim-2-6: var(--dim-1-3); + --dim-3-6: var(--dim-1-2); + --dim-4-6: var(--dim-2-3); + --dim-5-6: 83.3333%; + --dim-1-8: 12.5%; + --dim-2-8: var(--dim-1-4); + --dim-3-8: 37.5%; + --dim-4-8: var(--dim-1-2); + --dim-5-8: 62.5%; + --dim-6-8: var(--dim-3-4); + --dim-7-8: 87.5%; + --dim-1-10: 10%; + --dim-2-10: var(--dim-1-5); + --dim-3-10: 30%; + --dim-4-10: var(--dim-2-5); + --dim-5-10: var(--dim-1-2); + --dim-6-10: var(--dim-3-5); + --dim-7-10: 70%; + --dim-8-10: var(--dim-4-5); + --dim-9-10: 90%; + --dim-1-12: 8.3333%; + --dim-2-12: var(--dim-1-6); + --dim-3-12: var(--dim-1-4); + --dim-4-12: var(--dim-1-3); + --dim-5-12: 41.6667%; + --dim-6-12: var(--dim-1-2); + --dim-7-12: 58.3333%; + --dim-8-12: var(--dim-2-3); + --dim-9-12: var(--dim-3-4); + --dim-10-12: 83.3333%; + --dim-11-12: 91.6667%; + + /* == Semantic Spacing == */ + --spacing-none: var(--size-0); + --spacing-hairline: var(--size-px); + --spacing-tight: var(--size-1); + --spacing-snug: var(--size-2); + --spacing-cozy: var(--size-4); + --spacing-comfortable: var(--size-6); + --spacing-relaxed: var(--size-8); + --spacing-spacious: var(--size-12); + --spacing-generous: var(--size-16); + --spacing-luxurious: var(--size-24); + --spacing-expansive: var(--size-32); + + /* == Responsive Content Dimensions */ + --layout-max-width: 90rem; + --content-max-width: 75ch; + } +} diff --git a/src/styles/elements.css b/src/styles/base/elements.css similarity index 94% rename from src/styles/elements.css rename to src/styles/base/elements.css index 1906ab6..76be2b9 100644 --- a/src/styles/elements.css +++ b/src/styles/base/elements.css @@ -4,23 +4,18 @@ --el-h1-color: var(--color-text-primary); --el-h1-font-family: var(--font-display); --el-h1-font-size: var(--typo-size-7xl); - --el-h2-color: var(--color-text-primary); --el-h2-font-family: var(--font-header); --el-h2-font-size: var(--typo-size-5xl); - --el-h3-color: var(--color-text-secondary); --el-h3-font-family: var(--font-header); --el-h3-font-size: var(--typo-size-4xl); - --el-h4-color: var(--color-text-secondary); --el-h4-font-family: var(--font-header); --el-h4-font-size: var(--typo-size-3xl); - --el-h5-color: var(--color-text-secondary); --el-h5-font-family: var(--font-header); --el-h5-font-size: var(--typo-size-2xl); - --el-h6-color: var(--color-text-secondary); --el-h6-font-family: var(--font-header); --el-h6-font-size: var(--typo-size-xl); @@ -31,25 +26,20 @@ --el-h1-vspace-bottom: calc( var(--el-h1-vspace-base) * var(--vspace-compressed) ); - --el-h2-vspace-base: calc(1em * 1.1765); --el-h2-vspace-top: calc(var(--el-h2-vspace-base) * var(--vspace-snug)); --el-h2-vspace-bottom: calc( var(--el-h2-vspace-base) * var(--vspace-compressed) ); - --el-h3-vspace-base: calc(1em * 1.2); --el-h3-vspace-top: calc(var(--el-h3-vspace-base) * var(--vspace-cozy)); --el-h3-vspace-bottom: calc(var(--el-h3-vspace-base) * var(--vspace-snug)); - --el-h4-vspace-base: calc(1em * 1.125); --el-h4-vspace-top: calc(var(--el-h4-vspace-base) * var(--vspace-normal)); --el-h4-vspace-bottom: calc(var(--el-h4-vspace-base) * var(--vspace-tight)); - --el-h5-vspace-base: calc(1em * 1.28); --el-h5-vspace-top: calc(var(--el-h5-vspace-base) * var(--vspace-cozy)); --el-h5-vspace-bottom: calc(var(--el-h5-vspace-base) * var(--vspace-tight)); - --el-h6-vspace-base: calc(1em * 1.4); --el-h6-vspace-top: calc(var(--el-h6-vspace-base) * var(--vspace-snug)); --el-h6-vspace-bottom: calc( @@ -129,3 +119,18 @@ --hr-symbol-color: var(--color-text-tertiary); --hr-symbol-background: var(--color-surface-base); } + +.responsivewrapper { + max-width: clamp(60ch, 90vw, 90ch); + margin-inline: auto; + padding: 0 var(--spacing-cozy); +} + +.layoutwrapper { + width: 100%; + margin-inline: auto; + + @media screen and (--max-layout) { + padding-inline: var(--spacing-comfortable); + } +} diff --git a/src/styles/base/fonts.css b/src/styles/base/fonts.css new file mode 100644 index 0000000..0cd9a25 --- /dev/null +++ b/src/styles/base/fonts.css @@ -0,0 +1,73 @@ +@import '@fontsource/blaka'; +@import '@fontsource-variable/geist-mono'; +@import '@fontsource-variable/geist'; + +@font-face { + font-family: 'Unigrim Dee'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/src/fonts/UnigrimDee-Regular.woff2') format('woff2'); +} + +@font-face { + font-family: 'Iosevka Slab'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Regular.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Iosevka Slab'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Bold.woff2') format('woff2'); +} + +@font-face { + font-family: 'Iosevka Slab'; + font-style: normal; + font-weight: 900; + font-display: swap; + font-variant-ligatures: normal; + src: url('/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Heavy.woff2') format('woff2'); +} + +@font-face { + font-family: 'Iosevka Slab'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url('/src/fonts/IosevkaSlabQp/IosevkaSlabQp-Italic.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Iosevka Slab'; + font-style: italic; + srcfont-weight: 700; + font-display: swap; + src: url('/src/fonts/IosevkaSlabQp/IosevkaSlabQp-BoldItalic.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Iosevka Slab'; + font-style: italic; + srcfont-weight: 900; + font-display: swap; + src: url('/src/fonts/IosevkaSlabQp/IosevkaSlabQp-HeavyItalic.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Iosevka Mono'; + font-style: normal; + srcfont-weight: 400; + font-display: swap; + src: url('/src/fonts/IosevkaSansMono/IosevkaSansMono-Regular.woff2') + format('woff2'); +} diff --git a/src/styles/foundation.css b/src/styles/base/foundation.css similarity index 100% rename from src/styles/foundation.css rename to src/styles/base/foundation.css diff --git a/src/styles/typography.css b/src/styles/base/typography.css similarity index 66% rename from src/styles/typography.css rename to src/styles/base/typography.css index 703ecce..e49c004 100644 --- a/src/styles/typography.css +++ b/src/styles/base/typography.css @@ -1,10 +1,10 @@ :root { /* === Font Families === */ - --font-header: 'Geist Variable'; - --font-display: 'Blaka'; - --font-body: 'Geist Mono Variable'; - --font-mono: 'Geist Mono Variable'; - --font-symbols: 'Unigrim Dee'; + --font-header: 'Geist Variable', sans; + --font-display: 'Blaka',serif; + --font-body: 'Iosevka Slab', sans; + --font-mono: 'Iosevka Mono', monospace; + --font-symbols: 'Unigrim Dee', fantasy; /* === Type Scale === */ --typo-size-responsive: clamp(1rem, 2.5vw, 1.25rem); @@ -23,16 +23,16 @@ --typo-size-xs: 0.625em; --typo-size-2xs: 0.5em; - /* === Font Weights === */ - --typo-weight-thin: 100; - --typo-weight-extralight: 200; - --typo-weight-light: 300; - --typo-weight-normal: 400; - --typo-weight-medium: 500; - --typo-weight-semibold: 600; - --typo-weight-bold: 700; - --typo-weight-extrabold: 800; - --typo-weight-black: 900; + /* == Letter Spacing == */ + --typo-spacing-tightest: -0.05em; + --typo-spacing-tighter: -0.025em; + --typo-spacing-tight: -0.0125em; + --typo-spacing-normal: 0em; + --typo-spacing-relaxed: 0.025em; + --typo-spacing-comfortable: 0.05em; + --typo-spacing-loose: 0.1em; + --typo-spacing-looser: 0.15em; + --typo-spacing-loosest: 0.2em; /* === Line Height === */ --typo-leading-compressed: 1; diff --git a/src/styles/base/ui.css b/src/styles/base/ui.css new file mode 100644 index 0000000..aeac701 --- /dev/null +++ b/src/styles/base/ui.css @@ -0,0 +1,35 @@ +/* UI Specific Variables */ +:root { + /* Spacing */ + --ui-spacing-hairline: 0.0625rem; + --ui-spacing-tight: 0.125rem; + --ui-spacing-snug: 0.25rem; + --ui-spacing-cozy: 0.375rem; + --ui-spacing-comfortable:0.5rem; + --ui-spacing-relaxed: 0.625rem; + --ui-spacing-spacious: 0.75rem; + --ui-spacing-generous: 0.875rem; + --ui-spacing-luxurious: 1rem; + --ui-spacing-expansive: 1.25rem; + + /* Font Size */ + --ui-typo-size-2xs: 0.5625rem; + --ui-typo-size-xs: 0.625rem; + --ui-typo-size-sm: 0.6875rem; + --ui-typo-size-md: 0.8125rem; + --ui-typo-size-lg: 0.875rem; + --ui-typo-size-xl: 1rem; + --ui-typo-size-2xl: 1.25rem; + --ui-typo-size-3xl: 1.5rem; + --ui-typo-size-4xl: 1.75rem; + --ui-typo-size-5xl: 2rem; + + /* === MastHead === */ + --el-masthead-font-size: var(--ui-font-size-xl); + --el-masthead-line-height: var(--typo-leading-snug); + --el-masthead-paddingY: var(--ui-spacing-relaxed); + --el-masthead-height: calc( + (var(--el-masthead-font-size) * var(--el-masthead-line-height)) + + (var(--el-masthead-paddingY) * 2) + ); +} diff --git a/src/styles/dimensions.css b/src/styles/dimensions.css deleted file mode 100644 index 75fce36..0000000 --- a/src/styles/dimensions.css +++ /dev/null @@ -1,42 +0,0 @@ -:root { - /* === Size Scale === */ - --size-0: 0; - --size-px: 1px; - --size-05: 0.125rem; - --size-1: 0.25rem; - --size-2: 0.5rem; - --size-3: 0.75rem; - --size-4: 1rem; - --size-5: 1.25rem; - --size-6: 1.5rem; - --size-8: 2rem; - --size-10: 2.5rem; - --size-12: 3rem; - --size-16: 4rem; - --size-20: 5rem; - --size-24: 6rem; - --size-32: 8rem; - --size-40: 10rem; - --size-48: 12rem; - --size-64: 16rem; - --size-80: 20rem; - --size-96: 24rem; - --size-128: 32rem; - --size-160: 40rem; - --size-192: 48rem; - --size-256: 64rem; - --size-320: 80rem; - - /* === Semantic Spacing === */ - --spacing-none: var(--size-0); - --spacing-hairline: var(--size-px); - --spacing-tight: var(--size-1); - --spacing-snug: var(--size-2); - --spacing-cozy: var(--size-4); - --spacing-comfortable: var(--size-6); - --spacing-relaxed: var(--size-8); - --spacing-spacious: var(--size-12); - --spacing-generous: var(--size-16); - --spacing-luxurious: var(--size-24); - --spacing-expansive: var(--size-32); -} diff --git a/src/styles/fonts.css b/src/styles/fonts.css deleted file mode 100644 index ccf7a4c..0000000 --- a/src/styles/fonts.css +++ /dev/null @@ -1,11 +0,0 @@ -@import '@fontsource/blaka'; -@import '@fontsource-variable/geist-mono'; -@import '@fontsource-variable/geist'; - -@font-face { - font-family: 'Unigrim Dee'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('../fonts/UnigrimDee-Regular.woff2') format('woff2'); -} diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..726d946 --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,13 @@ +@import url('./base/fonts.css'); +@import url('./base/foundation.css'); +@import url('./base/colors.css'); +@import url('./base/dimensions.css'); +@import url('./base/typography.css'); +@import url('./base/elements.css'); +@import url('./base/custom-media.css'); +@import url('./base/content.css'); +@import url('./base/ui.css'); + +body { + font-variant-ligatures: discretionary-ligatures; +} diff --git a/src/keystatic/fields/pages.ts b/src/styles/mixins/animations.css similarity index 100% rename from src/keystatic/fields/pages.ts rename to src/styles/mixins/animations.css diff --git a/src/styles/mixins/borders.css b/src/styles/mixins/borders.css new file mode 100644 index 0000000..ed98a45 --- /dev/null +++ b/src/styles/mixins/borders.css @@ -0,0 +1,55 @@ +@define-mixin border-l $width: var(--size-1), $style: solid, + $color: var(--color-surface-inverse) { + border-left: $width $style $color; +} + +@define-mixin border-r $width: var(--size-1), $style: solid, + $color: var(--color-surface-inverse) { + border-right: $width $style $color; +} + +@define-mixin border-t $width: var(--size-1), $style: solid, + $color: var(--color-surface-inverse) { + border-top: $width $style $color; +} + +@define-mixin border-b $width: var(--size-1), $style: solid, + $color: var(--color-surface-inverse) { + border-bottom: $width $style $color; +} + +@define-mixin border-x $width: var(--size-1), $style: solid, + $color: var(--color-surface-inverse) { + border-right: $width $style $color; + border-left: $width $style $color; +} + +@define-mixin border-y $width: var(--size-1), $style: solid, + $color: var(--color-surface-inverse) { + border-top: $width $style $color; + border-bottom: $width $style $color; +} + +@define-mixin rounded-top $radius { + border-top-left-radius: $radius; + border-top-right-radius: $radius; +} + +@define-mixin rounded-bottom $radius { + border-bottom-right-radius: $radius; + border-bottom-left-radius: $radius; +} + +@define-mixin rounded-left $radius { + border-top-left-radius: $radius; + border-bottom-left-radius: $radius; +} + +@define-mixin rounded-right $radius { + border-top-left-radius: $radius; + border-bottom-left-radius: $radius; +} + +@define-mixin circle { + border-radius: 50%; +} diff --git a/src/styles/mixins/containers.css b/src/styles/mixins/containers.css new file mode 100644 index 0000000..e53fa02 --- /dev/null +++ b/src/styles/mixins/containers.css @@ -0,0 +1,25 @@ +@define-mixin responsive-wrapper $vspacing: 0, $hspacing: var(--spacing-cozy), + $fontSize: var(--typo-size-responsive) { + @mixin mx auto; + + max-width: clamp(60ch, 90vw, 90ch); + padding: $vspacing $hspacing; + font-family: var(--font-body); + font-size: $fontSize; +} + +@define-mixin layout-wrapper { + @mixin mx auto; + @mixin px var(--spacing-comfortable); + width: 100%; + max-width: var(--layout-max-width); + + @media screen and (--max-layout) { + @mixin px var(--spacing-comfortable); + } +} + +@define-mixin position $position: absolute, $inset-values { + position: $position; + inset: $inset-values; +} diff --git a/src/styles/mixins/grid.css b/src/styles/mixins/grid.css new file mode 100644 index 0000000..b255d95 --- /dev/null +++ b/src/styles/mixins/grid.css @@ -0,0 +1,19 @@ +@define-mixin grid-col $col, $spacing { + display: grid; + grid-template-columns: repeat($col, minmax(0, 1fr)); + gap: $spacing; +} + +@define-mixin grid-rows $row, $spacing { + display: grid; + grid-template-rows: repeat(1, minmax(0, 1fr)); + gap: $spacing; +} + +@define-mixin grid-col-span $span { + grid-column: span $span / span $span; +} + +@define-mixin grid-row-span $span { + grid-row: span $span / span $span; +} diff --git a/src/styles/mixins/spacing.css b/src/styles/mixins/spacing.css new file mode 100644 index 0000000..f0eeeab --- /dev/null +++ b/src/styles/mixins/spacing.css @@ -0,0 +1,85 @@ +@define-mixin ma $spacing { + margin: $spacing; +} + +@define-mixin mt $spacing { + margin-top: $spacing; +} + +@define-mixin mb $spacing { + margin-bottom: $spacing; +} + +@define-mixin ml $spacing { + margin-left: $spacing; +} + +@define-mixin mr $spacing { + margin-right: $spacing; +} + +@define-mixin my $spacing { + margin-block: $spacing; +} + +@define-mixin mx $spacing { + margin-inline: $spacing; +} + +@define-mixin ms $spacing { + margin-line-start: $spacing; +} + +@define-mixin me $spacing { + margin-line-end: $spacing; +} + +@define-mixin space-x $spacing $reverse: 0 { + & > :not(:last-child) { + margin-inline-start: calc($spacing * $reverse); + margin-inline-end: calc($spacing * (1 - $reverse)); + } +} + +@define-mixin pa $spacing { + padding: $spacing; +} + +@define-mixin pt $spacing { + padding-top: $spacing; +} + +@define-mixin pb $spacing { + padding-bottom: $spacing; +} + +@define-mixin pl $spacing { + padding-left: $spacing; +} + +@define-mixin pr $spacing { + padding-right: $spacing; +} + +@define-mixin py $spacing { + padding-block: $spacing; +} + +@define-mixin px $spacing { + padding-inline: $spacing; +} + +@define-mixin ps $spacing { + padding-line-start: $spacing; +} + +@define-mixin pe $spacing { + padding-line-end: $spacing; +} + +@define-mixin space-y $spacing, $reverse: 0 { + & > :not(:last-child) { + margin-block-start: calc($spacing * $reverse); + margin-block-end: calc($spacing * (1 - $reverse)); + } +} diff --git a/stylelint.config.mjs b/stylelint.config.mjs index a1ba389..55cbdd9 100644 --- a/stylelint.config.mjs +++ b/stylelint.config.mjs @@ -1,6 +1,7 @@ const stylelintConfig = { extends: [ 'stylelint-config-standard', + 'stylelint-config-astro', 'stylelint-config-clean-order', 'stylelint-config-html', ], diff --git a/tsconfig.json b/tsconfig.json index b7243b9..16fe52b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,17 @@ "extends": "astro/tsconfigs/strict", "compilerOptions": { "jsx": "react-jsx", - "jsxImportSource": "react" + "jsxImportSource": "preact", + "paths": { + "@layouts/*": ["./src/layouts/*"], + "@fonts/*": ["./src/fonts/*"], + "@pages/*": ["./src/pages/*"], + "@styles/*": ["./src/styles/*"], + "@compontents/*": ["./src/components/*"], + "@collections/*": ["./src/keystatic/collections/*"], + "@fields/*": ["./src/keystatic/fields/*"], + "@lib/*": ["./src/lib/*"], + "@/*": ["./src/*"] + } } }