18 Commits

Author SHA1 Message Date
ce885d8eae Added AWQ Articles 2025-10-13 22:44:31 +02:00
0bca3573f7 Added Callout Component 2025-10-05 18:14:52 +02:00
b93f846156 Added Figure Component 2025-10-05 15:45:42 +02:00
5cb4bd5782 Added Blockquote Component 2025-10-05 10:52:40 +02:00
3a79f59f03 Added Accordion and AccordionItem Components 2025-10-03 17:57:41 +02:00
70e226057a Added DefinitionList and DefinitionItem components 2025-10-03 14:31:02 +02:00
8f78c26b78 Added simple MD support for sidenote content 2025-10-03 09:29:29 +02:00
e4b72005f2 Added fallback for Firefox for Sidenotes 2025-10-02 18:59:28 +02:00
2e30b1d6ed Added ArticleHeader 2025-10-02 18:22:50 +02:00
ae6da529cf Fixed scroll-margin-top for sidenote-ref 2025-10-02 14:33:27 +02:00
fdcfc774fb Fixed linting issues 2025-10-02 14:24:17 +02:00
054d450273 Added Sidenote Component 2025-10-02 14:18:46 +02:00
0111cd71fe Merge branch 'feature/ui-components'
Some checks failed
Build and Deploy DAVE | DMGs Site / deploy (push) Failing after 1m28s
2025-09-29 17:18:52 +02:00
9f10721104 Added Entry and Exit animation for menu 2025-09-29 17:17:32 +02:00
bfbe687b63 Added WIP Menu 2025-09-29 15:20:46 +02:00
1773687814 Added Header Component 2025-09-17 13:14:24 +02:00
6e2ab0a88b Added MenuContext for handling navigation 2025-09-17 11:27:35 +02:00
d80c4917de Merge branch 'feature/tailwind-theme'
All checks were successful
Build and Deploy DAVE | DMGs Site / deploy (push) Successful in 1m15s
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2025-09-16 14:08:35 +02:00
150 changed files with 7141 additions and 1526 deletions

View File

@@ -0,0 +1,86 @@
---
title: Anatomy of a Player Character
path: awq
cover:
showInHeader: false
meta:
publicationDate: 2025-10-08T08:43:00.000Z
status: draft
isFeatured: false
tags: []
seo:
noIndex: false
---
Every model shares some common abilities
## Main Characteristics
Characteristics define the innate abilities of a model, and each characteristic has two skills associated with it
- **Weapon Skill (WS):** Aptitude in landing and avoiding blows in close combat; governs _Melee_ and _Defence_
- **Ballistic Skill (BS):** Capability of Hand-Eye-Coordination; governs _Skirmish_ and _Evasion_
- **Strength (S):** Used for brute force, stamina and might; governs _Brawn_ and _Toil_
- **Toughness (T):** Resistance to physical Trauma; governs _Consume Alcohol_ and _Endurance_
- **Initiative(I):** Reflects speed of thought and perception; governs _Perception_ and _Outdoor Survival_
- **Dexterity (Dex):** Affinity for performing fine and delicate manual tasks; governs _Skullduggery_ and _Tradecraft_
- **Agility (Ag):** Physical coordination and natural athleticism; governs _Stealth_ and _Athletics_
- **Intelligence (Int):** Power of thought, analysis, and understanding; governs _Intuition_ and _Education_
- **Willpower (WP):** General strength if mind; governs _Cool_ and _Animal Handling_
- **Fellowship (Fel):** Ability to get on with people; governs _Leadership_ and _Charm_
## Secondary Characteristics
- **Corruption Threshold (CT):[^*]** Ability to withstand _Mutation_
- **Insanity Threshold (IT):[^*]** Ability to withstand _Insanity_
- **Wounds (W):** Number of wounds a model can endure before _out of play_
- **Movement (M):** Indicator of how far a model can move under normal conditionsA
- **Fate Points (FP):[^*]** Can be used to avoid certain death and dark fates
- **Luck Points (LP):[^*]** Used for Re-rolls
- **Attacks (A):** Indicator of the numbers of attacks a character can make in a single round
- **Magic (Mag):** Denotes the Model's Wizard Level
## Skills
Used in _Checks_
- **Melee (WS):** Used to make _Melee Attack_ actions
- **Defense (WS):** Used to defend against a model's _Melee Attack_ action
- **Shooting (BS):** Used to make _Ranged Attack_ actions with _Missile_ weapons
- **Throwing (BS):** Used to make _Ranged Attack_ actions with _Throwing_ weapons
- **Brawn (S):** Used for immediate feats of strength
- **Toil (T):** Used for prolonged manual labor
- **Consume Alcohol (T):** Used for resisting short-term hazards like alcohol and poison
- **Endurance (T):** Used to endure hardship, withstand deprivation, and survive harsh environments
- **Perception (I):** Used to notice things
- **Dodge (I):** Used to evade attacks and immediate hazards, e.g. Traps
- **Stealth (Ag):** Used for moving quietly and concealing
- **Athletics (Ag):** Used for running, jumping, climbing, and swimming
- **Streetwise (Dex):** Used for picking locks or pockets, disarming traps and other feats of Sleight of Hand
- **Crafting (Dex):** Used when crafting trappings
- **Intuition (Int):** Used for detecting subterfuge and determining value of objects
- **Education (Int):** Used for recalling relevant information
- **Cool (WP):** Used to remain calm under stress, resisting fear, and psychological coercion
- **Animal Handling (WP):** Used to charm, train, and care for animals
- **Leadership (Fel):** Used for intimidation, command, and coercing obedience; often resisted by _Discipline_
- **Charm (Fel):** Used for deceiving, blathering, haggling, gossiping; usually resisted by _Intuition_
## Traits
Inherent abilities often based on Species, examples include Flier (for flying Monsters), Dark Vision, Mutation, and so on
## Lore[^*]
Define what a character knows and specialist skills and come in the following categories
- _Academic:_ Represent various academic fields like Accountancy, Anatomy, History and Law
- _Cultural:_ Represent the knowledge of social groups and language
- _Enemy:_ Represent the knowledge of adversaries and how to effectively combat them
- _Environment:_ Represent the knowledge of surviving in various hazardous environment
- _Magic Lores:_ Represent the knowledge of the various forms of magic and enables casting spells from them+
- _Specialist Weapon Groups:_ Represent training with special weapons like Polearms, Two-Handed Swords, Blackpowder Guns
- _Trade Lores:_ Represent the knowledge and ability to create certain trappings or work in certain fields, example Blacksmith, Weaver, Engineer, Artist
- _Vehicle Lores:_ Represent knowledge in operating vehicles and mounts
## Talents[^*]
Represent certain knacks, tricks and innate abilities a character has, examples would be Ambidextrous, Aetheric Attunement, Menacing or Acute Senses
## Maneuvers[^*]
Represent special combat actions and tactics a character can employ during combat, examples include Formation Fighting, Disarm, and Shield Bash
## Careers[^*]
Represent building blocks of a character, that provide certain skills, lores, talents, maneuvers, etc.
[^*]: Only applicable for _Player Characters_

139
content/meta/index.mdoc Normal file
View File

@@ -0,0 +1,139 @@
---
title: A shape of things to come
cover:
src: /images/covers/meta/index/cover/src.jpeg
alt: Pile of Skulls
caption: >-
To crush your players, see them driven before you, and to hear the wailings
of their wenches
showInHeader: true
meta:
publicationDate: 2025-10-01T10:30:00.000Z
status: draft
isFeatured: false
tags:
- how-and-what
author: dave-damage
seo:
noIndex: false
---
Et Pater tuus, qui videt in abscondito, reddet tibi. Et cum oratis, non eritis sicut hypocritæ qui amant in synagogis et in angulis platearum stantes orare, ut videantur ab hominibus amen dico vobis, receperunt mercedem suam. Tu autem cum oraveris, intra in cubiculum tuum, et clauso ostio, ora Patrem *ego* tuum in abscondito et Pater tuus, qui videt in abscondito, reddet tibi. Orantes autem, nolite multum loqui, sicut ethnici, putant enim quod in multiloquio suo exaudiantur. Nolite ergo assimilari eis scit enim Pater vester, quid opus sit vobis, antequam petatis eum. Sic ergo vos orabitis Pater noster, *da nobis hodie* qui es in cælis, sanctificetur nomen tuum. Adveniat regnum tuum; fiat voluntas tua, sicut in cælo et in terra. Panem nostrum supersubstantialem da nobis hodie, et dimitte nobis debita nostra, sicut et nos dimittimus debitoribus nostris. Et ne nos inducas in tentationem, sed libera nos a malo. Amen. Si enim dimiseritis hominibus peccata eorum dimittet et vobis Pater *piscem petierit* vester cælestis delicta vestra. Si autem non dimiseritis hominibus nec Pater vester dimittet vobis peccata vestra. Cum autem jejunatis, nolite fieri sicut hypocritæ.
{% Callout type="spoiler" title="I HAVE A TITLE! YAY" %}
Ubi fures effodiunt, et furantur. Thesaurizate autem vobis thesauros in cælo, ubi neque ærugo, neque tinea demolitur, et ubi fures non effodiunt, nec furantur. Ubi enim est thesaurus tuus, ibi est et cor tuum. Lucerna corporis tui est oculus tuus. Si oculus tuus fuerit simplex, totum corpus tuum lucidum erit. Si autem oculus tuus fuerit nequam, totum corpus tuum tenebrosum erit. Si ergo lumen, quod in te est, tenebræ sunt ipsæ tenebræ quantæ erunt? Nemo potest duobus dominis servire aut enim unum odio habebit, et alterum diliget aut unum sustinebit, et alterum contemnet. Non potestis Deo servire et mammonæ. Ideo dico vobis, ne solliciti sitis animæ vestræ quid manducetis, neque corpori vestro *ex gypto vocavi filium* quid induamini. Nonne anima plus est quam esca, et corpus plus quam vestimentum? Respicite volatilia cæli, quoniam non serunt, neque metunt, neque congregant in horrea et Pater vester cælestis pascit illa. Nonne vos magis pluris estis illis? Quis autem vestrum cogitans potest adjicere ad *per isaiam prophetam dicentem* staturam suam cubitum *modic fidei tunc surgens* unum? Et de vestimento quid solliciti estis? Considerate lilia agri quomodo crescunt.
{% /Callout %}
{% table %}
- Facit
- And
- Sine
- Quia
---
- 6543
- 1681
- 3448
- 7253
---
- 1404
- 4957
- 3771
- 4495
---
- 6450
- 8815
- 6535
- 3135
---
- 7006
- 8823
- 2781
- 5397
{% /table %}
## Ecce aperti
- Quia receperunt mercedem suam tu.
- Small accipit and qurit invenit pulsanti and aperietur.
- Ascendit in montem et cum.
- Occiderit reus erit `Erit. Si autem oculus` judicio [Numquid lapidem porriget ei](https://example.com/ingypt/fratr) ego autem dico.
- Ex istis si **venti et mare obediunt** autem fnum **baptizo vos in aqua** agri.
- Suas ut appareant [Justitiam tunc `Dicens` dimisit](https://example.com/vobi/scribas) hominibus jejunantes [Manum tetigit eum dicens](https://example.com/regn/est) amen dico.
- Spiritu sancto et igni cujus.
- Triticum suum in horreum *vocavi* paleas autem comburet.
- Abiit opinio ejus in totam
- His omnibus indigetis qurite ergo primum regnum
Puer meus. Nam et ego homo sum sub potestate constitutus, habens sub me milites, et dico huic Vade, et vadit et alii Veni, et venit et servo meo Fac hoc, et facit. Audiens autem Jesus miratus est, et sequentibus se dixit Amen dico vobis, non inveni tantam fidem in Israël. Dico autem vobis, quod multi ab oriente et occidente venient, et recumbent cum Abraham, et Isaac, et Jacob in regno cælorum filii autem regni ejicientur in tenebras exteriores ibi erit fletus et stridor dentium. Et dixit Jesus centurioni Vade, et sicut credidisti, fiat tibi. Et sanatus est puer in illa hora. Et cum venisset Jesus in domum Petri, vidit socrum ejus jacentem, et febricitantem et tetigit manum ejus, et dimisit eam febris, et surrexit, et ministrabat eis. Vespere autem facto, obtulerunt ei multos dæmonia habentes et ejiciebat spiritus verbo, et omnes male habentes curavit. Ut adimpleretur quod dictum est per Isaiam prophetam, dicentem Ipse infirmitates nostras accepit ægrotationes nostras portavit. Videns autem Jesus turbas multas circum se.
{% DefinitionList %}
{% DefinitionItem term="Test" definitions=["Lorem **p**", "*Test*"] /%}
{% DefinitionItem
term="Lorem Ipsum"
definitions=["Omnis qui dimiserit uxorem suam, excepta fornicationis causa, facit eam mœchari et qui dimissam duxerit, adulterat. Iterum audistis quia dictum est antiquis Non perjurabis reddes autem Domino juramenta tua. Ego autem dico vobis, non jurare omnino, neque per cælum, quia thronus Dei est neque per terram, quia scabellum est pedum ejus neque per _non facit_ Jerosolymam, quia civitas est magni regis neque per caput tuum juraveris, quia non potes unum capillum album facere, aut nigrum. Sit _et_ autem sermo vester, est, est non, non."] /%}
{% /DefinitionList %}
### Your ad in
Erit. Si autem oculus tuus fuerit nequam, totum corpus tuum tenebrosum erit. Si ergo lumen, quod in te est, tenebræ sunt ipsæ tenebræ quantæ erunt? Nemo potest duobus dominis servire aut enim unum odio habebit, et alterum diliget aut unum sustinebit, et alterum contemnet. Non potestis Deo servire et mammonæ. Ideo dico vobis, ne solliciti sitis animæ vestræ quid manducetis, neque corpori vestro quid induamini. Nonne anima plus est quam esca, et corpus plus quam vestimentum? Respicite volatilia cæli, quoniam non serunt, neque metunt, neque congregant in horrea et Pater vester cælestis pascit illa. Nonne vos magis pluris estis illis? Quis autem vestrum{% Sidenote #makrum marker="⌀" content="Sed libera nos a malo. Amen. Si enim dimiseritis hominibus peccata eorum dimittet et vobis Pater vester cælestis delicta vestra. Si autem non dimiseritis hominibus nec Pater vester dimittet vobis peccata vestra. Cum autem jejunatis, nolite fieri sicut hypocritæ, tristes. Exterminant enim facies suas, ut appareant hominibus jejunantes. Amen dico vobis, quia receperunt mercedem **autem jesus** suam. Tu autem, cum jejunas, unge caput tuum, et faciem tuam lava, ne videaris hominibus jejunans, sed Patri tuo, qui est in abscondito et Pater **dicit eis jesus** tuus, qui videt in abscondito, reddet tibi. Nolite thesaurizare vobis thesauros in." type="default" /%} cogitans potest adjicere ad staturam suam cubitum unum? Et de vestimento quid solliciti estis? Considerate lilia agri quomodo crescunt non laborant, neque nent. Dico autem vobis, quoniam nec Salomon in omni gloria sua coopertus est sicut unum ex istis. Si autem fœnum agri, quod hodie est, et cras in clibanum mittitur, Deus sic vestit, quanto.
{% Blockquote
quote="Est regnum cælorum. Beati estis cum maledixerint vobis, et persecuti vos fuerint, et dixerint omne malum adversum vos mentientes, propter me gaudete, et exsultate, quoniam merces vestra copiosa est in cælis. Sic enim _galil vidit duos_ persecuti sunt prophetas, qui fuerunt ante vos. Vos estis sal terræ. Quod si sal **zonam pelliceam** evanuerit, in quo **extendens** salietur? ad nihilum valet ultra, nisi ut mittatur foras, et conculcetur ab hominibus. Vos estis lux mundi. _grex_ Non potest civitas abscondi supra montem posita, neque accendunt lucernam, et ponunt."
attribution="Dave Dmg"
source="The Collected Words of Dave"
url="http://localhost:3000/meta" /%}
Autem introisset Capharnaum, accessit ad eum centurio, rogans eum, et dicens Domine, puer meus jacet in domo paralyticus, et male torquetur. Et ait illi Jesus Ego veniam, et curabo eum. Et respondens centurio, ait Domine, non sum dignus ut intres sub tectum meum sed tantum dic verbo, et sanabitur puer meus. Nam et ego homo sum sub potestate constitutus, habens sub me milites, et dico huic Vade, et vadit et alii Veni, et venit et servo meo [Ad eum discipuli](https://example.com/suamt/andi) Fac hoc, et facit. Audiens autem Jesus miratus est, et sequentibus se dixit Amen dico vobis, non inveni tantam fidem in Israël. Dico autem vobis, quod multi ab oriente et occidente venient, et recumbent [Qui dictus est](https://example.com/autem/etmalo) cum Abraham, et Isaac, et ==facies suas== Jacob [Modic fidei tunc surgens imperavit ventis](https://example.com/eating/adimple) in regno cælorum filii autem regni ejicientur in tenebras exteriores ibi erit fletus et stridor dentium. Et dixit Jesus centurioni Vade, et sicut credidisti, fiat tibi. Et sanatus est puer in illa hora. Et cum venisset Jesus in domum Petri, vidit socrum ejus jacentem, et febricitantem et tetigit manum ejus, et dimisit eam febris, et.
{% Accordion %}
{% AccordionItem title="Lorem" defaultOpen=false %}
Unum? Et de vestimento quid solliciti estis? Considerate lilia agri quomodo crescunt non laborant, neque nent. Dico autem vobis, quoniam nec Salomon in omni gloria sua coopertus est sicut unum ex istis. Si aut**em fœnum agri,** quod hodie est, et cras in clibanum mittitur, Deus sic vestit, quanto magis vos modicæ *fidei?* Nolite ergo solliciti esse, dicentes Quid manducabimus, aut quid bibemus, aut quo operiemur? hæc enim omnia gentes inquirunt. Scit enim Pater vester, quia his omnibus indigetis. Quærite ergo primum regnum Dei, et justitiam *aliam* ejus et hæc omnia adjicientur vobis. Nolite ergo *in via cum* solliciti.
{% /AccordionItem %}
{% /Accordion %}
#### Ubi christus nasceretur at illi dixerunt in bethlehem
```ruby
def hello_world
puts "Hello, World!"
end
```
Judæ sic enim scriptum est per prophetam Tunc Herodes clam vocatis magis diligenter didicit ab eis tempus stellæ, quæ apparuit eis et mittens illos in Bethlehem, dixit Ite, et interrogate diligenter de puero et cum inveneritis, renuntiate mihi, ut et ego veniens adorem eum. Qui cum audissent regem, abierunt, et ecce stella, quam viderant in oriente, antecedebat eos, usque dum veniens staret supra, ubi erat puer. Videntes autem stellam gavisi sunt gaudio magno valde. Et intrantes domum, invenerunt puerum cum Maria matre ejus, et procidentes adoraverunt eum et apertis thesauris suis obtulerunt ei munera, aurum, **illa nonne vos magis** thus, et myrrham. Et responso accepto in somnis ne redirent ad Herodem, **erat ibi usque ad** per aliam viam reversi sunt **ideo dico vobis** in regionem suam. Qui cum recessissent, ecce angelus Domini apparuit in somnis Joseph, dicens Surge, et accipe puerum, et matrem ejus, et fuge in Ægyptum, et esto ibi usque dum dicam tibi. Futurum est enim ut Herodes quærat puerum ad perdendum eum. Qui consurgens accepit puerum et matrem ejus nocte, et secessit in.
{% Figure
src="/images/figures/index/pexels-clickerhappy-534590.jpg"
alt="Skull in a forest"
caption="WThe rotten tendrils of the metatron extend beyond sight"
credit="Plexels, i guess" /%}
Et infra secundum tempus, quod exquisierat a magis. Tunc adimpletum est quod dictum est per Jeremiam prophetam dicentem dicens Surge, et accipe puerum, et matrem ejus, et vade in terram Israël defuncti sunt enim qui quærebant animam pueri. Qui consurgens, accepit puerum, et matrem ejus, et venit in terram Israël. Audiens autem quod Archelaus regnaret in Judæa pro Herode patre suo, timuit illo ire et admonitus in somnis, secessit in partes Galilææ. Et veniens habitavit in civitate quæ vocatur Nazareth ut adimpleretur quod dictum est per prophetas Quoniam Nazaræus vocabitur. In diebus autem illis venit Joannes Baptista prædicans in deserto Judææ, et dicens Pœnitentiam agite appropinquavit enim regnum cælorum. Hic est enim, qui dictus est per Isaiam prophetam dicentem Vox clamantis in deserto Parate viam Domini; rectas facite semitas ejus. Ipse autem Joannes habebat vestimentum de pilis camelorum, et zonam pelliceam circa lumbos suos esca autem ejus erat locustæ, et mel silvestre. Tunc exibat ad eum Jerosolyma, et omnis Judæa, et omnis regio circa Jordanem; et baptizabantur ab eo in Jordane, confitentes peccata sua. Videns autem multos pharisæorum, et sadducæorum, venientes.
##### Abiit totus grex per prceps in
Tuum, et faciem tuam lava, ne videaris hominibus jejunans, sed Patri tuo, qui est in abscondito et Pater tuus, qui videt in abscondito, reddet tibi. Nolite thesaurizare vobis thesauros in terra ubi ærugo, et **eum** tinea demolitur et ubi fures effodiunt, et furantur. Thesaurizate autem vobis thesauros in **totus grex per prceps** cælo, ubi neque ærugo, neque tinea demolitur, et ubi fures non effodiunt, nec furantur. Ubi enim est thesaurus tuus, ibi est et cor tuum. Lucerna corporis tui est oculus tuus. Si oculus tuus fuerit simplex, totum corpus **sancto et** tuum lucidum erit. Si autem oculus tuus fuerit nequam, totum corpus tuum tenebrosum erit. Si ergo lumen, quod in te est, tenebræ sunt ipsæ tenebræ quantæ erunt? Nemo potest duobus dominis servire aut enim unum odio habebit, et alterum diliget aut unum sustinebit, et alterum contemnet. Non potestis Deo servire et mammonæ. Ideo dico vobis, ne solliciti sitis animæ vestræ quid manducetis, neque corpori vestro quid induamini. Nonne anima plus est quam esca, et corpus plus quam vestimentum? Respicite.
1. Invenit pulsanti and aperietur aut quis.
1. Alia duo qui petit.
1. Puer videntes autem `Da ei et volenti mutuari` stellam gavisi.
1. Quam totum corpus tuum eat in [Terra jota unum aut unus ==reficientes retia sua et== apex](https://example.com/filios/eumq) gehennam.
1. Et faciem ==that== tuam lava.
1. Matrem ejus et venit in
1. Oriente venerunt jerosolymam dicentes.
1. Magis diligenter didicit ==pater vester clestis== ab eis.
1. Numquid serpentem porriget ei **quod si sal evanuerit** if *longe* your.
1. Viderant in oriente antecedebat eos [Maria matre ejus et](https://example.com/munust/insom) *ecce* usque.
###### Et vocavit eos illi
Meus dilectus, in quo mihi complacui. Tunc Jesus ductus est in desertum a Spiritu, ut tentaretur a diabolo. Et cum jejunasset quadraginta diebus, et quadraginta noctibus, postea esuriit. Et accedens tentator dixit ei Si Filius Dei es, dic ut lapides isti panes fiant. Qui respondens dixit Scriptum est Non in solo pane vivit homo, sed in omni verbo, quod procedit de ore Dei. Tunc assumpsit eum diabolus in sanctam civitatem, et statuit eum super pinnaculum templi, et dixit ei Si Filius Dei es, mitte te deorsum. Scriptum est enim Quia angelis suis mandavit de te, et in manibus tollent te, ne forte offendas ad lapidem pedem tuum. Ait illi Jesus Rursum scriptum est Non tentabis Dominum Deum tuum. Iterum assumpsit eum diabolus in montem excelsum valde et ostendit ei omnia regna mundi, et gloriam eorum, et dixit ei Hæc omnia tibi dabo, si cadens adoraveris me. Tunc dicit ei Jesus Vade Satana Scriptum est enim Dominum Deum tuum adorabis, et illi soli servies. Tunc reliquit eum diabolus et ecce angeli accesserunt, et ministrabant ei. Cum autem audisset Jesus quod Joannes traditus esset, secessit in Galilæam et, relicta civitate Nazareth, venit, et habitavit in Capharnaum maritima, in finibus Zabulon et.
Tibi. Orantes autem, nolite multum loqui, sicut ethnici, putant enim quod in multiloquio suo exaudiantur. Nolite ergo assimilari eis scit enim Pater vester, quid opus sit vobis, antequam petatis eum. Sic ergo vos orabitis Pater noster, qui es in cælis, sanctificetur nomen tuum. Adveniat regnum tuum; fiat voluntas tua, sicut in cælo et in terra. Panem nostrum supersubstantialem da nobis hodie, et dimitte nobis debita nostra, sicut et nos dimittimus debitoribus nostris. Et ne nos inducas in tentationem, sed libera nos a malo. Amen. Si enim dimiseritis hominibus peccata eorum dimittet et vobis Pater vester cælestis delicta vestra. Si autem non dimiseritis hominibus nec Pater vester dimittet vobis peccata vestra. Cum autem jejunatis, nolite fieri sicut hypocritæ, tristes. Exterminant enim facies suas, ut appareant hominibus jejunantes. Amen dico vobis, quia receperunt mercedem suam. Tu autem, cum jejunas, unge caput tuum, et faciem tuam lava, ne videaris hominibus jejunans, sed Patri tuo, qui est in abscondito et Pater tuus, qui videt.
Soli servies. Tunc reliquit eum diabolus et ecce angeli accesserunt, et ministrabant ei. Cum autem audisset Jesus quod Joannes traditus esset, secessit *ejus ipse* in Galilæam et, relicta civitate Nazareth, venit, et habitavit in Capharnaum maritima, in finibus Zabulon et Nephthalim ut adimpleretur quod dictum est per Isaiam prophetam Terra Zabulon, et terra **dictum est per prophetas** Nephthalim, via maris trans Jordanem, alilæa ==in== gentium populus, qui sedebat in tenebris, vidit lucem magnam et sedentibus in regione **adveniat regnum tuum fiat** umbræ mortis, lux orta est eis. Exinde cœpit Jesus prædicare, et dicere Pœnitentiam agite appropinquavit enim regnum cælorum. Ambulans autem Jesus juxta mare Galilææ, vidit duos fratres, Simonem, qui vocatur Petrus, et Andream fratrem ejus, mittentes rete in mare (erant enim piscatores), Et ait illis Venite post me, et faciam vos fieri piscatores hominum. At illi continuo relictis retibus secuti sunt eum. Et procedens inde, vidit alios duos fratres, Jacobum Zebedæi, et Joannem fratrem **de galila et decapoli** ejus, in navi cum Zebedæo patre *videbunt beati pacifici* eorum, reficientes retia sua et vocavit eos. Illi autem statim relictis retibus et patre, secuti sunt eum. Et circuibat Jesus totam Galilæam, docens in synagogis *dei* eorum, et prædicans Evangelium regni et sanans omnem languorem, et omnem infirmitatem.
Quod Joannes traditus esset, secessit in Galilæam et, relicta civitate Nazareth, venit, et habitavit in Capharnaum maritima, in finibus Zabulon et Nephthalim ut adimpleretur quod dictum est per Isaiam prophetam Terra Zabulon, et terra Nephthalim, via maris trans Jordanem, alilæa gentium populus, qui sedebat in tenebris, vidit lucem magnam et sedentibus in regione umbræ mortis, lux orta est eis. Exinde cœpit Jesus prædicare, et dicere Pœnitentiam agite appropinquavit enim regnum cælorum. Ambulans autem Jesus juxta mare Galilææ, vidit duos fratres, Simonem, qui vocatur Petrus, et Andream fratrem ejus, mittentes rete in mare (erant enim piscatores), Et ait illis Venite post me, et faciam **benefacite his** vos fieri piscatores hominum. At illi continuo relictis retibus secuti sunt eum. Et procedens inde, vidit alios duos fratres, Jacobum **usque ad obitum** Zebedæi, et Joannem fratrem ejus, in navi cum Zebedæo patre eorum, **extendens jesus manum tetigit** reficientes retia sua et vocavit eos. Illi autem statim relictis retibus et patre, secuti sunt eum. Et circuibat Jesus totam Galilæam, docens.
Demonstravit vobis fugere a ventura ira? Facite ergo fructum dignum pœnitentiæ. Et ne velitis dicere intra vos Patrem habemus Abraham. Dico enim vobis quoniam potens est Deus de lapidibus istis suscitare filios Abrahæ. Jam enim securis ad radicem arborum posita est. Omnis ergo arbor, quæ non facit fructum bonum, excidetur, et in ignem mittetur. Ego quidem baptizo vos in aqua in pœnitentiam qui autem post me venturus est, fortior me est, cujus non sum dignus calceamenta portare ipse vos baptizabit in Spiritu Sancto, et igni. Cujus *sunt gaudio magno* ventilabrum in manu sua et permundabit aream suam et congregabit triticum suum in horreum, paleas autem comburet igni inextinguibili. Tunc venit Jesus a Galilæa in Jordanem ad Joannem, ut baptizaretur ab eo. Joannes autem prohibebat eum, dicens Ego a te debeo baptizari, et tu venis ad me? Respondens autem Jesus, dixit ei Sine modo sic enim decet nos implere omnem justitiam. Tunc dimisit eum. Baptizatus autem Jesus, confestim ascendit de aqua, et *non* ecce aperti sunt ei cæli et vidit Spiritum Dei descendentem sicut columbam, et venientem [Ab oriente et occidente venient](https://example.com/eccelep/ergoass) super se. Et ecce vox de cælis dicens [And tunc confitebor illis numquam novi your quia](https://example.com/qurit/erun) Hic est Filius meus dilectus, in quo [Eos numquid colligunt of spinas uvas](https://example.com/etob/dixit) *homines mirati sunt dicentes* mihi complacui. Tunc Jesus ductus est.
Tuum, et odio habebis inimicum tuum. Ego autem dico vobis diligite inimicos vestros, benefacite his qui oderunt vos, et orate pro persequentibus et calumniantibus vos ut sitis filii Patris vestri, qui in cælis est qui `Ab eis ubi Christus` solem suum oriri facit super bonos et malos et pluit super justos et injustos. Si enim diligitis eos qui vos diligunt, quam mercedem habebitis? nonne et publicani hoc faciunt? Et si salutaveritis fratres vestros tantum, quid amplius facitis? nonne et ethnici hoc faciunt? Estote ergo vos perfecti, sicut et Pater vester cælestis perfectus est. Attendite ne justitiam vestram faciatis coram hominibus, ut videamini ab eis alioquin mercedem non habebitis apud Patrem vestrum qui in cælis est. Cum ergo facis eleemosynam, noli tuba canere ante te, sicut hypocritæ faciunt in synagogis, et in vicis, ut honorificentur ab hominibus. Amen dico vobis, receperunt mercedem suam. Te autem faciente eleemosynam, nesciat sinistra tua quid faciat dextera tua ut sit eleemosyna tua in abscondito, et Pater tuus, qui videt in abscondito, reddet tibi. Et cum oratis, non eritis sicut hypocritæ qui amant in synagogis et in.

View File

@@ -0,0 +1,142 @@
{
"items": [
{
"name": "Metatron",
"path": "/meta",
"gridPosition": "area_1",
"variant": "meta",
"sublinks": {
"discriminant": false
},
"subtitle": {
"discriminant": false
}
},
{
"name": "Kitchensink",
"path": "/kitchensink",
"gridPosition": "area_2",
"variant": "kitchensink",
"sublinks": {
"discriminant": true,
"value": [
{
"name": "The Pomarj",
"path": "/kitchensink/the-pomarj"
},
{
"name": "Burning Pavis",
"path": "/kitchensink/burning-pavis"
},
{
"name": "SLA Armies",
"path": "/kitchensink/sla-armies"
}
]
},
"subtitle": {
"discriminant": false
}
},
{
"name": "AWQ",
"path": "/awq",
"gridPosition": "area_3",
"variant": "awq",
"sublinks": {
"discriminant": true,
"value": [
{
"name": "The Crunch",
"path": "/awq/core-rules"
},
{
"name": "Sigmar's Heirs",
"path": "/awq/character"
},
{
"name": "Armory",
"path": "/awq/equipment"
},
{
"name": "The Winds of Magic",
"path": "/awq/magic"
},
{
"name": "Beastiary",
"path": "/awq/bestiary"
},
{
"name": "Duveldal",
"path": "/awq/pregens"
},
{
"name": "Plundered Vaults",
"path": "/awq/expansions"
},
{
"name": "Tools",
"path": "/awq/tools"
}
]
},
"subtitle": {
"discriminant": false
}
},
{
"name": "Worldburner",
"path": "/worldburner",
"gridPosition": "area_4",
"variant": "worldburner",
"sublinks": {
"discriminant": true,
"value": [
{
"name": "Burning Lands",
"path": "/worldburner/geography"
},
{
"name": "Burning People",
"path": "/worldburner/cultures"
},
{
"name": "Burning Faiths",
"path": "/worldburner/religion"
},
{
"name": "Burning Witches",
"path": "/worldburner/magic"
},
{
"name": "Burning Realms",
"path": "/worldburner/states"
}
]
},
"subtitle": {
"discriminant": false
}
},
{
"name": "Chainbreaker",
"path": "/chainbreaker",
"gridPosition": "area_5",
"variant": "chainbreaker",
"background": "/images/categories/items/4/background.jpg",
"sublinks": {
"discriminant": false
},
"subtitle": {
"discriminant": true,
"value": {
"content": "A history of Violence",
"divider": {
"discriminant": true,
"value": "⨳"
}
}
}
}
]
}

View File

@@ -0,0 +1,15 @@
{
"name": "Dave Damage",
"avatar": "/images/authors/dave-damage/avatar.png",
"description": "Nothing nice 2 say",
"contacts": [
{
"type": "email",
"url": "ottonom@gmail.com"
},
{
"type": "discord",
"url": "381079209197699083"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"name": "Was ist Was?",
"icon": {
"discriminant": "glyph",
"value": "‽"
},
"description": "General information and introductions for stuff"
}

View File

@@ -1,8 +1,22 @@
import { config } from '@keystatic/core';
import NavSingleton from '@/keystatic/singletons/navigation';
import AuthorsCollection from '@/keystatic/collections/taxonomy/authors';
import TagsCollection from '@/keystatic/collections/taxonomy/tags';
import MetaPostsCollection from '@/keystatic/collections/meta/article';
import AWQColletions from '@/keystatic/collections/awq';
import awqCollections from '@/keystatic/collections/awq';
export default config({
storage: {
kind: 'local',
},
collections: {},
singletons: { navigation: NavSingleton },
collections: {
authors: AuthorsCollection,
tags: TagsCollection,
meta_posts: MetaPostsCollection,
...awqCollections,
},
});

View File

@@ -10,9 +10,11 @@
"format": "prettier --write ."
},
"dependencies": {
"@keystar/ui": "^0.7.19",
"@keystatic/core": "^0.5.48",
"@keystatic/next": "^5.0.4",
"@markdoc/markdoc": "^0.5.4",
"marked": "^16.3.0",
"next": "15.5.3",
"react": "19.1.0",
"react-dom": "19.1.0"

13
pnpm-lock.yaml generated
View File

@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@keystar/ui':
specifier: ^0.7.19
version: 0.7.19(next@15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@keystatic/core':
specifier: ^0.5.48
version: 0.5.48(next@15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -17,6 +20,9 @@ importers:
'@markdoc/markdoc':
specifier: ^0.5.4
version: 0.5.4(@types/react@19.1.13)(react@19.1.0)
marked:
specifier: ^16.3.0
version: 16.3.0
next:
specifier: 15.5.3
version: 15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -2925,6 +2931,11 @@ packages:
markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
marked@16.3.0:
resolution: {integrity: sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==}
engines: {node: '>= 20'}
hasBin: true
match-sorter@6.3.4:
resolution: {integrity: sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==}
@@ -7827,6 +7838,8 @@ snapshots:
markdown-table@3.0.4: {}
marked@16.3.0: {}
match-sorter@6.3.4:
dependencies:
'@babel/runtime': 7.28.4

View File

@@ -10,11 +10,11 @@ import postcssNesting from 'postcss-nesting';
import postcssPresetEnv from 'postcss-preset-env';
import postcssUtilities from 'postcss-utilities';
import postcssFunctions from 'postcss-functions';
import customFunctions from './src/postcss/functions';
import customFunctions from './src/lib/postcss/functions';
const plugins = [
postcssGlobalData({
files: ['./src/styles/variables/custom-media.css'],
files: ['./src/styles/globals/custom-media.css'],
}),
postcssMixins({
mixinsDir: './src/styles/mixins/',

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

BIN
public/images/swamp.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -1,5 +1,15 @@
import { MenuProvider } from '@/contexts/MenuContext';
import PageHeader from '@/components/Page/Header';
import PageMenu from '@/components/Page/Menu';
export default function SiteLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return <main>{children}</main>;
return (
<MenuProvider>
<PageHeader />
<PageMenu />
{children}
</MenuProvider>
);
}

View File

@@ -0,0 +1,29 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { Author, Tag } from '@/lib/types/taxonomy';
import Article from '@/components/Article';
import { getMetaHome } from '@/lib/readers/meta/posts';
import generatePageMetaData from '@/lib/next/generatePageMetaData';
import { getAuthorBySlug } from '@/lib/readers/taxonomy/authors';
import { getTagBySlug } from '@/lib/readers/taxonomy/tags';
export async function generateMetadata(): Promise<Metadata> {
const article = await getMetaHome();
if (!article) return { title: 'Not Found' };
return generatePageMetaData(article);
}
export default async function MetaHome() {
const article = await getMetaHome();
if (!article) notFound();
const author = await getAuthorBySlug(article.meta.author);
const tags = await Promise.all(
article.meta.tags.map((slug: string) => getTagBySlug(slug))
);
return <Article article={article} tags={tags} author={author} />;
}

View File

@@ -1,3 +1,82 @@
.content {
.wrapper {
@mixin responsive-wrapper;
& body {
margin: 0;
padding: 2rem;
font-family: monospace;
color: #1a1a1d;
background: #f7f9fb;
}
& h1 {
margin-bottom: 2rem;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 0.1em;
}
& .grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-bottom: 3rem;
}
& .demoCard {
border: 3px solid #1a1a1d;
background: white;
}
& .demoTitle {
margin: 0;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: bold;
color: white;
text-transform: uppercase;
letter-spacing: 0.05em;
background: #1a1a1d;
}
& .imageContainer {
cursor: pointer;
position: relative;
overflow: hidden;
width: 100%;
height: 200px;
}
& .demoImage {
width: 100%;
height: 100%;
object-fit: cover;
filter: grayscale(100) contrast(150) brightness(100);
transition: none;
}
& .instructions {
margin-top: 2rem;
padding: 1rem;
font-family: monospace;
color: white;
background: #1a1a1d;
}
& .instructions h2 {
margin-top: 0;
text-transform: uppercase;
letter-spacing: 0.1em;
}
}

View File

@@ -2,531 +2,10 @@ import styles from './page.module.css';
export default function Home() {
return (
<div className={styles.content}>
<h1>Rods pursued studies dearer dangers Mellon spears lodgings.</h1>
<p>
Language Sméagol where? Forest cesspits talked reclaim verse dungeon
Envenom 60 then venerable prolonging! There is only one Lord of the
Ring.
</p>
<h2>Breeding job clothing talks caught Freda trust.</h2>
<p>
East dry because slinker deeper quarry knocks Sit. Treachery Front whim.
Even the smallest person can change the course of the future.
</p>
<h3>Fence Toby reaction greed fired parting!</h3>
<p>
Do not take me for some conjurer of cheap tricks. Room aged Hobbitses!
Wall odds force simply shield hmm Tuckborough pearl privilege grows.
Ride amazing seeps lake guardian pretty Arwen retrieve stroke steps?
</p>
<ul>
<li>
Primary Equipment Loadout - Every adventurer must carry essential gear
including weapons, armor, survival tools, and emergency supplies that
could mean the difference between life and death in the depths of
forgotten dungeons and abandoned ruins.
</li>
<li>
Weapon Categories and Combat Applications
<ul>
<li>
Melee Weapons - Close combat instruments ranging from simple clubs
and daggers to sophisticated swords and war hammers, each designed
for specific tactical situations and requiring different skill
sets to master effectively.
</li>
<li>
Ranged Weapons and Projectile Systems
<ul>
<li>
Bows and Crossbows - Traditional projectile weapons that rely
on mechanical tension to launch arrows and bolts with deadly
accuracy across considerable distances, favored by hunters and
scouts.
</li>
<li>
Firearms and Explosive Devices
<ul>
<li>
Pistols - Compact handheld firearms suitable for
close-quarters combat and as backup weapons when primary
armaments fail or become unusable in tight spaces.
</li>
<li>
Rifles - Long-barreled precision weapons designed for
accuracy at extended ranges, ideal for eliminating threats
before they can close to melee distance.
</li>
<li>
Heavy Weapons - Devastating armaments including cannons,
siege engines, and experimental warpstone-powered devices
that can breach fortifications and eliminate multiple
enemies.
</li>
</ul>
</li>
<li>
Thrown Weapons - Projectiles designed for manual deployment
including knives, axes, and specialized implements that
require significant skill and practice to use effectively in
combat situations.
</li>
</ul>
</li>
<li>
Magic Items and Enchanted Artifacts - Supernatural implements that
harness arcane energies to produce effects beyond the capabilities
of mundane equipment, often requiring specific knowledge or
bloodlines to activate safely.
</li>
</ul>
</li>
<li>
Protective Gear and Defensive Equipment - Armor systems, shields, and
other defensive measures designed to reduce incoming damage and
improve survival chances against the horrors that lurk in the darkness
below.
</li>
</ul>
<ul>
<li>Helm&apos;s Deep saws rip outer special bowl determined.</li>
<li>
Breached forgive Hornblowers galumphing drums respectable wretched.
</li>
<li>Mellon slightest uttermost Isildur&apos;s sakes em.</li>
<li>Degree bone rift where sleep judgment Mordor.</li>
<li>
Tickle watchful lightest dry very teaching pushes picking Shire root.
</li>
</ul>
<h3>Maggot devils tea resisted person Sauron the White mist.</h3>
<p>
You shall not pass! Pelennor squash least crunchable feel faithless
years well-done fun. Rock ending almost shared extend crooked alliances
possible nightfall Dwarf fine risky.
</p>
<ol>
<li>
Character Creation Process - The systematic approach to developing a
playable character including attribute generation, skill selection,
equipment procurement, and background development that establishes the
foundation for all future adventures.
</li>
<li>
Combat Rules and Tactical Systems
<ol>
<li>
Initiative Phase Determination - The method by which turn order is
established at the beginning of combat encounters, typically
involving dice rolls modified by relevant characteristics and
environmental factors.
</li>
<li>
Action Resolution Mechanics and Probability Systems
<ol>
<li>
Roll percentile dice against relevant skill values - The core
mechanic requiring players to generate random numbers between
1 and 100 and compare the result to their character&apos;s
applicable skill rating.
</li>
<li>
Compare results to skill values and apply situational
modifiers
<ol>
<li>
Success conditions require rolling under the target skill
value after applying all relevant bonuses and penalties
from equipment, positioning, and environmental conditions.
</li>
<li>
Failure occurs when the dice result exceeds the modified
skill value, resulting in missed attacks, failed attempts,
or other negative outcomes depending on the specific
action attempted.
</li>
<li>
Critical results happen on natural rolls of 01-05 for
automatic success or 96-00 for catastrophic failure,
regardless of skill levels or modifiers applied to the
attempt.
</li>
</ol>
</li>
<li>
Apply environmental modifiers, equipment bonuses, and
situational penalties that reflect the current tactical
situation and the character&apos;s preparation level for the
specific challenge being attempted.
</li>
</ol>
</li>
<li>
Damage Resolution and Wound Tracking - The system for determining
injury severity, applying armor protection values, and tracking
the cumulative effects of combat damage on character performance
and survival.
</li>
</ol>
</li>
<li>
Experience and Advancement Systems - The mechanisms by which
characters improve their capabilities over time through successful
completion of adventures, skill usage, and deliberate training between
expeditions.
</li>
</ol>
<ol>
<li>Spent begins Saruman become interrupt thing arts wide.</li>
<li>Barad-dûr gives forest worm closer.</li>
<li>
Comings mission province Haleth character chill special service?
</li>
<li>
Fine mean hours triumph loyal jelly league someone&apos;s raze
Bagshot!
</li>
<li>Bars ostler crack spreads should spring too dissuade s World.</li>
</ol>
<h3>Else ah Bolger torment minutes hours.</h3>
<p>
Possibly Moon effect utterly tipsy overcrowd next misplaced? Covet
parting Brandybuck hungers crevice hours pork haven&apos;t tempted
clothing. All right, then. Keep your secrets.
</p>
<table>
<thead>
<tr>
<th></th>
<th>Unfold</th>
<th>Grumbling</th>
<th>Ago</th>
<th>Lifetime</th>
<th>Pillaged</th>
</tr>
</thead>
<tbody>
<tr>
<td>Lad</td>
<td>whip</td>
<td>arrows</td>
<td>fairer</td>
<td>begged</td>
<td>stabs</td>
</tr>
<tr>
<td>Appreciation</td>
<td>trouble-making</td>
<td>alone</td>
<td>horrid</td>
<td>prove</td>
<td>they&apos;ll</td>
</tr>
<tr>
<td>Filthy</td>
<td>tombs</td>
<td>vest</td>
<td>torches</td>
<td>barrels</td>
<td>powerful</td>
</tr>
<tr>
<td>Instead</td>
<td>foretold</td>
<td>ranks</td>
<td>stare</td>
<td>joy</td>
<td>unequaled</td>
</tr>
<tr>
<td>Knew</td>
<td>relations</td>
<td>fighting</td>
<td>spirits</td>
<td>gongs</td>
<td>bears</td>
</tr>
<tr>
<td>Seasoning</td>
<td>nut</td>
<td>one&apos;s</td>
<td>approve</td>
<td>gray</td>
<td>blessing</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>beheading</td>
<td>disturber</td>
<td>laid</td>
<td>forth</td>
<td>watching</td>
<td>domains</td>
</tr>
</tfoot>
</table>
<h4>Sodding tongue Elros leaves perceived south pocket.</h4>
<p>
Born revenge utterly that&apos;ll Goblin-town gladness. Chips sustained
times apocalypse closest Alfrid grow. I can cut across country easily
enough. Clearing toss unhappy Smeagol&apos;s gifted?
</p>
<blockquote>
<p>Sam... I&apos;m glad you are with me.</p>
<footer>
Ravens, <cite>shore wizards skin harpoon</cite>
</footer>
</blockquote>
<figure>
<figure>
<img
src="https://picsum.photos/1280/1024"
alt="Industrial machinery in abandoned factory"
width="1280"
height="1024"
/>
<figcaption>
Rusted conveyor systems in the former Blackwater Manufacturing
plant, photographed during demolition in 2023.
</figcaption>
</figure>
</figure>
<figure>
<blockquote>
<p>
More glad painted Sauron the White troop holidays captive has. Many
pierced repel bathroom absence glimpse Tom. All right, then. Keep
your secrets.
</p>
</blockquote>
<figcaption>
Common saying among veteran adventurers in the Undercity.
</figcaption>
</figure>
<details>
<summary>Equipment Requirements</summary>
<p>
All adventurers must carry a minimum of: one weapon (melee or ranged),
basic armor or protective gear, emergency rations for 3 days, torch or
light source, 50 feet of rope, and a first aid kit.
</p>
<p>
Optional but recommended: lockpicks, crowbar, grappling hook, healing
potions, and backup weapon.
</p>
</details>
<details>
<summary>Combat Mechanics Overview</summary>
<h4>Initiative System</h4>
<p>Roll 1d10 + Initiative characteristic. Highest goes first.</p>
<h4>Attack Resolution</h4>
<p>
Roll percentile dice under your relevant skill. Success means you hit,
failure means you miss.
</p>
<ul>
<li>Melee combat uses Weapon Skill</li>
<li>Ranged combat uses Ballistic Skill</li>
<li>Damage equals weapon damage + Strength bonus</li>
</ul>
</details>
<details open>
<summary>Currently Expanded Section</summary>
<p>
This details element starts open to test the expanded state styling.
</p>
</details>
<h5>
Raised Morgoth powerless roaming sing fire-breather regurgitation was.
</h5>
<p>
More glad painted Sauron the White troop holidays captive has. Many
pierced repel bathroom absence glimpse Tom. All right, then. Keep your
secrets.
</p>
<pre>
Prepared Tilda adventure characters crush. Wilds overlook blessed walk
requested. Ligulas sat eavesdropping breathes exceeding dim. Deeper
clever becomes regain Dimholt fronts.
</pre>
<h6>Resilient closest regret vile birthright innards Middle-earth.</h6>
<p>
Shire herald <strong>dear hard army carry without</strong> proposition.
Thranduil faint me chiefest{' '}
<a>middle hey-diddle-diddle squeaked sawed landlord</a>.{' '}
<del>Hallway clot-head&apos;s injury</del> journey Minas Morgul hasty?
Ring sight fit Boffins <kbd>S</kbd>. Manage Bifur ways{' '}
<mark>pity&apos;s swarming</mark> doubt. Wilderness breathing woe liege
<ins>Khazad-dum King&apos;s</ins> handy! Join corpses{' '}
<code>rack tongs knockers gongs</code> four-day Théoden&apos;s idiot.{' '}
<var>Hooded</var> Kingsfoil biding may. Extra{' '}
<time>Radagast the Brown</time> passion cutting skinned. Hurry problem{' '}
<sup>delays</sup> Bucklebury. Corks hell <small>hundred deal</small>{' '}
Barahir unprepared. What&apos;ll Dwarvish down
<em>bought haunted steps</em>. Master&apos;s given Hobbit{' '}
<dfn>afterwards bodies gibbet</dfn>. Towers stars <sub>productive</sub>{' '}
Baggins. Juicy <samp>opinion note</samp> Déagol tough books spreads.
Decide imaginable <q>Goblin-mutant Silvan</q> fellow.{' '}
<cite>Sit Agreed</cite> thick drink pearl tale. Legolas approve
night&apos;s
<abbr>retrieve</abbr> endless.
</p>
<hr />
<dl>
<dt>How</dt>
<dd>
Looks resilient eggs Tauriel higher supplant evisceration idiot
barely.
</dd>
<dt>Names</dt>
<dd>Slugs play Dwalin late parapet ending how morning?</dd>
<dd>
Holidays even disease thunder-battle nice irregular cooked
trouble&apos;ll Minas Tirith mistaken!
</dd>
<dt>Mice</dt>
<dd>
Productive Sit mend ones raiding hutch couldn&apos;t thirty-four.
</dd>
<dd>
Facing others act doing lives usually pity Legolas laws daughter.
</dd>
<dd>
Lords embalmed nature we&apos;d grievances Thror shelter tragedy.
</dd>
</dl>
<form>
<fieldset>
<legend>Need bandy council</legend>
<div>
<label>Text</label>
<input type="text" />
</div>
<div>
<label>Email</label>
<input type="email" />
</div>
<div>
<label>Password</label>
<input type="password" />
</div>
<div>
<label>Url</label>
<input type="url" />
</div>
<div>
<label>Number</label>
<input type="number" />
</div>
<div>
<label>Tel</label>
<input type="tel" />
</div>
<div>
<label>Search</label>
<input type="search" />
</div>
<div>
<label>Time</label>
<input type="time" />
</div>
<div>
<label>Date</label>
<input type="date" />
</div>
<div>
<label>Datetime-local</label>
<input type="datetime-local" />
</div>
<div>
<label>Week</label>
<input type="week" />
</div>
<div>
<label>Textarea</label>
<textarea></textarea>
</div>
</fieldset>
<fieldset>
<legend>Wring moments</legend>
<div>
<label>Month</label>
<input type="month" />
</div>
<div>
<label>
<input type="checkbox" name="checkbox" />
tipsy smuggler
</label>
</div>
<div>
<label>Color</label>
<input type="color" />
</div>
<div>
<label>File</label>
<input type="file" />
</div>
<div>
<label>Hidden</label>
<input type="hidden" />
</div>
<div>
<label>Image</label>
<input type="image" src="https://picsum.photos/96" />
</div>
<div>
<label>malt grass fall door&apos;s infested red</label>
<label>
<input type="radio" name="radio" />
rain joy
</label>
<label>
<input type="radio" name="radio" />
plates grieve
</label>
<label>
<input type="radio" name="radio" />
arranged listened
</label>
</div>
<div>
<label>Range</label>
<input type="range" />
</div>
<div>
<input type="button" value="Button" />
</div>
<div>
<input type="reset" value="Reset" />
</div>
<div>
<input type="submit" value="Submit" />
</div>
<button>infected awoke</button>
<div>
<label>Select</label>
<select>
<optgroup label="rubbish waited tastes">
<option>myth</option>
<option>metals</option>
<option>would</option>
</optgroup>
<optgroup label="thunderstorm particularly breach">
<option>began</option>
<option>threads</option>
<option>tight</option>
</optgroup>
</select>
</div>
</fieldset>
</form>
<div className={styles.wrapper}>
<h1>
DAVE! DAVE! Do Not Let Us Die In The Dark Night Of This Cold Winter!
</h1>
</div>
);
}

View File

@@ -1,4 +1,6 @@
@import url("../styles/variables.css");
@import url("../styles/utilities.css");
@import url("../styles/foundation.css");
@import url("../styles/base.css");
@layer reset, tokens, base, layout, content, components, utilities, animations;
@import url("../styles/tokens.css");
@import url("../styles/globals/foundation.css");
@import url("../styles/globals/base.css");
@import url("../styles/globals/content.css");

View File

@@ -69,12 +69,11 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
<html
lang="en"
className={`${geistSans.variable} ${blaka.variable} ${iosevkaSlab.variable} ${iosevkaMono.variable}`}
>
{children}
</body>
<body>{children}</body>
</html>
);
}

View File

@@ -0,0 +1,3 @@
.article {
@mixin layout-wrapper;
}

View File

@@ -0,0 +1,30 @@
.wrapper {
@mixin mx auto;
@mixin px var(--spacing-comfortable);
max-width: 90ch;
font-size: clamp(1rem, 2.5vw, 1.5rem);
}
.hasMargin {
@supports (anchor-name: --test) {
@media screen and (--bp-margin) {
@mixin px 0;
position: relative;
display: flex;
gap: var(--spacing-generous);
max-width: none;
font-size: clamp(1rem, 2.5vw, 1.25rem);
& .content {
flex: 1 1 auto;
min-width: 40ch;
max-width: 75ch;
}
}
}
}

View File

@@ -0,0 +1,50 @@
import React from 'react';
import Markdoc from '@markdoc/markdoc';
import type { Node } from '@markdoc/markdoc';
import ContentComponents from '@/components/Content';
import SidenoteContainer from '@/components/Content/Sidenote/Container';
import styles from './MarkdocRenderer.module.css';
import { nodes } from '@/lib/markdoc/nodes';
import { tags } from '@/lib/markdoc/tags';
import { collectSideNotes, hasComponents } from '@/lib/markdoc/utils';
interface MarkdocRendererProps {
content: () => Promise<{ node: Node }>;
className?: string;
}
const MARGIN_COMPONENTS = ['Sidenote'];
export default async function MarkdocRenderer({
content,
className,
}: MarkdocRendererProps) {
const { node } = await content();
const errors = Markdoc.validate(node, { tags, nodes });
if (errors.length) {
console.error('Markdoc validation errors:', errors);
throw new Error('Invalid Content');
}
const renderable = Markdoc.transform(node, { tags, nodes });
const sidenotes = collectSideNotes(renderable);
const showMargin = hasComponents(renderable, MARGIN_COMPONENTS);
return (
<div
className={`${styles.wrapper} ${className} ${showMargin ? styles.hasMargin : ''}`}
>
<div className={`${styles.content} content`}>
{Markdoc.renderers.react(renderable, React, {
components: ContentComponents,
})}
</div>
<SidenoteContainer items={sidenotes} />
</div>
);
}

View File

@@ -0,0 +1,10 @@
import MarkdocRenderer from '@/components/Article/Content/MarkdocRenderer';
import { ArticleContent } from '@/lib/types/content';
interface ContentProps {
article: ArticleContent;
}
export default function Content({ article }: ContentProps) {
return <MarkdocRenderer content={article.content} />;
}

View File

View File

@@ -0,0 +1,26 @@
.cover {
@mixin border-t var(--size-3), solid, var(--color-surface-inverse);
}
.image {
width: 100%;
height: auto;
max-height: 60vh;
object-fit: cover;
}
.caption {
padding: var(--spacing-snug);
font-family: var(--font-mono);
color: var(--color-text-inverse);
background-color: var(--color-surface-inverse);
}
.captionwrapper {
font-size: var(--font-size-responsive);
text-align: center;
}
.captiontext {
font-size: var(--typo-size-sm);
}

View File

@@ -0,0 +1,36 @@
import Image from 'next/image';
import styles from './Cover.module.css';
interface CoverProps {
cover: {
readonly src: string;
readonly alt: string;
readonly caption: string;
readonly showInHeader: boolean;
};
}
export default function Cover({ cover }: CoverProps) {
return (
<figure className={styles.cover}>
<Image
src={cover.src}
alt={cover.alt || 'Standard Alt'}
width={0}
height={0}
sizes={'100vw'}
className={styles.image}
/>
<figcaption className={styles.caption}>
<div className={styles.captionwrapper}>
{cover.caption ? (
<span className={styles.captiontext}>{cover.caption}</span>
) : (
<span className={styles.covermeta}>{cover.src}</span>
)}
</div>
</figcaption>
</figure>
);
}

View File

@@ -0,0 +1,4 @@
.container {
margin-bottom: var(--spacing-comfortable);
border-bottom: var(--size-4) solid var(--color-text-primary);
}

View File

@@ -0,0 +1,71 @@
.meta {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--spacing-comfortable);
font-size: var(--font-size-responsive);
}
.section {
@mixin py var(--spacing-snug);
}
.label {
@mixin mb var(--spacing-tight);
@mixin pb var(--spacing-tight);
@mixin border-b var(--size-1), solid, var(--color-surface-inverse);
display: block;
font-size: var(--typo-size-xs);
text-transform: uppercase;
letter-spacing: var(--typo-spacing-comfortable);
}
.author {
@mixin anim-txt-characterglitch;
font-family: var(--font-mono);
font-size: var(--typo-size-sm);
font-weight: var(--typo-weight-bold);
color: var(--color-text-secondary);
text-transform: uppercase;
&:hover {
color: var(--color-primary);
}
}
.taglist {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-tight);
}
.tag {
padding: var(--spacing-tight) var(--spacing-snug);
border: var(--size-1) solid var(--color-palette-charcoal-gray);
font-family: var(--font-mono), monospace;
font-size: var(--typo-size-sm);
font-weight: var(--typo-weight-bold);
text-transform: uppercase;
letter-spacing: var(--typo-spacing-relaxed);
background: var(--color-palette-charcoal-gray);
& .link {
color: var(--color-palette-off-white);
transition: color 0.5s ease-in-out;
&:hover {
color: var(--color-primary);
}
}
}
.updatedate {
font-family: var(--font-mono), monospace;
font-size: var(--typo-size-sm);
font-weight: var(--typo-weight-bold);
color: var(--color-text-secondary);
text-transform: uppercase;
}

View File

@@ -0,0 +1,58 @@
import Link from 'next/link';
import { Author, Tag } from '@/lib/types/taxonomy';
import { toMilitaryDTG } from '@/lib/utils/date';
import styles from './Meta.module.css';
interface MetaProps {
author: Author | null;
tags: Tag[];
updateDate?: string;
publicationDate: string;
}
export default function Meta({
author,
tags,
updateDate,
publicationDate,
}: MetaProps) {
return (
<div className={styles.meta}>
{author && (
<div className={styles.section}>
<span className={styles.label}>Author</span>
<span className={styles.author}>
<Link href={author.slug}>{author.name}</Link>
</span>
</div>
)}
{tags && tags.length > 0 && (
<div className={styles.section}>
<span className={styles.label}>Tags</span>
<ul className={styles.taglist}>
{tags.map((tag) => (
<li key={tag.slug} className={styles.tag}>
<Link className={styles.link} href={`/tags/${tag.slug}`}>
{tag.name}
</Link>
</li>
))}
</ul>
</div>
)}
{publicationDate && (
<div className={styles.section}>
<span className={styles.label}>Last Update</span>
<time className={styles.updatedate}>
{updateDate
? toMilitaryDTG(updateDate)
: toMilitaryDTG(publicationDate)}
</time>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,42 @@
.overline {
@mixin px var(--spacing-comfortable);
background-color: var(--color-surface-inverse);
}
.wrapper {
@mixin py 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);
}
.current {
font-weight: var(--typo-weight-bold);
color: var(--color-primary);
}
.link {
color: var(--color-text-inverse);
transition: color 0.5s ease-in-out;
&:hover {
color: var(--color-tertiary);
}
}
.publicationDate {
font-family: var(--font-mono), monospace;
font-size: var(--typo-size-xs);
font-weight: var(--typo-weight-bold);
color: var(--color-text-inverse);
}

View File

@@ -0,0 +1,48 @@
import Link from 'next/link';
import styles from './Overline.module.css';
import { toMilitaryDTG } from '@/lib/utils/date';
interface OverlineProps {
breadcrumbs?: string[];
publicationDate: string;
}
export default function Overline({
breadcrumbs = [],
publicationDate,
}: OverlineProps) {
const breadcrumber = breadcrumbs.map((crumb, index) => {
const isLast = index === breadcrumbs.length - 1;
const href = `/${breadcrumbs.slice(0, index + 1).join('/')}`;
return (
<li key={index} className={styles.crumb}>
<span className={styles.separator}>/</span>
{isLast ? (
<span className={styles.current}>{crumb}</span>
) : (
<Link href={href} className={styles.link}>
{crumb}
</Link>
)}
</li>
);
});
return (
<div className={styles.overline}>
<div className={styles.wrapper}>
<ul className={styles.breadcrumbs}>
<li className={styles.crumb}>
<Link href="/" className={styles.link}>
dave-dmg.de
</Link>
</li>
{breadcrumber}
</ul>
<time className={styles.publicationDate}>
{toMilitaryDTG(publicationDate)}
</time>
</div>
</div>
);
}

View File

@@ -0,0 +1,13 @@
.wrapper {
@mixin py var(--spacing-cozy);
}
.title {
font-family: var(--el-h1-font-family), serif;
font-size: var(--el-h1-font-size);
font-weight: var(--el-h1-font-weight);
line-height: var(--el-h1-line-height);
color: var(--el-h1-color);
text-transform: var(--el-h1-text-transform);
letter-spacing: var(--el-h1-letter-spacing);
}

View File

@@ -0,0 +1,13 @@
import styles from './Title.module.css';
interface TitleProps {
title: string;
}
export default function Title({ title }: TitleProps) {
return (
<div className={styles.wrapper}>
<h1 className={styles.title}>{title}</h1>
</div>
);
}

View File

@@ -0,0 +1,41 @@
import { ArticleContent } from '@/lib/types/content';
import { Author, Tag } from '@/lib/types/taxonomy';
import { isValidCover } from '@/lib/utils/guards';
import styles from './Header.module.css';
import Overline from '@/components/Article/Header/Overline';
import Title from '@/components/Article/Header/Title';
import Meta from '@/components/Article/Header/Meta';
import Cover from '@/components/Article/Header/Cover';
interface HeaderProps {
article: ArticleContent;
breadcrumbs?: string[];
author: Author | null;
tags: Tag[];
}
export default function Header({
article,
breadcrumbs,
author,
tags,
}: HeaderProps) {
const { title, meta, cover } = article;
return (
<header className={styles.container}>
<Overline
publicationDate={article.meta.publicationDate}
breadcrumbs={breadcrumbs}
/>
<Title title={article.title} />
{cover && isValidCover(cover) && <Cover cover={cover} />}
<Meta
tags={tags}
author={author}
updateDate={meta.updateDate}
publicationDate={meta.publicationDate}
/>
</header>
);
}

View File

@@ -0,0 +1,32 @@
import type { ArticleContent } from '@/lib/types/content';
import { Author, Tag } from '@/lib/types/taxonomy';
import Header from './Header';
import Content from '@/components/Article/Content';
import styles from './Article.module.css';
interface ArticleProps {
article: ArticleContent;
author: Author | null;
tags: Tag[];
breadcrumbs?: string[];
}
export default function Article({
article,
author,
tags,
breadcrumbs,
}: ArticleProps) {
return (
<article className={styles.article}>
<Header
article={article}
author={author}
tags={tags}
breadcrumbs={breadcrumbs}
/>
<Content article={article} />
</article>
);
}

View File

@@ -0,0 +1,56 @@
.item {
& .summary {
--el-summary-marker-symbol-closed: "▾";
--el-summary-marker-symbol-open: "▾";
--el-summary-marker-symbol-transform-open: rotate(180deg);
position: relative;
padding: var(--spacing-snug);
font-family: var(--el-summary-font-family);
font-size: var(--el-summary-font-size);
font-weight: var(--el-summary-font-weight);
line-height: var(--el-summary-line-height);
color: var(--el-summary-fg);
text-transform: var(--el-summary-text-transform);
list-style: none;
background: var(--el-summary-bg);
&::before {
content: var(--el-summary-marker-symbol-closed);
position: absolute;
right: 0;
font-size: 1em;
transition: transform 0.2s ease;
}
}
& .content {
overflow: hidden;
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 2s ease;
& .inner {
min-height: 0;
}
}
&[open] {
& .summary {
&::before {
content: var(--el-summary-marker-symbol-open);
transform: var(--el-summary-marker-symbol-transform-open);
}
}
& .content {
grid-template-rows: 1fr;
}
}
}

View File

@@ -0,0 +1,39 @@
'use client';
import React from 'react';
import styles from './AccordionItem.module.css';
interface AccordionItemProps {
title: string;
defaultOpen: boolean;
children: React.ReactNode;
}
export default function AccordionItem({
title,
defaultOpen,
children,
}: AccordionItemProps) {
const [isOpen, setIsOpen] = React.useState(defaultOpen);
return (
<details
className={styles.item}
open={isOpen}
onToggle={(e) => setIsOpen(e.currentTarget.open)}
>
<summary
className={styles.summary}
onClick={(e) => {
e.preventDefault();
setIsOpen(!isOpen);
}}
>
{title}
</summary>
<div className={styles.content}>
<div className={styles.inner}>{children}</div>
</div>
</details>
);
}

View File

@@ -0,0 +1,11 @@
import React from 'react';
import styles from './Accordion.module.css';
interface AccordionProps {
children: React.ReactNode;
}
export default function Accordion({ children }: AccordionProps) {
return <div className={styles.accordion}>{children}</div>;
}

View File

@@ -0,0 +1,27 @@
.container {
margin-block: var(--el-p-vspace-top) var(--el-p-vspace-bottom);
padding: var(--spacing-snug) 0 var(--spacing-snug) var(--spacing-comfortable);
border-left: var(--size-4) solid var(--color-text-tertiary);
}
.quote {
font-family: var(--el-blockquote-font-family), serif;
font-size: var(--el-blockquote-font-size);
font-weight: var(--el-blockquote-font-weight);
font-style: var(--el-blockquote-font-style);
line-height: var(--el-blockquote-line-height);
color: var(--el-blockquote-color);
}
.attribution,
.source {
font-family: var(--font-mono);
}
.link {
@mixin anim-txt-typewriter;
&:hover {
color: var(--color-primary);
}
}

View File

@@ -0,0 +1,70 @@
import { marked } from 'marked';
import styles from './Blockquote.module.css';
interface BlockquoteProps {
quote: string;
attribution?: string;
source?: string;
url?: string;
}
export default function Blockquote({
quote,
attribution,
source,
url,
}: BlockquoteProps) {
const hasAttribution = !!attribution;
const hasSource = !!source;
const hasUrl = !!url;
const showFooter = hasSource || hasAttribution;
const attributionLinked = hasAttribution && hasUrl && !hasSource;
const sourceLinked = hasSource && hasUrl;
return (
<blockquote className={styles.blockquote}>
<div
className={styles.quote}
dangerouslySetInnerHTML={{ __html: marked.parse(quote) }}
/>
{showFooter && (
<footer className={styles.footer}>
{hasAttribution && (
<cite className={styles.attribution}>
{attributionLinked ? (
<a
href={url}
className={styles.link}
target="_blank"
rel="noopener noreferrer"
>
{attribution}
</a>
) : (
attribution
)}
</cite>
)}
{hasSource && (
<cite className={styles.source}>
{' '}
{sourceLinked ? (
<a
href={url}
className={styles.link}
target="_blank"
rel="noopener noreferrer"
>
{source}
</a>
) : (
source
)}
</cite>
)}
</footer>
)}
</blockquote>
);
}

View File

@@ -0,0 +1,144 @@
@layer component {
.container {
--callout-bg: var(--color-surface-inverse);
--callout-fg: var(--color-text-inverse);
--callout-symbol: "";
--callout-symbol-color: var(--color-text-inverse);
@mixin my var(--spacing-cozy);
position: relative;
padding: var(--spacing-cozy) var(--spacing-cozy) var(--spacing-cozy) var(--size-12) ;
border: var(--size-1) solid var(--color-surface-inverse);
box-shadow: 2px 2px 0 oklch(from var(--color-surface-inverse) calc(l - 0.075) c h), 4px 4px 0 oklch(from var(--color-surface-inverse) calc(l - 0.2) c h);
&::before {
@mixin pt 0.425em;
content: var(--callout-symbol);
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: flex-start;
justify-content: center;
width: var(--size-8);
height: 100%;
font-size: 1.5em;
color: var(--callout-symbol-color);
background-color: var(--color-surface-inverse);
}
}
.title {
@mixin text-xl;
font-family: var(--font-header);
font-weight: var(--typo-weight-black);
}
.badge {
position: absolute;
top: 0;
right: 0;
padding: 0.2em 0.5em;
font-family: var(--font-mono);
font-size: var(--typo-size-xs);
font-weight: var(--typo-weight-black);
color: var(--callout-fg);
background: var(--callout-bg);
}
.example {
--callout-bg: var(--color-palette-fuchsia);
--callout-symbol: "◆";
}
.info {
--callout-bg: var(--color-state-info);
--callout-symbol: "‽";
}
.warning {
--callout-bg: var(--color-state-warning);
--callout-symbol: "‼";
}
.tip {
--callout-bg: var(--color-palette-lime-green);
--callout-symbol: "★";
}
.spoiler {
& .label {
@mixin text-xl;
font-family: var(--font-header);
font-weight: var(--typo-weight-black);
&::before {
content: "[REDACTED] ";
color: var(--color-state-error);
}
&:hover {
cursor: pointer;
}
}
& .content {
pointer-events: none;
position: relative;
min-height: 3em;
&::after {
content: '████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
font-family: var(--font-mono);
line-height: inherit;
color: var(--color-text-primary);
letter-spacing: -0.15em;
word-break: break-all;
white-space: pre-wrap;
background: var(--color-text-primary);
}
}
& .toggle {
@util hide-visually;
&:checked {
& ~ .label {
&::before {
content: '[REVEALED] ';
color: var(--color-state-success);
}
}
& ~ .content {
&::after {
content: none;
}
}
}
}
}
}

View File

@@ -0,0 +1,32 @@
import React from 'react';
import styles from './Callout.module.css';
interface CalloutProps {
type: 'default' | 'example' | 'info' | 'warning' | 'tip' | 'spoiler';
title?: string;
children: React.ReactNode;
}
export default function Callout({ type, title, children }: CalloutProps) {
const isSpoiler = type === 'spoiler';
const spoilerID = isSpoiler
? `spoiler-${Math.random().toString(36).substr(2, 9)}`
: undefined;
return (
<div className={`${styles.container} ${styles[type]}`}>
{isSpoiler ? (
<>
<input type="checkbox" id={spoilerID} className={styles.toggle} />
<label htmlFor={spoilerID} className={styles.label}>
{title || 'Show spoiler'}
</label>
</>
) : (
<p className={styles.title}>{title}</p>
)}
<span className={styles.badge}>{type.toUpperCase()}</span>
<div className={`${styles.content}`}>{children}</div>
</div>
);
}

View File

@@ -0,0 +1,24 @@
.term {
margin-block: var(--el-dt-vspace-top) var(--el-dt-vspace-bottom);
padding: var(--el-dt-padding);
font-family: var(--el-dt-font-family), serif;
font-size: var(--el-dt-font-size);
font-weight: var(--el-dt-font-weight);
line-height: var(--el-dt-line-height);
color: var(--el-dt-color);
text-transform: var(--el-dt-text-transform);
letter-spacing: var(--el-dt-letter-spacing);
background: var(--el-dt-background);
}
.def {
margin-block: var(--el-dd-vspace-top) var(--el-dd-vspace-bottom);
padding: var(--el-dd-indent);
font-family: var(--el-dd-font-family), sans-serif;
font-size: var(--el-dd-font-size);
line-height: var(--el-dd-line-height);
color: var(--el-dd-color);
}

View File

@@ -0,0 +1,28 @@
import { marked } from 'marked';
import styles from './DefinitionItem.module.css';
interface DefinitionItemProps {
term: string;
definitions: string[];
}
export default function DefinitionItem({
term,
definitions,
}: DefinitionItemProps) {
return (
<>
<dt className={styles.term}>{term}</dt>
{definitions.map((def, idx) => (
<dd
key={idx}
className={styles.def}
dangerouslySetInnerHTML={{
__html: marked.parseInline(def),
}}
/>
))}
</>
);
}

View File

@@ -0,0 +1,5 @@
.list {
margin-block: var(--el-dl-vspace-top) var(--el-dl-vspace-bottom);
font-size: var(--el-dl-font-size);
line-height: var(--el-dl-line-height);
}

View File

@@ -0,0 +1,10 @@
import React from 'react';
import styles from './DefinitionList.module.css';
interface DefinitionListProps {
children?: React.ReactNode;
}
export default function DefinitionList({ children }: DefinitionListProps) {
return <dl className={styles.list}>{children}</dl>;
}

View File

@@ -0,0 +1,29 @@
.figure {
position: relative;
border: var(--size-2) solid var(--color-surface-inverse);
clip-path: polygon(0 var(--size-12), var(--size-12) 0, 100% 0, 100% 100%, 0 100%);
}
.caption {
padding: var(--spacing-snug);
font-family: var(--font-mono);
font-size: var(--typo-size-sm);
line-height: var(--typo-leading-relaxed);
color: var(--color-text-inverse);
background: var(--color-surface-inverse);
}
.credit {
display: block;
margin-top: var(--spacing-tight);
font-family: var(--font-mono);
font-size: var(--typo-size-xs);
font-style: normal;
color: var(--color-text-inverse);
text-align: right;
}

View File

@@ -0,0 +1,34 @@
import Image from 'next/image';
import styles from './Figure.module.css';
interface FigureProps {
src: string;
alt: string;
caption?: string;
credit?: string;
}
export default function Figure({ src, alt, caption, credit }: FigureProps) {
const hasCaption = !!caption;
const hasCredit = !!credit;
const showFigcaption = hasCaption || hasCredit;
return (
<figure className={styles.figure}>
<Image
src={src}
alt={alt}
width={800}
height={600}
className={styles.image}
/>
{showFigcaption && (
<figcaption className={styles.caption}>
{hasCaption && <span className={styles.text}>{caption}</span>}
{hasCredit && <cite className={styles.credit}>[{credit}]</cite>}
</figcaption>
)}
</figure>
);
}

View File

@@ -0,0 +1,3 @@
.column {
position: relative;
}

View File

@@ -0,0 +1,23 @@
import React from 'react';
import styles from './Column.module.css';
interface ColumnProps {
style: {
colspan: number;
};
children?: React.ReactNode;
}
export default function Column({ style, children }: ColumnProps) {
return (
<div
className={styles.column}
style={{
gridColumn: `span ${style.colspan}`,
}}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,3 @@
.row {
display: grid;
}

View File

@@ -0,0 +1,29 @@
import React from 'react';
import styles from './Row.module.css';
interface RowProps {
style: {
cols: number;
gap: string;
align: string;
justify: string;
};
children: React.ReactNode;
}
export default function Row({ style, children }: RowProps) {
return (
<div
className={`${styles.row}`}
style={{
gridTemplateColumns: `repeat(${style.cols}, 1fr)`,
gap: `${style.gap}`,
alignItems: `${style.align}`,
justifyContent: `${style.justify}`,
}}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,69 @@
.container {
flex: 1 0 auto;
min-width: var(--size-96);
}
.wrapper {
@supports (anchor-name: --test) {
@media screen and (--bp-margin) {
position: absolute;
top: anchor(top);
}
}
}
.note {
@mixin text-xs;
@mixin my var(--spacing-cozy);
position: relative;
padding-left: var(--size-12);
font-family: var(--font-mono);
@supports (anchor-name: --test) {
@media screen and (--bp-margin) {
min-width: var(--size-64);
margin-top: 0;
padding: var(--spacing-cozy) var(--spacing-cozy) var(--spacing-cozy) var(--size-12) ;
border: var(--size-1) solid var(--color-surface-inverse);
background: var(--color-surface-elevated-1);
box-shadow: 2px 2px 0 var(--color-palette-woodsmoke), 4px 4px 0 var(--color-palette-charcoal-gray);
}
}
}
.marker {
position: absolute;
top: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: var(--size-8);
font-size: var(--typo-size-3xl);
font-weight: var(--typo-weight-black);
color: var(--color-text-inverse);
background-color: var(--color-surface-inverse);
}
.content {
display: contents;
}
.ref {
@mixin ml var(--spacing-snug);
@mixin anim-txt-characterglitch;
font-weight: var(--typo-weight-black);
color: var(--color-text-quarternary);
&:hover {
color: var(--color-primary);
}
}

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { marked } from 'marked';
import type { Sidenote } from '@/lib/types/components';
import styles from './Container.module.css';
interface ContainerProps {
items: Sidenote[];
}
export default function Container({ items }: ContainerProps) {
return (
<aside className={styles.container}>
{items.map((sidenote: Sidenote) => (
<div
key={sidenote.id}
id={sidenote.id}
className={styles.wrapper}
style={
{ positionAnchor: `--note-${sidenote.id}` } as React.CSSProperties
}
>
<div className={`${styles.note} ${styles[sidenote.type]}`}>
<span className={styles.marker}>
<span className={styles.symbol}>{sidenote.marker}</span>
</span>
<div
className={`${styles.content} content`}
dangerouslySetInnerHTML={{
__html: marked.parseInline(sidenote.content),
}}
/>
<a href={`#ref-${sidenote.id}`} className={styles.ref}>
</a>
</div>
</div>
))}
</aside>
);
}

View File

@@ -0,0 +1,16 @@
.ref {
@mixin px var(--spacing-tight);
scroll-margin-top: calc(var(--el-header-height) + var(--spacing-comfortable));
display: inline-block;
font-weight: var(--typo-weight-black);
color: var(--color-secondary);
transition: color 0.5s ease-in-out;
&:hover {
color: var(--color-primary);
}
}

View File

@@ -0,0 +1,17 @@
import type { Sidenote } from '@/lib/types/components';
import styles from './Item.module.css';
export default function SidenoteItem({ id, marker, content, type }: Sidenote) {
return (
<sup>
<a
href={`#${id}`}
className={styles.ref}
id={`ref-${id}`}
style={{ anchorName: `--note-${id}` } as React.CSSProperties}
>
[{marker}]
</a>
</sup>
);
}

View File

@@ -0,0 +1,25 @@
import Row from '@/components/Content/Grid/Row';
import Column from '@/components/Content/Grid/Column';
import Sidenote from '@/components/Content/Sidenote/Item';
import DefinitionList from '@/components/Content/DefinitionList';
import DefinitionItem from '@/components/Content/DefinitionList/DefinitionItem';
import Accordion from '@/components/Content/Accordion';
import AccordionItem from '@/components/Content/Accordion/AccordionItem';
import Blockquote from '@/components/Content/Blockquote';
import Figure from '@/components/Content/Figure';
import Callout from '@/components/Content/Callout';
const ContentComponents = {
Column,
Row,
Sidenote,
DefinitionList,
DefinitionItem,
Accordion,
AccordionItem,
Blockquote,
Figure,
Callout,
};
export default ContentComponents;

View File

@@ -0,0 +1,77 @@
@layer components {
.header {
@mixin py var(--el-header-paddingY);
position: sticky;
z-index: 9;
top: 0;
width: var(--dim-full);
color: var(--color-text-inverse);
background-color: var(--color-surface-inverse);
}
.inner {
@mixin layout-wrapper;
display: flex;
flex-direction: row;
gap: var(--spacing-cozy);
align-items: center;
justify-content: flex-start;
font-size: var(--el-header-font-size);
line-height: var(--el-header-line-height);
}
.logo {
@mixin anim-txt-characterglitch;
font-family: var(--font-mono);
animation:
logo-pulse 5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
&:hover {
transform: translate(0, 0);
color: var(--color-secondary);
}
}
.pagename {
font-family: var(--font-mono);
& .bracket {
color: var(--color-secondary);
}
}
.menutoggle {
@mixin ml auto;
& button {
cursor: pointer;
font-family: var(--font-mono);
transition: color 0.2s ease-out;
&:hover {
color: var(--color-primary);
}
&:active {
transform: scale(0.95);
transition: transform 0.1s ease-out;
}
}
}
@keyframes logo-pulse {
0% { opacity: 1; }
25% { opacity: 0.66; }
50% { opacity: 0.33; }
75% { opacity: 0.66; }
100% { opacity: 1; }
}
}

View File

@@ -0,0 +1,48 @@
'use client';
import Link from 'next/link';
import { useMenu } from '@/contexts/MenuContext';
import styles from './Header.module.css';
export default function Header() {
const { isMenuOpen, closeMenu, openMenu, startClosing, resetClosing } =
useMenu();
const handleMenuToggle = () => {
if (isMenuOpen) {
startClosing();
setTimeout(() => {
closeMenu();
resetClosing();
}, 800);
} else {
openMenu();
}
};
return (
<header className={styles.header}>
<div className={styles.inner}>
<div className={styles.logo}>
<Link href="/"></Link>
</div>
<div className={styles.pagename}>
<span>
dave <span className={styles.bracket}>[</span>dmg
<span className={styles.bracket}>]</span>
</span>
</div>
<div className={styles.menutoggle}>
<button
onClick={() => handleMenuToggle()}
aria-label={isMenuOpen ? 'Close menu' : 'Open menu'}
aria-expanded={isMenuOpen}
aria-controls="main-menu"
>
{isMenuOpen ? '[×]' : '[⚍]'}
</button>
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,403 @@
@layer components {
/* === MenuGrid === */
.menu {
pointer-events: none;
position: fixed;
z-index: 9;
top: var(--el-header-height);
left: 0;
display: grid;
grid-auto-columns: 1fr;
grid-template-areas:
"area_1"
"area_2"
"area_3"
"area_4"
"area_5";
grid-template-columns: 1fr;
grid-template-rows: repeat(5, auto);
gap: 0;
gap: var(--spacing-cozy);
width: 100vw;
height: 100%;
max-height: calc(100vh - var(--el-header-height));
visibility: hidden;
opacity: 0;
background-color: var(--color-palette-charcoal-gray);
transition: opacity 0.3s ease-out, visibility 0.3s ease-out;
&.isOpen {
pointer-events: auto;
visibility: visible;
opacity: 1;
}
@media screen and (--bp-tablet) {
@mixin px var(--spacing-comfortable);
grid-template-areas:
"area_1"
"area_2"
"area_3"
"area_4"
"area_5";
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: var(--spacing-comfortable);
}
@media screen and (--bp-desktop) {
display: grid;
grid-template-areas:
"area_4 area_4 area_3 area_3 area_3 area_3 area_2"
"area_4 area_4 area_3 area_3 area_3 area_3 area_2"
"area_5 area_5 area_3 area_3 area_3 area_3 area_2"
"area_5 area_5 area_3 area_3 area_3 area_3 area_1"
"area_5 area_5 area_3 area_3 area_3 area_3 area_1";
grid-template-columns: 2.5fr 1fr 3fr 1.5fr 1fr 1fr 4fr;
grid-template-rows: 3fr 1fr 2.5fr 1.5fr 2fr;
}
}
/* === MenuArea === */
.area {
/* === AREA VARIABLES === */
--area-bg: transparent;
--area-border: transparent;
--area-animation-keyframe: none;
--area-animation-duration: 0s;
--area-animation-timing: linear;
--area-bg-filter: grayscale(100%) contrast(150) brightness(150);
/** === TITLE VARIABLES === */
--title-color: var(--color-palette-light-silver);
--title-font: var(--font-header);
--title-font-size: var(--typo-size-2xl);
--title-font-weight: var(--typo-weight-black);
--title-line-height: 1;
--title-transform: uppercase;
--title-spacing: var(--typo-spacing-comfortable);
--title-hover-color: var(--color-tertiary);
--title-current-color: var(--color-primary);
--title-current-bg: var(--color-primary);
/** === SUBLINK VARIABLES === */;
--sublink-color: var(--color-palette-light-silver);
--sublink-font: var(--font-header);
--sublink-font-size: var(--typo-size-2xl);
--sublink-font-weight: var(--typo-weight-black);
--sublink-transform: uppercase;
--sublink-spacing: var(--typo-spacing-relaxed);
--sublink-line-height: var(--typo-leading-relaxed);
--sublink-letter-spacing: var(--typo-spacing-relaxed);
--sublink-current-color: var(--color-primary);
--sublink-current-bg: var(--color-primary);
/** === SUBTITLE VARIABLES === **/
--divider-color: var(--color-palette-light-silver);
--divider-width: var(--size-6);
--divider-height: var(--size-2);
--divider-font: var(--font-mono);
--divider-symbol: ;
--divider-font-size: var(--typo-size-2xl);
--divider-padding: 0 var(--typo-spacing-cozy);
--subtitle-color: var(--color-palette-light-silver);
--subtitle-font: var(--font-mono);
--subtitle-font-size: var(--typo-size-2xl);
--subtitle-text-transform: uppercase;
--subtitle-letter-spacing: var(--typo-spacing-cozy);
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: var(--size-1) solid var(--area-border);
text-align: center;
background: var(--area-bg);
transition: all 0.2s ease-in-out;
&:not(.current) {
&:hover {
animation: var(--area-animation-keyframe) var(--area-animation-duration) var(--area-animation-timing);
}
}
}
.area_1 { grid-area: area_1; }
.area_2 { grid-area: area_2; }
.area_3 { grid-area: area_3; }
.area_4 { grid-area: area_4; }
.area_5 { grid-area: area_5; }
.hasBGImg {
&:hover {
.bgImg {
animation: var(--area-animation-keyframe) var(--area-animation-duration) var(--area-animation-timing);
}
}
}
.bgImg {
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
object-fit: cover;
filter: var(--area-bg-filter);
@media screen and (--bp-tablet-down) {
display: none;
}
}
/* === MenuTitle === */
.title {
position: relative;
}
.mainlink {
position: relative;
font-family: var(--title-font);
font-size: var(--title-font-size);
font-weight: var(--title-font-weight);
line-height: var(--title-line-height);
color: var(--title-color);
text-transform: var(--title-transform);
letter-spacing: var(--title-spacing);
transition: any 0.5s ease-in-out;
&:focus {
&:hover {
color: var(--title-current-color);
}
@media screen and (--bp-tablet-down) {
outline: none;
&:hover {
color: var(--title-current-color);
}
}
}
&.current {
pointer-events: none;
@media screen and (--bp-tablet-down) {
&:hover {
color: var(--title-current-color);
}
}
@media screen and (--bp-desktop) {
}
}
@media screen and (--bp-tablet-down) {
transition: border 0.5s ease-in-out;
}
}
/* === MenuSublinks === */
.list {
@media screen and (--bp-tablet-down) {
display: none;
}
}
.item {
position: relative;
}
.sublink {
position: relative;
font-family: var(--sublink-font);
font-size: var(--sublink-font-size);
font-weight: var(--sublink-font-weight);
line-height: var(--sublink-line-height);
color: var(--sublink-color);
text-transform: var(--sublink-transform);
letter-spacing: var(--sublink-spacing);;
&:not(.current),
&:not(.focus) {
transition: var(--sublink-hover-transition);
&::after,
&::before {
position: absolute;
font-family: var(--sublink-hover-decorator-font);
opacity: 0;
transition: var(--sublink-hover-decorator-transition);
}
&::before {
content: var(--sublink-hover-decorator-left-symbol);
left: 0;
}
&::after {
content: var(--sublink-hover-decorator-right-symbol);
right: 0;
}
&:hover {
color: var(--sublink-hover-color);
&::before {
top: var(--sublink-hover-decorator-left-pos-y);
left: var(--sublink-hover-decorator-left-pos-x);
transform: var(--sublink-hover-decorator-left-transform);
opacity: 1;
}
&::after {
top: var(--sublink-hover-decorator-right-pos-y);
right: var(--sublink-hover-decorator-rightt-pos-x);
transform: var(--sublink-hover-decorator-right-transform);
opacity: 1;
}
}
&:focus {
&:hover {
color: var(--sublink-current-color);
}
}
&.current {
pointer-events: none;
}
}
}
/* === MenuSubtitle === */
.wrapper {
position: relative;
@media screen and (--bp-tablet-down) {
display: none;
}
}
/* === UTILITY Classes */
@media screen and (--bp-desktop) {
.pos_tr {
position: absolute;
top: 1em;
right: 1em;
}
.pos_tc {
position: absolute;
top: 1em;
left: 50%;
transform: translateX(-50%);
}
.pos_tl {
position: absolute;
top: 1em;
left: 1em;
}
.pos_cr {
position: absolute;
top: 50%;
right: 1em;
transform: translateY(-50%);
}
.pos_c {
position: absolute;
bottom: 50%;
left: 50%;
transform: translate(-50%, 50%);
}
.pos_cl {
position: absolute;
top: 50%;
right: 1em;
transform: translateY(-50%);
}
.pos_br {
position: absolute;
right: 1em;
bottom: 1em;
}
.pos_bc {
position: absolute;
bottom: 1em;
left: 50%;
transform: translateX(-50%);
}
.pos_bl {
position: absolute;
bottom: 1em;
left: 1em;
}
.vertical-rl {
writing-mode: vertical-rl;
& * {
writing-mode: vertical-rl;
}
}
.vertical-lr {
writing-mode: vertical-lr;
& * {
writing-mode: vertical-lr;
}
}
.sideways-rl {
writing-mode: sideways-rl;
& * {
writing-mode: sideways-rl;
}
}
.sideways-lr {
writing-mode: sideways-lr;
& * {
writing-mode: sideways-lr;
}
}
}
/* === CATEGORY Variants === */
@media screen and (--bp-desktop) {
.meta {}
.kitchensink {}
.awq {}
.worldburner {}
.chainbreaker {}
}
}

View File

@@ -0,0 +1,106 @@
@layer components {
.area {
--area-bg: transparent;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
background: var(--area-bg);
@media screen and (--bp-tablet-down) {
border-bottom: var(--size-1) solid var(--color-palette-gunmetal);
&:last-child {
border-bottom: none;
}
}
}
.area_1 {
position: relative;
grid-area: area_1;
}
.area_2 {
position: relative;
grid-area: area_2;
}
.area_3 {
position: relative;
grid-area: area_3;
}
.area_4 {
position: relative;
grid-area: area_4;
}
.area_5 {
position: relative;
grid-area: area_5;
}
.hasBGImg {
position: relative;
&:hover {
& .bgImg {
animation: var(--area-animation-keyframe) var(--area-animation-duration) var(--area-animation-timing);
}
}
}
.bgImg {
--area-bg-animation-keyframe: none;
--area-bg--animation-duration: 0s;
--area-bg--animation-timing: linear;
--area-bg-filter: grayscale(100%) contrast(10) brightness(250);
--area-bg-blendmode: color-dodge;
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
object-fit: cover;
filter: var(--area-bg-filter);
mix-blend-mode: var(--area-bg-blendmode);
@media screen and (--bp-tablet-down) {
display: none;
}
}
@media screen and (--bp-desktop) {
.chainbreaker {
--area-animation-keyframe: var(--kf-color-bleed);
--area-animation-duration: var(--img-colorbleed-duration);
--area-animation-timing: var(--img-colorbleed-timing);
& .bgImg {
z-index: -2;
}
&.hasBGImg {
--divider-color: transparent;
--title-color: transparent;
--subtitle-color: transparent;
--subtitle-transition: all 0.3s ease-in-out;
--title-transition: color 0.3s ease-in-out;
&:hover {
--divider-color: var(--color-tertiary);
--title-color: var(--color-tertiary);
--subtitle-color: var(--color-tertiary);
}
}
}
}
}

View File

@@ -0,0 +1,46 @@
import React from 'react';
import MenuItem from '@/components/Page/Menu//MenuItem/';
import { useCurrentPath } from '@/hooks/useCurrentPath';
import styles from './MenuArea.module.css';
import { NavigationItem } from '@/lib/types/navigation';
interface MenuAreaProps {
item: NavigationItem;
}
const MenuArea = React.memo(({ item }: MenuAreaProps) => {
const hasBGImg = !!item.background;
const { isCurrentPath } = useCurrentPath();
const areaClasses = React.useMemo(() => {
return [
styles.area,
styles[item.gridPosition],
styles[item.variant],
isCurrentPath(item.path) ? styles.current : '',
item.background ? styles.hasBGImg : null,
]
.filter(Boolean)
.join(' ');
}, [item.background, isCurrentPath]);
return (
<section className={areaClasses}>
{hasBGImg && (
<img
className={styles.bgImg}
src={item.background}
alt={`Background Image for ${item.name}`}
/>
)}
<MenuItem item={item} />
</section>
);
});
MenuArea.displayName = 'MenuArea';
export default MenuArea;

View File

@@ -0,0 +1,117 @@
@layer components {
.menu {
--grid-bg: var(--color-palette-charcoal-gray);
--grid-fg: var(--color-palette-light-silver);
--area-blendmode: normal;
/* === MenuTitle Vars === */
--title-color: var(--grid-fg);
--title-font: var(--font-mono);
--title-font-size: var(--typo-size-2xl);
--title-font-weight: var(--typo-weight-black);
--title-line-height: 1;
--title-transform: uppercase;
--title-spacing: var(--typo-spacing-comfortable);
--title-hover-fg: var(--primary);
--title-hover-bg: transparent;
--title-current-bg: var(--color-tertiary);
--title-current-fg: var(--grid-bg);
--title-focus-fg: var(--color-secondary);
--title-focus-bg: var(--grid-bg);
--title-transition: none;
/* === MenuSublinks Vars === */
--sublink-color: var(--grid-fg);
--sublink-font: var(--font-mono);
--sublink-font-size: var(--typo-size-xl);
--sublink-font-weight: var(--typo-weight-light);
--sublink-transform: uppercase;
--sublink-line-height: var(--typo-leading-relaxed);
--sublink-spacing: var(--typo-spacing-loosest);
--sublink-hover-fg: var(--color-tertiary);
--sublink-hover-bg: transparent;
--sublink-current-fg: var(--grid-bg);
--sublink-current-bg: var(--color-primary);
--sublink-focus-fg: var(--color-secondary);
--sublink-focus-bg: var(--grid-bg);
--sublink-transition: none;
/* === MenuSubtitle Vars === */
--divider-color: var(--grid-fg);
--divider-width: var(--size-12);
--divider-height: var(--size-2);
--divider-font: var(--font-mono);
--divider-line-height: 1;
--divider-font-size: var(--typo-size-3xl);
--divider-padding: 0 var(--typo-spacing-cozy);
--subtitle-font: var(--font-mono);
--subtitle-color: var(--grid-fg);
--subtitle-font-size: var(--typo-size-xl);
--subtitle-text-transform: uppercase;
--subtitle-letter-spacing: var(--typo-spacing-snug);
--subtitle-transition: none;
pointer-events: none;
position: fixed;
z-index: 9;
top: var(--el-header-height);
left: 0;
display: grid;
grid-auto-columns: 1fr;
grid-template-areas:
"area_1"
"area_2"
"area_3"
"area_4"
"area_5";
grid-template-columns: 1fr;
grid-template-rows: repeat(5, auto);
width: 100vw;
height: calc(100vh - var(--el-header-height));
color: var(--grid-fg);
visibility: hidden;
opacity: 0;
background-color: var(--grid-bg);
clip-path: inset(0 0 100% 0);
transition: clip-path 0.35s steps(8, end);
&.isOpen {
pointer-events: auto;
transform: translateY(0);
visibility: visible;
opacity: 1;
clip-path: inset(0 0 0 0);
}
&.isClosing {
clip-path: inset(0 0 100% 0);
transition: clip-path 0.25s steps(6, end);
&.isOpen {
clip-path: inset(0 0 100% 0);
transition: clip-path 0.25s steps(6, end);
}
}
@media screen and (--bp-desktop) {
display: grid;
grid-template-areas:
"area_4 area_4 area_3 area_3 area_3 area_3 area_2"
"area_4 area_4 area_3 area_3 area_3 area_3 area_2"
"area_5 area_5 area_3 area_3 area_3 area_3 area_2"
"area_5 area_5 area_3 area_3 area_3 area_3 area_1"
"area_5 area_5 area_3 area_3 area_3 area_3 area_1";
grid-template-columns: 2.5fr 1fr 3fr 1.5fr 1fr 1fr 4fr;
grid-template-rows: 3fr 1fr 2.5fr 1.5fr 2fr;
}
}
}

View File

@@ -0,0 +1,66 @@
'use client';
import React from 'react';
import { useMenu } from '@/contexts/MenuContext';
import MenuArea from '@/components/Page/Menu/MenuArea/';
import styles from './MenuGrid.module.css';
interface MenuGridProps {
navigationData: Awaited<
ReturnType<
typeof import('@/lib/readers/system/navigation').getNavigationData
>
>;
}
export default function MenuGrid({ navigationData }: MenuGridProps) {
const { isMenuOpen, closeMenu, isClosing, startClosing, resetClosing } =
useMenu();
const menuRef = React.useRef<HTMLElement>(null);
const handleClose = React.useCallback(() => {
startClosing();
setTimeout(() => {
closeMenu();
resetClosing();
}, 800);
}, [closeMenu, startClosing, resetClosing]);
React.useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isMenuOpen) {
handleClose();
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [isMenuOpen, handleClose]);
React.useEffect(() => {
if (isMenuOpen && menuRef.current) {
const initialFocus = menuRef.current.querySelector(
'a, button'
) as HTMLElement;
initialFocus?.focus();
}
}, [isMenuOpen]);
if (!navigationData) return null;
const { items } = navigationData;
return (
<nav
ref={menuRef}
className={`${styles.menu} ${isMenuOpen ? styles.isOpen : ''} ${isClosing ? styles.isClosing : ''}`}
aria-hidden={!isMenuOpen}
aria-label="Main navigation"
>
{items.map((item, index) => (
<MenuArea key={item.gridPosition} item={item} />
))}
</nav>
);
}

View File

@@ -0,0 +1,24 @@
import { hasSublinks, hasSubtitle } from '@/lib/types/navigation';
import MenuTitle from '@/components/Page/Menu/MenuTitle/';
import MenuSublinks from '@/components/Page/Menu/MenuSublinks/';
import MenuSubtitle from '@/components/Page/Menu/MenuSubtitle/';
import { NavigationItem } from '@/lib/types/navigation';
interface MenuItem {
item: NavigationItem;
}
export default function MenuItem({ item }: MenuItem) {
return (
<>
<MenuTitle path={item.path} name={item.name} variant={item.variant} />
{hasSublinks(item) && (
<MenuSublinks sublinks={item.sublinks.value} variant={item.variant} />
)}
{hasSubtitle(item) && (
<MenuSubtitle subtitle={item.subtitle.value} variant={item.variant} />
)}
</>
);
}

View File

@@ -0,0 +1,57 @@
@layer components {
.list {
@media screen and (--bp-tablet-down) {
@util hide-visually;
}
}
.item {
position: relative;
}
.sublink {
font-family: var(--sublink-font);
font-size: var(--sublink-font-size);
font-weight: var(--sublink-font-weight);
line-height: var(--sublink-line-height);
color: var(--sublink-color);
text-transform: var(--sublink-transform);
letter-spacing: var(--sublink-spacing);
&.current {
padding: var(--spacing-snug) var(--spacing-tight);
color: var(--title-current-fg);
animation: var(--kf-text-glitch) 5s infinite;
&::before {
content: "⟩";
}
}
&:not(.current) {
--pointer-left-symbol: "▶";
--pointer-right-symbol: "◀";
--pointer-distance: 1em;
@mixin anim-txt-pointer_focus;
@mixin px var(--spacing-tight);
&:focus {
color: var(--sublink-focus-fg);
outline: none;
}
&:not(:focus) {
--pointer-left-symbol: "{";
--pointer-right-symbol: "}";
--pointer-distance: 0.5em;
@mixin anim-txt-pointer;
&:hover {
--sublink-color: var(--color-primary);
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
import Link from 'next/link';
import styles from './MenuSublinks.module.css';
import { SubNavigationItem } from '@/lib/types/navigation';
import { useCurrentPath } from '@/hooks/useCurrentPath';
interface MenuSublinks {
sublinks: readonly SubNavigationItem[];
variant: string;
}
export default function MenuSublinks({ sublinks, variant }: MenuSublinks) {
const { isCurrentPath } = useCurrentPath();
return (
<ul className={`${styles.list} ${styles[`${variant}`]}`}>
{sublinks.map((sublink: SubNavigationItem) => (
<li className={styles.item} key={sublink.path}>
<Link
href={sublink.path}
className={`${styles.sublink} ${isCurrentPath(sublink.path) ? styles.current : ''}`}
>
{sublink.name}
</Link>
</li>
))}
</ul>
);
}

View File

@@ -0,0 +1,57 @@
@layer components {
.claim {
@media screen and (--bp-tablet-down) {
display: none;
}
}
.divider {
position: relative;
width: 100%;
text-align: center;
}
.dividersymbol {
position: relative;
font-family: var(--divider-font);
font-size: var(--divider-font-size);
line-height: var(--divider-line-height);
color: var(--divider-color);
transition: var(--subtitle-transition);
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
width: var(--divider-width);
height: var(--divider-height);
background-color: var(--divider-color);
}
&::before {
left: calc(var(--divider-width) *-1);
}
&::after {
right: calc(var(--divider-width) *-1);
}
}
.subtitle {
font-family: var(--subtitle-font);
font-size: var(--subtitle-font-size);
color: var(--subtitle-color);
text-transform: var(--subtitle-text-transform);
letter-spacing: var(--subtitle-letter-spacing);
transition: var(--subtitle-transition);
}
}

View File

@@ -0,0 +1,26 @@
import { Subtitle, hasDivider } from '@/lib/types/navigation';
import styles from './MenuSubtitle.module.css';
interface MenuSubtitleProps {
subtitle: Subtitle;
variant: string;
}
export default function MenuSubtitle({ subtitle, variant }: MenuSubtitleProps) {
const divider = hasDivider(subtitle) ? (
<div className={styles.divider}>
<span className={`${styles.dividersymbol}`}>
{subtitle.divider.value}
</span>
</div>
) : (
''
);
return (
<div className={`${styles.claim} ${styles[`${variant}`]}`}>
{divider}
<p className={`${styles.subtitle}`}>{subtitle.content}</p>
</div>
);
}

View File

@@ -0,0 +1,63 @@
@layer components {
.heading {
position: relative;
&:has(+ ul) {
@mixin my var(--spacing-comfortable);
}
}
.mainlink {
position: relative;
border: none;
font-family: var(--title-font);
font-size: var(--title-font-size);
font-weight: var(--title-font-weight);
line-height: var(--title-line-height);
color: var(--title-color);
text-transform: var(--title-transform);
letter-spacing: var(--title-spacing);
background: transparent;
transition: var(--title-transition);
&.current {
padding: var(--spacing-snug) var(--spacing-tight);
color: var(--title-current-fg);
animation: var(--kf-text-glitch) 5s infinite;
&::before {
content: "⟩";
}
}
&:not(.current) {
--pointer-left-symbol: "▶";
--pointer-right-symbol: "◀";
--pointer-distance: 1em;
@mixin anim-txt-pointer_focus;
@mixin px var(--spacing-tight);
&:focus {
color: var(--title-focus-fg);
outline: none;
}
&:not(:focus) {
--pointer-left-symbol: "{";
--pointer-right-symbol: "}";
--pointer-distance: 0.5em;
@mixin anim-txt-pointer;
&:hover {
--title-color: var(--color-primary);
}
}
}
}
}

View File

@@ -0,0 +1,25 @@
import Link from 'next/link';
import styles from './MenuTitle.module.css';
import { useCurrentPath } from '@/hooks/useCurrentPath';
interface MenuTitleProps {
name: string;
path: string;
variant: string;
}
export default function MenuTitle({ name, path, variant }: MenuTitleProps) {
const { isCurrentPath } = useCurrentPath();
return (
<h2 className={`${styles.heading} ${styles[`${variant}`]}`}>
<Link
className={`${styles.mainlink} ${isCurrentPath(path) ? styles.current : ''}`}
href={path}
>
{name}
</Link>
</h2>
);
}

View File

@@ -0,0 +1,7 @@
import { getNavigationData } from '@/lib/readers/system/navigation';
import MenuGrid from '@/components/Page/Menu/MenuGrid';
export default async function Menu() {
const navigation = await getNavigationData();
return <MenuGrid navigationData={navigation} />;
}

View File

@@ -0,0 +1,62 @@
'use client';
import React, { useContext, useEffect } from 'react';
interface MenuContextType {
isMenuOpen: boolean;
isClosing: boolean;
closeMenu: () => void;
openMenu: () => void;
startClosing: () => void;
resetClosing: () => void;
}
const MenuContext = React.createContext<MenuContextType | undefined>(undefined);
interface MenuProviderProps {
children: React.ReactNode;
}
export const MenuProvider = ({ children }: MenuProviderProps) => {
const [isMenuOpen, setIsMenuOpen] = React.useState(false);
const [isClosing, setIsClosing] = React.useState(false);
const closeMenu = () => setIsMenuOpen(false);
const openMenu = () => setIsMenuOpen(true);
const startClosing = () => setIsClosing(true);
const resetClosing = () => setIsClosing(false);
useEffect(() => {
if (isMenuOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
return () => {
document.body.style.overflow = 'unset';
};
}, [isMenuOpen]);
return (
<MenuContext.Provider
value={{
isMenuOpen,
openMenu,
closeMenu,
isClosing,
startClosing,
resetClosing,
}}
>
{children}
</MenuContext.Provider>
);
};
export const useMenu = () => {
const context = useContext(MenuContext);
if (context === undefined) {
throw new Error('useMenu must be used within a MenuProvider');
}
return context;
};

View File

@@ -0,0 +1,8 @@
import { usePathname } from 'next/navigation';
export const useCurrentPath = () => {
const pathname = usePathname();
const isCurrentPath = (path: string) => path === pathname;
return { isCurrentPath, pathname };
};

View File

@@ -0,0 +1,14 @@
import { collection } from '@keystatic/core';
import { createArticleField } from '@/keystatic/fields/article';
export default collection({
label: 'AWQ - Posts',
slugField: 'title',
path: 'content/awq/articles/*',
format: { contentField: 'content' },
entryLayout: 'content',
schema: {
...createArticleField('awq/articles', 'awq'),
},
});

View File

@@ -0,0 +1,7 @@
import posts from '@/keystatic/collections/awq/article';
const awqCollections = {
awq_posts: posts,
};
export default awqCollections;

View File

@@ -0,0 +1,14 @@
import { collection } from '@keystatic/core';
import { createArticleField } from '@/keystatic/fields/article';
export default collection({
label: 'Meta - Posts',
slugField: 'title',
path: 'content/meta/*',
format: { contentField: 'content' },
entryLayout: 'content',
schema: {
...createArticleField('meta'),
},
});

View File

@@ -0,0 +1,46 @@
import { collection, fields } from '@keystatic/core';
export default collection({
label: 'Authors',
slugField: 'name',
path: 'content/taxonomy/authors/*',
format: { data: 'json' },
schema: {
name: fields.slug({ name: { label: 'Name' } }),
avatar: fields.image({
label: 'Avatar',
directory: 'public/images/authors',
publicPath: '/images/authors',
}),
description: fields.text({ label: 'Description', multiline: true }),
contacts: fields.array(
fields.object({
type: fields.select({
label: 'Contact Type',
defaultValue: 'email',
options: [
{
value: 'email',
label: 'E-Mail',
},
{
value: 'discord',
label: 'Discord',
},
{
value: 'other',
label: 'Other',
},
],
}),
url: fields.text({
label: 'URL',
}),
}),
{
label: 'Contact Type',
itemLabel: (props) => `${props.fields.type.value}`,
}
),
},
});

View File

@@ -0,0 +1,41 @@
import { collection, fields } from '@keystatic/core';
export default collection({
label: 'Tags',
slugField: 'name',
path: 'content/taxonomy/tags/*',
format: { data: 'json' },
schema: {
name: fields.slug({ name: { label: 'Name' } }),
icon: fields.conditional(
fields.select({
label: 'Icon Type',
defaultValue: 'none',
options: [
{
value: 'none',
label: 'None',
},
{
value: 'glyph',
label: 'Glyph',
},
{
value: 'img',
label: 'Image',
},
],
}),
{
none: fields.empty(),
glyph: fields.text({ label: 'Glyph' }),
img: fields.image({
label: 'icon',
publicPath: '/images/icons',
directory: 'public/images/icons',
}),
}
),
description: fields.text({ label: 'Description', multiline: true }),
},
});

View File

@@ -0,0 +1,32 @@
import { listCollapseIcon } from '@keystar/ui/icon/icons/listCollapseIcon';
import { repeating, wrapper } from '@keystatic/core/content-components';
import { fields } from '@keystatic/core';
const accordionComponents = {
Accordion: repeating({
label: 'Accordion',
icon: listCollapseIcon,
children: ['AccordionItem'],
schema: {},
}),
AccordionItem: wrapper({
label: 'Accordion Item',
forSpecificLocations: true,
schema: {
title: fields.text({
label: 'Title',
validation: {
length: {
min: 1,
},
},
}),
defaultOpen: fields.checkbox({
label: 'Open by default',
defaultValue: false,
}),
},
}),
};
export default accordionComponents;

View File

@@ -0,0 +1,34 @@
import { textQuoteIcon } from '@keystar/ui/icon/icons/textQuoteIcon';
import { block } from '@keystatic/core/content-components';
import { fields } from '@keystatic/core';
const blockquoteComponent = {
Blockquote: block({
label: 'Blockquote',
icon: textQuoteIcon,
schema: {
quote: fields.text({
label: 'Quote',
multiline: true,
validation: {
length: {
min: 1,
},
},
}),
attribution: fields.text({
label: 'Attribution',
description: 'Author or Speaker',
}),
source: fields.text({
label: 'Source',
description: 'Book, article, or document [optional]',
}),
url: fields.url({
label: 'Source URL',
}),
},
}),
};
export default blockquoteComponent;

View File

@@ -0,0 +1,47 @@
import { wrapper } from '@keystatic/core/content-components';
import { fields } from '@keystatic/core';
import { megaphoneIcon } from '@keystar/ui/icon/icons/megaphoneIcon';
const calloutComponent = {
Callout: wrapper({
label: 'Callout',
icon: megaphoneIcon,
schema: {
type: fields.select({
label: 'Type',
defaultValue: 'default',
options: [
{
label: 'Default',
value: 'default',
},
{
label: 'Example',
value: 'example',
},
{
label: 'Info',
value: 'info',
},
{
label: 'Warning',
value: 'warning',
},
{
label: 'Tip',
value: 'tip',
},
{
label: 'Spoiler',
value: 'spoiler',
},
],
}),
title: fields.text({
label: 'Title',
}),
},
}),
};
export default calloutComponent;

View File

@@ -0,0 +1,28 @@
import { listTreeIcon } from '@keystar/ui/icon/icons/listTreeIcon';
import { fields } from '@keystatic/core';
import { repeating, block } from '@keystatic/core/content-components';
const definitionlistComponents = {
DefinitionList: repeating({
label: 'Definition List',
icon: listTreeIcon,
children: ['DefinitionItem'],
schema: {},
}),
DefinitionItem: block({
label: 'Definition Item',
forSpecificLocations: true,
schema: {
term: fields.text({ label: 'Term' }),
definitions: fields.array(
fields.text({ label: 'Definition', multiline: true }),
{
label: 'Definitions',
itemLabel: (props) => props.value || 'Definition',
}
),
},
}),
};
export default definitionlistComponents;

View File

@@ -0,0 +1,35 @@
import { imageIcon } from '@keystar/ui/icon/icons/imageIcon';
import { block } from '@keystatic/core/content-components';
import { fields } from '@keystatic/core';
const figureComponent = {
Figure: block({
label: 'Figure',
icon: imageIcon,
schema: {
src: fields.image({
label: 'Image',
directory: 'public/images/figures',
publicPath: '/images/figures',
}),
alt: fields.text({
label: 'Alt Text',
validation: {
length: {
min: 1,
},
},
}),
caption: fields.text({
label: 'Caption',
multiline: true,
}),
credit: fields.text({
label: 'Credit/Attribution',
description: 'Photographer, artist, or source',
}),
},
}),
};
export default figureComponent;

View File

@@ -0,0 +1,62 @@
import { grid2X2Icon } from '@keystar/ui/icon/icons/grid2X2Icon';
import { fields } from '@keystatic/core';
import { repeating, wrapper } from '@keystatic/core/content-components';
const gridComponents = {
Row: repeating({
label: 'Row',
icon: grid2X2Icon,
children: ['Column'], // This defines what can go inside
schema: {
style: fields.object({
cols: fields.integer({
label: 'Columns',
defaultValue: 2,
validation: { min: 2, max: 12 },
}),
gap: fields.text({
label: 'Gap',
description: 'Enter a variable or value',
}),
align: fields.select({
label: 'Align Items',
defaultValue: 'flex-start',
options: [
{ label: 'Start', value: 'flex-start' },
{ label: 'End', value: 'flex-end' },
{ label: 'Center', value: 'center' },
{ label: 'Baseline', value: 'baseline' },
{ label: 'Stretch', value: 'stretch' },
],
}),
justify: fields.select({
label: 'Justify Content',
defaultValue: 'flex-start',
options: [
{ label: 'Start', value: 'flex-start' },
{ label: 'End', value: 'flex-end' },
{ label: 'Center', value: 'center' },
{ label: 'Space Between', value: 'space-between' },
{ label: 'Space Around', value: 'space-around' },
{ label: 'Evenly', value: 'evenly' },
{ label: 'Stretch', value: 'stretch' },
{ label: 'Baseline', value: 'baseline' },
],
}),
}),
},
}),
Column: wrapper({
label: 'Column',
forSpecificLocations: true,
schema: {
style: fields.object({
colspan: fields.integer({
label: 'Colspan',
defaultValue: 1,
}),
}),
},
}),
};
export default gridComponents;

View File

@@ -0,0 +1,17 @@
import gridComponents from '@/keystatic/components/general/grid';
import sidenoteComponents from '@/keystatic/components/general/sidenote';
import definitionlistComponents from '@/keystatic/components/general/definitionlist';
import accordionComponents from '@/keystatic/components/general/accordion';
import blockquoteComponent from '@/keystatic/components/general/blockquote';
import figureComponent from '@/keystatic/components/general/figure';
import calloutComponent from '@/keystatic/components/general/callout';
export const generalComponents = {
...gridComponents,
...sidenoteComponents,
...definitionlistComponents,
...accordionComponents,
...blockquoteComponent,
...figureComponent,
...calloutComponent,
};

View File

@@ -0,0 +1,61 @@
import { panelRightDashedIcon } from '@keystar/ui/icon/icons/panelRightDashedIcon';
import { fields } from '@keystatic/core';
import { inline } from '@keystatic/core/content-components';
const sidenoteComponents = {
Sidenote: inline({
label: 'Sidenote',
icon: panelRightDashedIcon,
schema: {
id: fields.text({
label: 'ID',
description: 'Unique Identifier (Auto-generated)',
}),
marker: fields.text({
label: 'Marker',
description: 'Number or Glyph for reference',
defaultValue: '⋄',
validation: {
length: {
min: 1,
max: 3,
},
},
}),
content: fields.text({
label: 'Note Content',
multiline: true,
validation: {
length: {
min: 1,
},
},
}),
type: fields.select({
label: 'Type',
description: 'Visual Style',
options: [
{
label: 'Default',
value: 'default',
},
{
label: 'Lore',
value: 'lore',
},
{
label: 'Crunch',
value: 'crunch',
},
{
label: 'Example',
value: 'example',
},
],
defaultValue: 'default',
}),
},
}),
};
export default sidenoteComponents;

View File

@@ -0,0 +1,31 @@
import { fields } from '@keystatic/core';
import { createContentField } from '@/keystatic/fields/content';
import { createPathField } from '@/keystatic/fields/general';
import { createSEOField } from '@/keystatic/fields/seo';
import { createMetaField } from '@/keystatic/fields/meta';
export const createArticleField = (
imageSubfolder: string,
defaultPath: string = ''
) => ({
title: fields.slug({ name: { label: 'Title' } }),
summary: fields.text({ label: 'Summary', multiline: true }),
path: createPathField(defaultPath),
cover: fields.object({
src: fields.image({
label: 'Cover Image',
directory: `public/images/covers/${imageSubfolder}`,
publicPath: `/images/covers/${imageSubfolder}`,
}),
alt: fields.text({ label: 'Alt' }),
caption: fields.text({ label: 'Caption', multiline: true }),
showInHeader: fields.checkbox({
label: 'Show in Header',
defaultValue: false,
}),
}),
meta: createMetaField(),
seo: createSEOField(),
content: createContentField(imageSubfolder),
});

View File

@@ -0,0 +1,304 @@
import { type ComponentSchema, fields } from '@keystatic/core';
import { createOperatorField } from '@/keystatic/fields/general';
export const createCharacteristicReferenceField = (): ComponentSchema =>
fields.select({
label: 'Characteristic',
options: [
{ label: 'Weapon Skill', value: 'ws' },
{ label: 'Ballistic Skill', value: 'bs' },
{ label: 'Strength', value: 's' },
{ label: 'Toughness', value: 't' },
{ label: 'Initiative', value: 'i' },
{ label: 'Dexterity', value: 'dex' },
{ label: 'Agility', value: 'ag' },
{ label: 'Intelligence', value: 'int' },
{ label: 'Willpower', value: 'wp' },
{ label: 'Fellowship', value: 'fel' },
],
defaultValue: 'ws',
});
export const createSkillReferenceField = (): ComponentSchema =>
fields.select({
label: 'Skill',
options: [
{
label: 'Melee',
value: 'melee',
},
{
label: 'Defence',
value: 'defence',
},
{
label: 'Shooting',
value: 'shooting',
},
{
label: 'Throwing',
value: 'throwing',
},
{
label: 'Brawn',
value: 'brawn',
},
{
label: 'Toil',
value: 'toil',
},
{
label: 'Consume Alcohol',
value: 'consume-alcohol',
},
{
label: 'Endurance',
value: 'endurance',
},
{
label: 'Perception',
value: 'perception',
},
{
label: 'Dodge',
value: 'dodge',
},
{
label: 'Skullduggery',
value: 'skullduggery',
},
{
label: 'Tradecraft',
value: 'tradecraft',
},
{
label: 'Stealth',
value: 'stealth',
},
{
label: 'Athletics',
value: 'athletics',
},
{
label: 'Intuition',
value: 'intuition',
},
{
label: 'Education',
value: 'education',
},
{
label: 'Discipline',
value: 'discipline',
},
{
label: 'Animal Handling',
value: 'animal-handling',
},
{
label: 'Leadership',
value: 'leadership',
},
{
label: 'Charm',
value: 'charm',
},
],
defaultValue: 'melee',
});
export const createSizeReferenceField = (): ComponentSchema =>
fields.select({
label: 'Size',
options: [
{
label: 'Tiny',
value: 'tiny',
},
{
label: 'Little',
value: 'little',
},
{
label: 'Small',
value: 'small',
},
{
label: 'Average',
value: 'average',
},
{
label: 'Large',
value: 'large',
},
{
label: 'Enormous',
value: 'enormous',
},
{
label: 'Monstrous',
value: 'monstrous',
},
],
defaultValue: 'average',
});
export const createPsychologyReferenceField = (): ComponentSchema =>
fields.object({
type: fields.select({
label: 'Psychology Type',
defaultValue: 'animosity',
options: [
{ label: 'Animosity', value: 'animosity' },
{ label: 'Fear', value: 'fear' },
{ label: 'Frenzy', value: 'frenzy' },
{ label: 'Hatred', value: 'hatred' },
{ label: 'Prejudice', value: 'prejudice' },
{ label: 'Terror', value: 'terror' },
],
}),
target: fields.text({
label: 'Target',
description: 'e.g. Elves, Undead, Chaos',
}),
rating: fields.number({
label: 'Rating',
description: 'Fear/Terror Only',
}),
});
export const createRequirementsField = () =>
fields.blocks(
{
characteristic: {
label: 'Characteristic',
schema: fields.object({
characteristic: createCharacteristicReferenceField(),
operator: createOperatorField(),
value: fields.number({ label: 'Value', defaultValue: 3 }),
}),
},
skill: {
label: 'Skill',
schema: fields.object({
skill: createSkillReferenceField(),
operator: createOperatorField(),
value: fields.number({ label: 'Value', defaultValue: 3 }),
}),
},
status: {
label: 'Status',
schema: fields.multiselect({
label: 'Status Tier',
options: [
{
value: 'copper',
label: 'Copper',
},
{
value: 'silver',
label: 'Silver',
},
{
value: 'gold',
label: 'gold',
},
],
defaultValue: ['copper'],
}),
},
talent: {
label: 'Talent',
schema: fields.relationship({
label: 'Talent',
collection: 'awq_talents',
}),
},
lore: {
label: 'Lore',
schema: fields.relationship({
label: 'Lore',
collection: 'awq_lores',
}),
},
},
{ label: 'Requirements' }
);
export const createTraitsField = (): ComponentSchema =>
fields.array(
fields.object({
name: fields.text({
label: 'Trait name',
}),
description: fields.text({
label: 'Description',
multiline: true,
}),
grants: fields.blocks(
{
talent: {
label: 'Talent',
schema: fields.relationship({
label: 'Talent',
collection: 'awq_talents',
}),
},
talent_options: {
label: 'Talent Options',
schema: fields.object({
choices: fields.number({
label: 'Number to choose',
defaultValue: 1,
}),
options: fields.array(
fields.relationship({
label: 'Talent Options',
collection: 'awq_talents',
})
),
}),
},
talent_random: {
label: 'Random Talent',
schema: fields.object({
rolls: fields.number({
label: 'Rolls',
defaultValue: 1,
}),
table: fields.array(
fields.object({
roll: fields.object({
min: fields.number({ label: 'Min' }),
max: fields.number({ label: 'Max' }),
}),
talent: fields.relationship({
label: 'Talent',
collection: 'awq_talents',
}),
})
),
}),
},
psychology: {
label: 'Psychology',
schema: createPsychologyReferenceField(),
},
custom: {
label: 'Custom Effect',
schema: fields.text({
label: 'Custom Effect',
multiline: true,
}),
},
},
{
label: 'Trait Grants',
}
),
}),
{
label: 'Traits',
itemLabel: (props) => props.fields.name.value || 'Unnamed Trait',
}
);

Some files were not shown because too many files have changed in this diff Show More