Added Masthead, CMDPalette and PostHeader Components
This commit is contained in:
72
src/components/content/ElementSymbol.astro
Normal file
72
src/components/content/ElementSymbol.astro
Normal file
@@ -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' && (
|
||||
<span
|
||||
class="element font"
|
||||
style={`font-family: '${(symbol as FontSymbol).value.family}', sans-serif; font-size: ${size}; color: ${color};`}
|
||||
>
|
||||
{(symbol as FontSymbol).value.character}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
symbol?.discriminant === 'svg' && (
|
||||
<span
|
||||
class="element svg"
|
||||
style={`width: ${size}; height: ${size}; color: ${color}`}
|
||||
set:html={(symbol as SVGSymbol).value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<style>
|
||||
.font {
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.svg {
|
||||
display: inline-flex;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.svg :global(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
||||
211
src/components/layout/CMDPalette/CMDPalette.module.css
Normal file
211
src/components/layout/CMDPalette/CMDPalette.module.css
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
287
src/components/layout/CMDPalette/index.tsx
Normal file
287
src/components/layout/CMDPalette/index.tsx
Normal file
@@ -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<ContentType, string> = {
|
||||
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<IndexedEntry[]>([]);
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
|
||||
const selectedRef = useRef(-1);
|
||||
const loadingRef = useRef(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const resultsRef = useRef<HTMLDivElement>(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 (
|
||||
<>
|
||||
<button
|
||||
class={styles.trigger}
|
||||
onClick={open}
|
||||
aria-label="Open Navigation"
|
||||
>
|
||||
<span>Navigate…</span>
|
||||
<kbd class={styles.kbd}>⌘K</kbd>
|
||||
</button>
|
||||
{isOpen && <div class={styles.backdrop} onClick={close} />}
|
||||
{isOpen && (
|
||||
<div
|
||||
class={styles.palette}
|
||||
role="dialog"
|
||||
aria-label="Navigation palette"
|
||||
>
|
||||
<div class={styles.header}>
|
||||
<span class={styles.icon}>⟩</span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
class={styles.input}
|
||||
type="text"
|
||||
placeholder="Where do you want to go?"
|
||||
autocomplete="off"
|
||||
spellcheck={false}
|
||||
value={query}
|
||||
onInput={(e) => setQuery((e.target as HTMLInputElement).value)}
|
||||
onKeyDown={onInputKeydown}
|
||||
/>
|
||||
<kbd class={styles.esc}>esc</kbd>
|
||||
</div>
|
||||
<div class={styles.results} ref={resultsRef} role="listbox">
|
||||
{rows.map((row) =>
|
||||
row.kind === 'label' ? (
|
||||
<div key={row.key} class={styles.groupLabel}>
|
||||
{row.group}
|
||||
</div>
|
||||
) : (
|
||||
<a
|
||||
key={row.key}
|
||||
class={`${styles.result}${row.index === selectedIndex ? ` ${styles.selected}` : ''}`}
|
||||
href={row.entry!.path}
|
||||
role="option"
|
||||
data-index={row.index}
|
||||
aria-selected={row.index === selectedIndex}
|
||||
>
|
||||
<span class={styles.type}>
|
||||
{TYPE_LABELS[row.entry!.type] ?? row.entry!.type}
|
||||
</span>
|
||||
<span class={styles.label}>{row.entry!.label}</span>
|
||||
<span class={styles.path}>{row.entry!.path}</span>
|
||||
<span class={styles.arrow}>→</span>
|
||||
</a>
|
||||
),
|
||||
)}
|
||||
{resultCount === 0 && (
|
||||
<div class={styles.empty}>No results found</div>
|
||||
)}
|
||||
</div>
|
||||
<div class={styles.footer}>
|
||||
<div class={styles.group}>
|
||||
<span>
|
||||
<kbd>↑</kbd>
|
||||
<kbd>↓</kbd> navigate
|
||||
</span>
|
||||
<span>
|
||||
<kbd>↵</kbd> open
|
||||
</span>
|
||||
</div>
|
||||
<span>
|
||||
<kbd>esc</kbd> close
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
129
src/components/layout/MastHead.astro
Normal file
129
src/components/layout/MastHead.astro
Normal file
@@ -0,0 +1,129 @@
|
||||
---
|
||||
import CommandPalette from "./CMDPalette"
|
||||
---
|
||||
|
||||
<header class="site-header">
|
||||
<div class="inner">
|
||||
<a href="/" class="link">
|
||||
<span class="site-logo">◬</span>
|
||||
<span class="site-name">
|
||||
dave
|
||||
<span class="bracket">[</span>
|
||||
dmg
|
||||
<span class="bracket">]</span>
|
||||
</span>
|
||||
</a>
|
||||
<CommandPalette client:load />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.site-header {
|
||||
@mixin py var(--el-masthead-paddingY);
|
||||
|
||||
position: sticky;
|
||||
z-index: 9;
|
||||
top: 0;
|
||||
|
||||
width: 100%;
|
||||
|
||||
color: var(--color-text-inverse);
|
||||
|
||||
background-color: var(--color-surface-inverse);
|
||||
}
|
||||
|
||||
.inner {
|
||||
@mixin layout-wrapper;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--ui-spacing-cozy);
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
font-size: var(--el-masthead-font-size);
|
||||
line-height: var(--el-masthead-line-height);
|
||||
}
|
||||
|
||||
.site-logo {
|
||||
display: inline-block;
|
||||
font-family: var(--font-mono);
|
||||
animation:
|
||||
logo-pulse 5s cubic-bezier(0.4, 0, 0.6, 1) infinite,
|
||||
logo-glitch 13s step-end infinite;
|
||||
}
|
||||
|
||||
.site-name {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.bracket {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
@keyframes logo-pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
25% {
|
||||
opacity: 0.66;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.33;
|
||||
}
|
||||
|
||||
75% {
|
||||
opacity: 0.66;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes logo-glitch {
|
||||
0%,
|
||||
84% {
|
||||
transform: translate(0, 0);
|
||||
filter: brightness(1);
|
||||
}
|
||||
|
||||
85% {
|
||||
transform: translate(-2px, 0);
|
||||
filter: brightness(0.4);
|
||||
}
|
||||
|
||||
86% {
|
||||
transform: translate(1px, -1px);
|
||||
filter: brightness(0.2);
|
||||
}
|
||||
|
||||
87% {
|
||||
transform: translate(-1px, 1px);
|
||||
filter: brightness(0.7);
|
||||
}
|
||||
|
||||
88% {
|
||||
transform: translate(2px, 0);
|
||||
filter: brightness(0.1);
|
||||
}
|
||||
|
||||
89% {
|
||||
transform: translate(-2px, 1px);
|
||||
filter: brightness(0.5);
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: translate(0, -1px);
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
|
||||
91%,
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
filter: brightness(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
75
src/components/layout/PostHeader/Cover.astro
Normal file
75
src/components/layout/PostHeader/Cover.astro
Normal file
@@ -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;
|
||||
---
|
||||
|
||||
<figure class="cover">
|
||||
{
|
||||
image ? (
|
||||
<Image
|
||||
class="image"
|
||||
src={image}
|
||||
alt={alt}
|
||||
widths={[600, 900, 1280, 1440, 1600, 1920]}
|
||||
sizes="100vw"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<img class="image" src={src} alt={alt} loading="lazy" />
|
||||
)
|
||||
}
|
||||
<figcaption class="caption">
|
||||
<div class="wrapper">
|
||||
{
|
||||
caption ? (
|
||||
<span class="caption-text">{caption}</span>
|
||||
) : (
|
||||
<span class="caption-meta">{src}</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<style>
|
||||
.cover {
|
||||
@mixin border-t 12px, solid, var(--color-surface-inverse);
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 60vw;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.caption {
|
||||
padding: var(--spacing-snug);
|
||||
font-family: var(--font-mono);
|
||||
color: var(--color-text-inverse);
|
||||
background-color: var(--color-surface-inverse);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
font-size: var(font-size-responsive);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.caption-text,
|
||||
.caption-meta {
|
||||
font-size: var(--typo-size-sm);
|
||||
}
|
||||
</style>
|
||||
78
src/components/layout/PostHeader/Meta.astro
Normal file
78
src/components/layout/PostHeader/Meta.astro
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
interface Props {
|
||||
tags: string[];
|
||||
updateDate?: string;
|
||||
}
|
||||
|
||||
const { tags, updateDate } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="meta">
|
||||
<div class="section">
|
||||
<span class="label"> Author </span>
|
||||
<span class="author item">Dave Damage</span>
|
||||
</div>
|
||||
{
|
||||
tags && tags.length > 0 && (
|
||||
<div class="section">
|
||||
<span class="label">Tags</span>
|
||||
<ul class="taglist">
|
||||
{tags.map((tag) => (
|
||||
<li class="tag item">{tag}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
updateDate && (
|
||||
<div class="section">
|
||||
<span class="label">Last Update</span>
|
||||
<time class="updatedate item">{updateDate}</time>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<style>
|
||||
.meta {
|
||||
@mixin layout-wrapper;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--spacing-relaxed);
|
||||
}
|
||||
.section {
|
||||
min-width: 12ch;
|
||||
@mixin py var(--spacing-snug);
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
.label {
|
||||
@mixin mb var(--spacing-tight);
|
||||
@mixin pb var(--spacing-tight);
|
||||
@mixin border-b 2px, solid, var(--color-border-strong);
|
||||
display: block;
|
||||
font-size: var(--typo-size-xs);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--typo-spacing-comfortable);
|
||||
}
|
||||
.item {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 700;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.taglist {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-tight);
|
||||
}
|
||||
.tag {
|
||||
@mixin py var(--spacing-tight);
|
||||
@mixin px var(--spacing-snug);
|
||||
border: 4px solid var(--color-palette-charcoal-gray);
|
||||
letter-spacing: var(--typo-spacing-relaxed);
|
||||
background: var(--color-palette-charcoal-gray);
|
||||
color: var(--color-palette-off-white);
|
||||
}
|
||||
</style>
|
||||
68
src/components/layout/PostHeader/Overline.astro
Normal file
68
src/components/layout/PostHeader/Overline.astro
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
import type { BreadcrumbSegment } from '@lib/types/content';
|
||||
|
||||
interface Props {
|
||||
breadcrumbs: BreadcrumbSegment[];
|
||||
publicationDate: string;
|
||||
}
|
||||
|
||||
const { breadcrumbs, publicationDate } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="overline">
|
||||
<div class="wrapper">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumbs">
|
||||
<li class="crumb">
|
||||
<a href="/" class="link">dave-dmg.de</a>
|
||||
</li>
|
||||
{
|
||||
breadcrumbs.map((crumb) => (
|
||||
<li class="crumb">
|
||||
<span class="separator">/</span>
|
||||
<a href={crumb.href} class="link">
|
||||
{crumb.label}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ol>
|
||||
</nav>
|
||||
<time class="publicationdate">
|
||||
⟫ {publicationDate}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.overline {
|
||||
background-color: var(--color-surface-inverse);
|
||||
}
|
||||
.wrapper {
|
||||
@mixin layout-wrapper;
|
||||
padding-block: var(--spacing-snug);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: var(--typo-size-responsive);
|
||||
}
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--typo-size-sm);
|
||||
color: var(--color-text-inverse);
|
||||
}
|
||||
.link {
|
||||
color: var(--color-text-inverse);
|
||||
transition: color 0.5s ease-in-out;
|
||||
&:hover {
|
||||
color: var(--color-tertiary);
|
||||
}
|
||||
}
|
||||
.publicationdate {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--typo-size-xs);
|
||||
font-weight: var(--typo-weight-bold);
|
||||
color: var(--color-text-inverse);
|
||||
}
|
||||
</style>
|
||||
38
src/components/layout/PostHeader/Title.astro
Normal file
38
src/components/layout/PostHeader/Title.astro
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
const { title, subtitle } = Astro.props;
|
||||
|
||||
console.log(subtitle);
|
||||
---
|
||||
|
||||
<div class="wrapper">
|
||||
<h1 class="title">{title}</h1>
|
||||
{subtitle && <p class="subtitle">{subtitle}</p>}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
@mixin layout-wrapper;
|
||||
@mixin py var(--spacing-relaxed);
|
||||
}
|
||||
.title {
|
||||
font-size: var(--el-h1-color);
|
||||
font-family: var(--el-h1-font-family);
|
||||
font-size: var(--el-h1-font-size);
|
||||
line-height: var(--typo-leading-tight);
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@mixin mt var(--spacing-snug);
|
||||
font-family: var(--font-header);
|
||||
font-size: var(--typo-size-lg);
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.075em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
46
src/components/layout/PostHeader/index.astro
Normal file
46
src/components/layout/PostHeader/index.astro
Normal file
@@ -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);
|
||||
---
|
||||
|
||||
<header class="wrapper">
|
||||
<Overline breadcrumbs={breadcrumbs} publicationDate={publishDate} />
|
||||
<Title title={title} subtitle={subtitle} />
|
||||
{
|
||||
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>
|
||||
BIN
src/content/articles/alchemical-materialism/cover/src.png
Normal file
BIN
src/content/articles/alchemical-materialism/cover/src.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
309
src/content/articles/alchemical-materialism/index.mdoc
Normal file
309
src/content/articles/alchemical-materialism/index.mdoc
Normal file
@@ -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 %}
|
||||
13
src/content/articles/awq/index.mdoc
Normal file
13
src/content/articles/awq/index.mdoc
Normal file
@@ -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
|
||||
---
|
||||
13
src/content/articles/chainbreaker/index.mdoc
Normal file
13
src/content/articles/chainbreaker/index.mdoc
Normal file
@@ -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
|
||||
---
|
||||
14
src/content/articles/elements/index.mdoc
Normal file
14
src/content/articles/elements/index.mdoc
Normal file
@@ -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
|
||||
---
|
||||
23
src/content/articles/framework/index.mdoc
Normal file
23
src/content/articles/framework/index.mdoc
Normal file
@@ -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
|
||||
20
src/content/articles/materia/index.mdoc
Normal file
20
src/content/articles/materia/index.mdoc
Normal file
@@ -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
|
||||
17
src/content/articles/prima-materia/index.mdoc
Normal file
17
src/content/articles/prima-materia/index.mdoc
Normal file
@@ -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
|
||||
---
|
||||
14
src/content/articles/references/index.mdoc
Normal file
14
src/content/articles/references/index.mdoc
Normal file
@@ -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
|
||||
---
|
||||
@@ -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**
|
||||
|
||||
@@ -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 };
|
||||
|
||||
52
src/content/crucible/elements/aether/index.mdoc
Normal file
52
src/content/crucible/elements/aether/index.mdoc
Normal file
@@ -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
|
||||
50
src/content/crucible/elements/air/index.mdoc
Normal file
50
src/content/crucible/elements/air/index.mdoc
Normal file
@@ -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
|
||||
23
src/content/crucible/elements/art/index.mdoc
Normal file
23
src/content/crucible/elements/art/index.mdoc
Normal file
@@ -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
|
||||
49
src/content/crucible/elements/body/index.mdoc
Normal file
49
src/content/crucible/elements/body/index.mdoc
Normal file
@@ -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
|
||||
50
src/content/crucible/elements/earth/index.mdoc
Normal file
50
src/content/crucible/elements/earth/index.mdoc
Normal file
@@ -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
|
||||
23
src/content/crucible/elements/entities/index.mdoc
Normal file
23
src/content/crucible/elements/entities/index.mdoc
Normal file
@@ -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
|
||||
47
src/content/crucible/elements/fire/index.mdoc
Normal file
47
src/content/crucible/elements/fire/index.mdoc
Normal file
@@ -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
|
||||
23
src/content/crucible/elements/forces/index.mdoc
Normal file
23
src/content/crucible/elements/forces/index.mdoc
Normal file
@@ -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
|
||||
23
src/content/crucible/elements/matter/index.mdoc
Normal file
23
src/content/crucible/elements/matter/index.mdoc
Normal file
@@ -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
|
||||
23
src/content/crucible/elements/mind/index.mdoc
Normal file
23
src/content/crucible/elements/mind/index.mdoc
Normal file
@@ -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
|
||||
23
src/content/crucible/elements/mysteries/index.mdoc
Normal file
23
src/content/crucible/elements/mysteries/index.mdoc
Normal file
@@ -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
|
||||
23
src/content/crucible/elements/society/index.mdoc
Normal file
23
src/content/crucible/elements/society/index.mdoc
Normal file
@@ -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
|
||||
46
src/content/crucible/elements/soul/index.mdoc
Normal file
46
src/content/crucible/elements/soul/index.mdoc
Normal file
@@ -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
|
||||
57
src/content/crucible/elements/spirit/index.mdoc
Normal file
57
src/content/crucible/elements/spirit/index.mdoc
Normal file
@@ -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
|
||||
50
src/content/crucible/elements/water/index.mdoc
Normal file
50
src/content/crucible/elements/water/index.mdoc
Normal file
@@ -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
|
||||
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-Bold.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-Bold.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-BoldItalic.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-BoldOblique.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-BoldOblique.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraBold.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraBoldItalic.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraBoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraBoldOblique.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraBoldOblique.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraLight.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraLight.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraLightItalic.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraLightItalic.woff2
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-Heavy.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-Heavy.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-HeavyItalic.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-HeavyItalic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-HeavyOblique.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-HeavyOblique.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-Italic.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-Italic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-Light.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-Light.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-LightItalic.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-LightItalic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-LightOblique.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-LightOblique.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-Medium.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-Medium.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-MediumItalic.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-MediumItalic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-MediumOblique.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-MediumOblique.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-Oblique.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-Oblique.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-Regular.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-Regular.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-SemiBold.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-SemiBold.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-SemiBoldItalic.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-SemiBoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-SemiBoldOblique.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-SemiBoldOblique.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-Thin.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-Thin.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-ThinItalic.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-ThinItalic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-ThinOblique.woff2
Normal file
BIN
src/fonts/IosevkaSansMono/IosevkaSansMono-ThinOblique.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-Bold.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-Bold.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-BoldItalic.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-BoldOblique.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-BoldOblique.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraBold.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraBoldItalic.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraBoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraBoldOblique.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraBoldOblique.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraLight.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraLight.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraLightItalic.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraLightItalic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraLightOblique.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-ExtraLightOblique.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-Heavy.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-Heavy.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-HeavyItalic.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-HeavyItalic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-HeavyOblique.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-HeavyOblique.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-Italic.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-Italic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-Light.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-Light.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-LightItalic.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-LightItalic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-LightOblique.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-LightOblique.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-Medium.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-Medium.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-MediumItalic.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-MediumItalic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-MediumOblique.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-MediumOblique.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-Oblique.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-Oblique.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-Regular.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-Regular.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-SemiBold.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-SemiBold.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-SemiBoldItalic.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-SemiBoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-SemiBoldOblique.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-SemiBoldOblique.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-Thin.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-Thin.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-ThinItalic.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-ThinItalic.woff2
Normal file
Binary file not shown.
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-ThinOblique.woff2
Normal file
BIN
src/fonts/IosevkaSlabQp/IosevkaSlabQp-ThinOblique.woff2
Normal file
Binary file not shown.
@@ -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',
|
||||
|
||||
66
src/keystatic/collections/crucible/elements.ts
Normal file
66
src/keystatic/collections/crucible/elements.ts
Normal file
@@ -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',
|
||||
}),
|
||||
}),
|
||||
},
|
||||
),
|
||||
},
|
||||
});
|
||||
7
src/keystatic/collections/crucible/index.ts
Normal file
7
src/keystatic/collections/crucible/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { elements } from './elements';
|
||||
|
||||
const crucibleCollections = {
|
||||
cr_elements: elements,
|
||||
};
|
||||
|
||||
export default crucibleCollections;
|
||||
@@ -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',
|
||||
|
||||
50
src/keystatic/components/element.ts
Normal file
50
src/keystatic/components/element.ts
Normal file
@@ -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;
|
||||
5
src/keystatic/components/index.ts
Normal file
5
src/keystatic/components/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import elementComponent from './element.ts';
|
||||
|
||||
export const generalComponents = {
|
||||
...elementComponent,
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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}</title>
|
||||
<title>{pageTitle}</title>
|
||||
{pageDescription && <meta name="description" content={pageDescription} />}
|
||||
{seo?.noIndex && <meta name="robots" content="noindex" />}
|
||||
</head>
|
||||
<body>
|
||||
<MastHead />
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
58
src/layouts/ContentLayout.astro
Normal file
58
src/layouts/ContentLayout.astro
Normal file
@@ -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;
|
||||
---
|
||||
|
||||
<Base title={title} seo={seo}>
|
||||
<main>
|
||||
<article>
|
||||
<Header
|
||||
title={title}
|
||||
breadcrumbs={breadcrumbs}
|
||||
publishDate={formattedPublishDate}
|
||||
updateDate={formattedUpdateDate}
|
||||
cover={headerCover}
|
||||
tags={tags}
|
||||
subtitle={subtitle}
|
||||
/>
|
||||
<div class="content">
|
||||
<slot name="before-content" />
|
||||
<Content />
|
||||
<slot name="after-content" />
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
</Base>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user