Style updates

This commit is contained in:
Endeavorance 2025-03-21 12:20:51 -04:00
parent a9a979c5f8
commit e7218143ec
18 changed files with 928 additions and 230 deletions

View file

@ -7,8 +7,17 @@
},
"files": {
"ignoreUnknown": false,
"ignore": ["*.d.ts", "*.json"],
"include": ["./src/**/*.ts"]
"ignore": [
"*.d.ts",
"*.json"
],
"include": [
"./src/**/*.ts",
"./src/**/*.tsx",
"./src/**/*.js",
"./src/**/*.jsx",
"./src/**/*.css"
]
},
"formatter": {
"enabled": true,
@ -24,6 +33,9 @@
"recommended": true,
"suspicious": {
"noArrayIndexKey": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
}
}
},

2
spires.json Normal file

File diff suppressed because one or more lines are too long

2
src/css.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
declare module '*.css' {
}

View file

@ -86,6 +86,15 @@ const MethodSchema = Base.extend({
$define: z.literal("method"),
curator: z.string().default("Someone"),
abilities: z.array(z.array(z.string())).default([]),
rank1: z.array(z.string()).default([]),
rank2: z.array(z.string()).default([]),
rank3: z.array(z.string()).default([]),
rank4: z.array(z.string()).default([]),
rank5: z.array(z.string()).default([]),
rank6: z.array(z.string()).default([]),
rank7: z.array(z.string()).default([]),
rank8: z.array(z.string()).default([]),
rank9: z.array(z.string()).default([]),
});
const ResourceSchema = z.discriminatedUnion("type", [
@ -265,13 +274,40 @@ function parseItemDefinition(obj: unknown): ParsedComponent {
function parseMethodDefinition(obj: unknown): ParsedComponent {
const parsed = MethodSchema.parse(obj);
// Prefer `abilities` if defined, otherwise
// construct `abilities` using the rankX properties
const abilities = parsed.abilities;
if (abilities.length === 0) {
const allRanks = [
parsed.rank1,
parsed.rank2,
parsed.rank3,
parsed.rank4,
parsed.rank5,
parsed.rank6,
parsed.rank7,
parsed.rank8,
parsed.rank9,
];
// Add all ranks until an empty rank is found
for (const rank of allRanks) {
if (rank.length > 0) {
abilities.push(rank);
} else {
break;
}
}
}
const method = {
id: parsed.id,
name: parsed.name,
description: parsed.description,
categories: parsed.categories,
curator: parsed.curator,
abilities: parsed.abilities,
abilities: abilities,
};
return {

View file

@ -17,7 +17,7 @@ import { FileNotFoundError } from "./errors";
// binding.yaml file schema
const BindingSchema = z.object({
extend: z.string().optional(),
include: z.array(z.string()).default([]),
name: z.string().default("Unnamed playbill"),
author: z.string().default("Someone"),
@ -25,6 +25,7 @@ const BindingSchema = z.object({
version: z.string().default("0.0.0"),
files: z.array(z.string()).default(["**/*.yaml", "**/*.md"]),
omitDefaultStyles: z.boolean().default(false),
styles: z
.union([z.string(), z.array(z.string())])
.transform((val) => (Array.isArray(val) ? val : [val]))
@ -40,7 +41,7 @@ export interface BoundPlaybill {
// File information
bindingFilePath: string;
bindingFileDirname: string;
includedFiles: string[];
files: string[];
componentFiles: ComponentFile[];
// Binding properties
@ -49,6 +50,7 @@ export interface BoundPlaybill {
version: string;
description: string;
omitDefaultStyles: boolean;
styles: string;
playbill: Playbill;
@ -94,8 +96,8 @@ export async function loadFromBinding(
const playbill = new Playbill();
// If this is extending another binding, load that first
if (binding.extend) {
const pathFromHere = path.resolve(bindingFileDirname, binding.extend);
for (const includePath of binding.include) {
const pathFromHere = path.resolve(bindingFileDirname, includePath);
const extendBindingPath = await resolveBindingPath(pathFromHere);
const loadedBinding = await loadFromBinding(extendBindingPath);
playbill.extend(loadedBinding.playbill);
@ -239,7 +241,7 @@ export async function loadFromBinding(
_raw: binding,
bindingFilePath: bindingPath,
bindingFileDirname,
includedFiles: filePathsWithoutBindingFile,
files: filePathsWithoutBindingFile,
componentFiles,
name: binding.name,
@ -247,6 +249,7 @@ export async function loadFromBinding(
description: binding.description ?? "",
version: binding.version,
omitDefaultStyles: binding.omitDefaultStyles,
styles,
playbill,

View file

@ -9,31 +9,59 @@ export function AbilityCard({ ability }: AbilityCardProps) {
const costs: React.ReactNode[] = [];
if (ability.ap > 0) {
costs.push(<li key="ap" className="ability-cost-ap">{ability.ap} AP</li>);
costs.push(
<span key="ap" className="ability-cost ap">
{ability.ap} AP
</span>,
);
}
if (ability.hp > 0) {
costs.push(<li key="hp" className="ability-cost-hp">{ability.hp} HP</li>);
costs.push(
<span key="hp" className="ability-cost hp">
{ability.hp} HP
</span>,
);
}
if (ability.ep > 0) {
costs.push(<li key="ep" className="ability-cost-ep">{ability.ep} EP</li>);
costs.push(
<span key="ep" className="ability-cost ep">
{ability.ep} EP
</span>,
);
}
const costList =
costs.length > 0
? <ul className="ability-costs">{costs}</ul>
: "";
const damage =
ability.damage !== null ? (
<div className={`ability-damage ${ability.damage.type}`}>
{ability.damage.amount} {ability.damage.type === "phy" ? "Phy" : "Arc"}
</div>
) : null;
const roll =
ability.roll !== "none" ? (
<div className={`ability-roll ${ability.roll}`}>{ability.roll}</div>
) : null;
const type = (
<span className={`ability-type ${ability.type}`}>{ability.type}</span>
);
const classList = `ability ability-${ability.type}`;
return <div className={classList} data-component-id={ability.id}>
<div className="ability-header">
<h4>{ability.name}</h4>
<span className="ability-type">{ability.type}</span>
<span className="ability-cost-xp">{ability.xp} XP</span>
{costList}
return (
<div className={classList} data-component-id={ability.id}>
<div className="ability-header">
<h4>{ability.name}</h4>
<div className="ability-details">
{type}
{roll}
{damage}
{costs}
</div>
</div>
<HTML className="ability-content" html={ability.description} />
</div>
<HTML className="ability-content" html={ability.description} />
</div>
);
}

View file

@ -4,5 +4,7 @@ interface HTMLWrapperProps {
}
export function HTML({ html, className }: HTMLWrapperProps) {
return <div className={className} dangerouslySetInnerHTML={{ __html: html }} />;
return (
<div className={className} dangerouslySetInnerHTML={{ __html: html }} />
);
}

View file

@ -1,5 +1,5 @@
import classNames from "classnames";
import React from "react";
import type React from "react";
interface SectionProps {
type: string;
@ -26,17 +26,16 @@ export function Section({
const headerClasses = classNames("section-header", `${type}-header`);
const contentClasses = classNames("section-content", `${type}-content`);
return <section className={sectionClasses} data-component-id={componentId}>
<div className={headerClasses}>
{preInfo}
<h2>{title}</h2>
{info}
</div>
<div className="section-tag section-tag--left">{leftCornerTag}</div>
<div className="section-tag section-tag--right">{rightCornerTag}</div>
<div className={contentClasses}>
{children}
</div>
</section>
return (
<section className={sectionClasses} data-component-id={componentId}>
<div className={headerClasses}>
{preInfo}
<h2>{title}</h2>
{info}
</div>
<div className="section-tag section-tag--left">{leftCornerTag}</div>
<div className="section-tag section-tag--right">{rightCornerTag}</div>
<div className={contentClasses}>{children}</div>
</section>
);
}

View file

@ -4,20 +4,23 @@ interface GlossaryTableProps {
glossary: Record<string, string[]>;
}
export function GlossaryTable(
{ glossary }: GlossaryTableProps,
) {
return <Section type="glossary" componentId="glossary" title="Glossary">
<div className="glossary-content">
<dl>
{Object.entries(glossary).map(([term, defs]) => {
return <div key={term}>
<dt>{term}</dt>
{defs.map((d, i) => <dd key={i}>{d}</dd>)}
</div>
})}
</dl>
</div>
</Section>
export function GlossaryTable({ glossary }: GlossaryTableProps) {
return (
<Section type="glossary" componentId="glossary" title="Glossary">
<div className="glossary-content">
<dl>
{Object.entries(glossary).map(([term, defs]) => {
return (
<div key={term}>
<dt>{term}</dt>
{defs.map((d, i) => (
<dd key={i}>{d}</dd>
))}
</div>
);
})}
</dl>
</div>
</Section>
);
}

View file

@ -5,9 +5,14 @@ interface PlaybillHeaderProps {
}
export function PageHeader({ playbill }: PlaybillHeaderProps) {
return <header>
<h1 className="header-title">{playbill.name}</h1>
<p className="header-byline">A Playbill by {playbill.author} </p>
<p> <small className="header-info"> Version {playbill.version}</small></p>
</header>
return (
<header>
<h1 className="header-title">{playbill.name}</h1>
<p className="header-byline">A Playbill by {playbill.author} </p>
<p>
{" "}
<small className="header-info"> Version {playbill.version}</small>
</p>
</header>
);
}

View file

@ -28,7 +28,7 @@ export function ItemCard({ item }: ItemSectionProps) {
infoParts.push(
<p key="affinity" className={`item-affinity ${item.affinity}`}>
{item.affinity} Affinity
</p>
</p>,
);
}
@ -36,18 +36,20 @@ export function ItemCard({ item }: ItemSectionProps) {
infoParts.push(
<p key="damage" className={`item-damage ${item.damage.type}`}>
{item.damage.amount} {capitalize(item.damage.type)} Damage
</p>
</p>,
);
}
return <Section
type="item"
componentId={item.id}
title={item.name}
info={<div className="item-info">{infoParts}</div>}
rightCornerTag={capitalize(item.rarity)}
leftCornerTag={itemTypeDescriptor}
>
<HTML html={item.description} />
</Section>
return (
<Section
type="item"
componentId={item.id}
title={item.name}
info={<div className="item-info">{infoParts}</div>}
rightCornerTag={capitalize(item.rarity)}
leftCornerTag={itemTypeDescriptor}
>
<HTML html={item.description} />
</Section>
);
}

View file

@ -9,25 +9,36 @@ interface MethodSectionProps {
export function MethodSection({ method, playbill }: MethodSectionProps) {
const ranks = method.abilities.map((rank, i) => {
return <div className="method-rank" key={i}>
<h3>Rank {i + 1}</h3>
<div className="method-rank-content">
{rank.map((abilityId) => {
const ability = playbill.abilities[abilityId];
return <AbilityCard ability={ability} key={abilityId} />;
})
}
const gridTemplateColumns = new Array(Math.min(rank.length, 3))
.fill("1fr")
.join(" ");
return (
<div className="method-rank" key={i}>
<h3>Rank {i + 1}</h3>
<div className="method-rank-content" style={{ gridTemplateColumns }}>
{rank.map((abilityId) => {
const ability = playbill.abilities[abilityId];
return <AbilityCard ability={ability} key={abilityId} />;
})}
</div>
</div>
</div>
);
});
return <Section
type="method"
componentId={method.id}
title={method.name}
preInfo={<p className="method-curator">{method.curator}'s Method of</p>}
info={<p className="method-description" dangerouslySetInnerHTML={{ __html: method.description }}></p>}
>
<div className="method-ranks">{ranks}</div>
</Section>
return (
<Section
type="method"
componentId={method.id}
title={method.name}
preInfo={<p className="method-curator">{method.curator}'s Method of</p>}
info={
<p
className="method-description"
dangerouslySetInnerHTML={{ __html: method.description }}
/>
}
>
<div className="method-ranks">{ranks}</div>
</Section>
);
}

View file

@ -12,14 +12,22 @@ interface TextResourceSectionProps {
}
function TextResourceSection({ resource }: TextResourceSectionProps) {
return <Section
type="resource"
componentId={resource.id}
title={resource.name}
info={<ul className="resource-categories">{
resource.categories.map((category) => <li key={category}>{category}</li>)
}</ul>}
><HTML html={resource.description} /></Section>
return (
<Section
type="resource"
componentId={resource.id}
title={resource.name}
info={
<ul className="resource-categories">
{resource.categories.map((category) => (
<li key={category}>{category}</li>
))}
</ul>
}
>
<HTML html={resource.description} />
</Section>
);
}
interface ImageResourceSectionProps {
@ -27,19 +35,27 @@ interface ImageResourceSectionProps {
}
function ImageResourceSection({ resource }: ImageResourceSectionProps) {
return <Section
type="resource"
componentId={resource.id}
title={resource.name}
info={<ul className="resource-categories">{
resource.categories.map((category) => <li key={category}>{category}</li>)
}</ul>}
>
<figure className="resource-image">
<img src={resource.url} alt={resource.name} />
<figcaption dangerouslySetInnerHTML={{ __html: resource.description }} />
</figure>
</Section>
return (
<Section
type="resource"
componentId={resource.id}
title={resource.name}
info={
<ul className="resource-categories">
{resource.categories.map((category) => (
<li key={category}>{category}</li>
))}
</ul>
}
>
<figure className="resource-image">
<img src={resource.url} alt={resource.name} />
<figcaption
dangerouslySetInnerHTML={{ __html: resource.description }}
/>
</figure>
</Section>
);
}
interface TableResourceSectionProps {
@ -48,27 +64,39 @@ interface TableResourceSectionProps {
function TableResourceSection({ resource }: TableResourceSectionProps) {
const [headers, ...rows] = resource.data;
return <Section
type="resource"
componentId={resource.id}
title={resource.name}
info={<ul className="resource-categories">{
resource.categories.map((category) => <li key={category}>{category}</li>)
}</ul>}
>
<table className="resource-table">
<thead>
<tr>
{headers.map((header) => <th key={header} dangerouslySetInnerHTML={{ __html: header }} />)}
</tr>
</thead>
<tbody>
{rows.map((row, i) => <tr key={i}>{
row.map((cell, j) => <td key={j} dangerouslySetInnerHTML={{ __html: cell }} />)
}</tr>)}
</tbody>
</table>
</Section>
return (
<Section
type="resource"
componentId={resource.id}
title={resource.name}
info={
<ul className="resource-categories">
{resource.categories.map((category) => (
<li key={category}>{category}</li>
))}
</ul>
}
>
<table className="resource-table">
<thead>
<tr>
{headers.map((header) => (
<th key={header} dangerouslySetInnerHTML={{ __html: header }} />
))}
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i}>
{row.map((cell, j) => (
<td key={j} dangerouslySetInnerHTML={{ __html: cell }} />
))}
</tr>
))}
</tbody>
</table>
</Section>
);
}
interface ResourceSectionProps {

View file

@ -7,11 +7,9 @@ interface RuleSectionProps {
}
export function RuleSection({ rule }: RuleSectionProps) {
return <Section
title={rule.name}
componentId={rule.id}
type="rule"
>
<HTML html={rule.description} />
</Section>
return (
<Section title={rule.name} componentId={rule.id} type="rule">
<HTML html={rule.description} />
</Section>
);
}

View file

@ -15,77 +15,105 @@ export function SpeciesSection({ species, playbill }: SpeciesSectionProps) {
const hasAbilities = species.abilities.length > 0;
const abilitySection = hasAbilities
? (<div className="species-abilities">
const abilitySection = hasAbilities ? (
<div className="species-abilities">
<h3>Innate Abilities</h3>
{species.abilities.map((abilityId) => {
return <AbilityCard ability={playbill.abilities[abilityId]} key={abilityId} />;
return (
<AbilityCard
ability={playbill.abilities[abilityId]}
key={abilityId}
/>
);
})}
</div>)
: "";
const sectionContent = <>
<div className="species-stats">
<div className="species-stat-hp">
<h4 className="hp species-stat-hp-title" title="Health Points">HP</h4>
<p className="species-stat-hp-value">{hpString}</p>
</div>
<div className="species-stat-ap">
<h4 className="ap species-stat-ap-title" title="Action Points">AP</h4>
<p className="species-stat-ap-value">{apString}</p>
</div>
<div className="species-stat-ep">
<h4 className="ep species-stat-ep-title" title="Exhaustion Points">EP</h4>
<p className="species-stat-ep-value">{epString}</p>
</div>
</div >
<div className="species-talents">
<div className="species-talent-muscle">
<h4 className="species-talent-muscle-title muscle">Muscle</h4>
<p className={`species-talent-muscle-value ${species.muscle}`}>{species.muscle}</p>
</div>
<div className="species-talent-focus">
<h4 className="species-talent-focus-title focus">Focus</h4>
<p className={`species-talent-focus-value ${species.focus}`}>{species.focus}</p>
</div>
<div className="species-talent-knowledge">
<h4 className="species-talent-knowledge-title knowledge">Knowledge</h4>
<p className={`species-talent-knowledge-value ${species.knowledge}`}>{species.knowledge}</p>
</div>
<div className="species-talent-charm">
<h4 className="species-talent-charm-title charm">Charm</h4>
<p className={`species-talent-charm-value ${species.charm}`}>{species.charm}</p>
</div>
<div className="species-talent-cunning">
<h4 className="species-talent-cunning-title cunning">Cunning</h4>
<p className={`species-talent-cunning-value ${species.cunning}`}>{species.cunning}</p>
</div>
<div className="species-talent-spark">
<h4 className="species-talent-spark-title spark">Spark</h4>
<p className={`species-talent-spark-value ${species.spark}`}>{species.spark}</p>
</div>
</div>
<HTML className="species-content" html={species.description} />
{abilitySection}
</>
) : (
""
);
return <Section
type="species"
componentId={species.id}
title={species.name}
info={<p>Species</p>}
>{sectionContent}</Section>
const sectionContent = (
<>
<div className="species-stats">
<div className="species-stat-hp">
<h4 className="hp species-stat-hp-title" title="Health Points">
HP
</h4>
<p className="species-stat-hp-value">{hpString}</p>
</div>
<div className="species-stat-ap">
<h4 className="ap species-stat-ap-title" title="Action Points">
AP
</h4>
<p className="species-stat-ap-value">{apString}</p>
</div>
<div className="species-stat-ep">
<h4 className="ep species-stat-ep-title" title="Exhaustion Points">
EP
</h4>
<p className="species-stat-ep-value">{epString}</p>
</div>
</div>
<div className="species-talents">
<div className="species-talent-muscle">
<h4 className="species-talent-muscle-title muscle">Muscle</h4>
<p className={`species-talent-muscle-value ${species.muscle}`}>
{species.muscle}
</p>
</div>
<div className="species-talent-focus">
<h4 className="species-talent-focus-title focus">Focus</h4>
<p className={`species-talent-focus-value ${species.focus}`}>
{species.focus}
</p>
</div>
<div className="species-talent-knowledge">
<h4 className="species-talent-knowledge-title knowledge">
Knowledge
</h4>
<p className={`species-talent-knowledge-value ${species.knowledge}`}>
{species.knowledge}
</p>
</div>
<div className="species-talent-charm">
<h4 className="species-talent-charm-title charm">Charm</h4>
<p className={`species-talent-charm-value ${species.charm}`}>
{species.charm}
</p>
</div>
<div className="species-talent-cunning">
<h4 className="species-talent-cunning-title cunning">Cunning</h4>
<p className={`species-talent-cunning-value ${species.cunning}`}>
{species.cunning}
</p>
</div>
<div className="species-talent-spark">
<h4 className="species-talent-spark-title spark">Spark</h4>
<p className={`species-talent-spark-value ${species.spark}`}>
{species.spark}
</p>
</div>
</div>
<HTML className="species-content" html={species.description} />
{abilitySection}
</>
);
return (
<Section
type="species"
componentId={species.id}
title={species.name}
info={<p>Species</p>}
>
{sectionContent}
</Section>
);
}

505
src/render/html/default.css Normal file
View file

@ -0,0 +1,505 @@
@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Urbanist:ital,wght@0,100..900;1,100..900&display=swap");
:root {
/* Spacing and sizing */
--max-width: 800px;
--padding: 0.5rem;
--margin: 1rem;
/* Typography */
--line-height: 1.5;
--font-size: 18px;
--font-body: "Open Sans", sans-serif;
--font-header: "Urbanist", sans-serif;
/* Page Colors */
--color-background: hsl(34deg 18 15);
--color-background--dark: hsl(34deg 18 10);
--color-background--light: hsl(34deg 18 20);
--color-text: hsl(35deg 37 73);
--color-text--dim: hsl(35deg 17 50);
--color-heading: hsl(32deg 48 85);
--color-emphasis: hsl(56deg 59 56);
--color-strong: hsl(4deg 88 61);
/* Concept Colors */
--color-muscle: var(--color-heading);
--color-focus: var(--color-heading);
--color-knowledge: var(--color-heading);
--color-charm: var(--color-heading);
--color-cunning: var(--color-heading);
--color-spark: var(--color-heading);
--color-novice: gray;
--color-adept: pink;
--color-master: papayawhip;
--color-theatrical: red;
--color-phy: red;
--color-arc: hsl(56deg 59 56);
--color-ap: hsl(215deg 91 75);
--color-hp: hsl(15deg 91 75);
--color-ep: hsl(115deg 40 75);
--color-xp: hsl(0deg 0 45);
}
/* Normalize box sizing */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Strip default margins */
* {
margin: 0;
}
/* Global style classes */
.phy {
color: var(--color-phy);
}
.arc {
color: var(--color-arc);
}
.ap {
color: var(--color-ap);
}
.hp {
color: var(--color-hp);
}
.ep {
color: var(--color-ep);
}
.muscle {
color: var(--color-muscle);
}
.focus {
color: var(--color-focus);
}
.knowledge {
color: var(--color-knowledge);
}
.charm {
color: var(--color-charm);
}
.cunning {
color: var(--color-cunning);
}
.spark {
color: var(--color-spark);
}
.novice {
color: var(--color-novice);
}
.adept {
color: var(--color-adept);
}
.master {
color: var(--color-master);
}
.theatrical {
color: var(--color-theatrical);
}
/* Sections */
section {
position: relative;
max-width: var(--max-width);
margin: 0 auto var(--margin);
border: 2px solid var(--color-background--dark);
}
.section-header {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 8rem;
text-align: center;
background-color: var(--color-background--dark);
}
.section-header p {
margin: 0;
}
.section-content {
padding: var(--padding);
}
.section-content :last-child {
margin-bottom: 0;
}
.section-tag {
position: absolute;
top: var(--padding);
color: var(--color-text--dim);
font-weight: bold;
max-width: 45%;
}
.section-tag--left {
left: var(--padding);
}
.section-tag--right {
right: var(--padding);
}
body {
background-color: var(--color-background);
color: var(--color-text);
font-family: var(--font-body);
font-size: var(--font-size);
}
p {
margin-bottom: var(--margin);
line-height: var(--line-height);
}
em {
color: var(--color-emphasis);
}
strong {
color: var(--color-strong);
}
/* Basic Typography */
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-header);
color: var(--color-heading);
}
h1 {
font-size: 4rem;
text-align: center;
}
h2 {
font-size: 2rem;
}
h3 {
font-size: 1.75rem;
}
h4 {
font-size: 1.5rem;
}
h5 {
font-size: 1.25rem;
}
h6 {
font-weight: bold;
font-size: 1rem;
}
table {
width: 100%;
margin-bottom: var(--margin);
}
thead {
background-color: var(--color-background--light);
}
td {
padding: var(--padding);
text-align: left;
vertical-align: top;
}
tr:nth-child(even) {
background-color: var(--color-background--light);
}
tr:hover {
background-color: var(--color-background--dark);
}
article blockquote {
font-style: italic;
border-left: 3px solid var(--color-emphasis);
background-color: var(--color-background--light);
margin-bottom: var(--margin);
padding: 0.5rem 0.5rem 0.5rem 2rem;
}
article blockquote p:last-child {
margin-bottom: 0;
}
article img {
display: block;
margin: 0 auto;
max-width: 70%;
max-height: 35vh;
}
@media screen and (min-width: 480px) {
article img {
float: right;
margin-left: var(--margin);
margin-right: 0;
max-width: 45%;
}
article img.left {
float: left;
float: left;
margin-right: var(--margin);
}
}
/* Main Header */
header {
min-height: 20vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.header-byline {
font-size: 1.2rem;
}
/* Overview */
.overview {
padding: 1rem;
background-color: var(--color-background--dark);
text-align: center;
}
.overview p {
margin: 0 auto;
}
/* Nav */
nav {
max-width: var(--max-width);
padding: var(--padding);
margin: 0 auto;
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--padding);
}
nav button {
padding: var(--padding);
font-size: inherit;
color: inherit;
background-color: var(--color-background--light);
border: 1px solid var(--color-background--dark);
cursor: pointer;
flex: 1;
}
nav button:hover {
background-color: var(--color-background--dark);
}
/* Ability */
.ability {
padding: var(--padding);
background-color: var(--color-background--light);
}
.ability-details {
display: flex;
flex-direction: row;
gap: var(--padding);
flex-wrap: wrap;
text-transform: capitalize;
font-weight: 300;
font-style: italic;
}
.ability-header {
position: relative;
margin-bottom: var(--margin);
}
/* Method */
.method-rank {
margin-bottom: var(--margin);
overflow-x: auto;
}
.method-curator {
font-style: italic;
}
.method-description {
margin-top: var(--margin);
}
.method-rank-content {
display: grid;
gap: var(--padding);
}
.method-rank-content .ability {
width: 100%;
height: 100%;
}
/* Species */
.species {
margin-bottom: var(--margin);
}
.species-stats {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
text-align: center;
gap: var(--padding);
}
.species-talents .adept {
font-weight: 600;
}
.species-talents .master {
font-weight: 700;
}
.species-stats h4,
.species-talents h4 {
font-size: var(--font-size);
}
.species-talents {
text-transform: capitalize;
}
.species-talents {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: var(--padding);
text-align: center;
margin-bottom: var(--margin);
}
.species-abilities .ability-cost-xp {
display: none;
}
/* Items */
.item {
position: relative;
background-color: var(--color-background--light);
}
.item-info {
display: flex;
flex-direction: row;
gap: var(--padding);
}
.item-affinity {
text-transform: capitalize;
}
.item-rarity {
position: absolute;
top: var(--padding);
right: var(--padding);
text-transform: capitalize;
}
/* Glossary */
.glossary-content > dl > div {
margin-bottom: var(--margin);
}
dt {
color: var(--color-strong);
font-weight: bold;
}
dd {
margin-left: 2ch;
}
dd + dd {
margin-top: var(--padding);
}
dd + dt {
margin-top: var(--margin);
}
/* Resources */
.resource-categories {
list-style: none;
padding: 0;
display: flex;
flex-direction: row;
text-transform: capitalize;
gap: var(--padding);
color: var(--color-text--dim);
}
.resource-categories li {
font-style: italic;
}
.resource-categories li:before {
content: "#";
opacity: 0.5;
}
.resource-image {
display: block;
}
.resource-image img {
float: none;
margin: 0 auto;
max-width: 90%;
max-height: 100vh;
margin-bottom: var(--padding);
}
.resource-image figcaption {
font-style: italic;
text-align: center;
}

View file

@ -9,6 +9,7 @@ import { ResourceSection } from "./component/resource";
import { RuleSection } from "./component/rule";
import { SpeciesSection } from "./component/species";
import { renderToString } from "react-dom/server";
import defaultCSS from "./default.css" with { type: "text" };
export async function renderPlaybillToHTML(
boundPlaybill: BoundPlaybill,
@ -22,7 +23,9 @@ export async function renderPlaybillToHTML(
// Define a processor function to iterate over all components and process their markdown
const processMarkdownInComponent = async (entry: TaggedComponent) => {
entry.component.description = await renderMarkdown(entry.component.description);
entry.component.description = await renderMarkdown(
entry.component.description,
);
if (entry.type === "resource" && entry.component.type === "table") {
const newData: string[][] = [];
@ -35,7 +38,7 @@ export async function renderPlaybillToHTML(
}
entry.component.data = newData;
}
}
};
// Process all components
await playbill.processComponents(processMarkdownInComponent);
@ -44,33 +47,59 @@ export async function renderPlaybillToHTML(
const rulesByOrder = sortBy(Object.values(playbill.rules), "order");
// Prepare stylesheet
const css = `<style>${boundPlaybill.styles}</style>`;
const cssParts: string[] = [];
const body = renderToString(<>
<article id="species" className="view">
{Object.values(playbill.species).map((species) => <SpeciesSection key={species.id} species={species} playbill={playbill} />)}
</article>
if (!boundPlaybill.omitDefaultStyles) {
cssParts.push(`<style>${defaultCSS}</style>`);
}
<article id="methods" className="view" style={{ display: "none" }}>
{Object.values(playbill.methods).map((method) => <MethodSection key={method.id} method={method} playbill={playbill} />)}
</article>
if (boundPlaybill.styles.length > 0) {
cssParts.push(`<style>${boundPlaybill.styles}</style>`);
}
<article id="items" className="view" style={{ display: "none" }}>
{Object.values(playbill.items).map((item) => <ItemCard key={item.id} item={item} />)}
</article>
const css = cssParts.join("\n");
<article id="rules" className="view" style={{ display: "none" }}>
{rulesByOrder.map((rule) => <RuleSection key={rule.id} rule={rule} />)}
</article>
const body = renderToString(
<>
<article id="species" className="view">
{Object.values(playbill.species).map((species) => (
<SpeciesSection
key={species.id}
species={species}
playbill={playbill}
/>
))}
</article>
<article id="glossary" className="view" style={{ display: "none" }}>
{<GlossaryTable glossary={playbill.glossary} />}
</article>
<article id="methods" className="view" style={{ display: "none" }}>
{Object.values(playbill.methods).map((method) => (
<MethodSection key={method.id} method={method} playbill={playbill} />
))}
</article>
<article id="resources" className="view" style={{ display: "none" }}>
{Object.values(playbill.resources).map((resource) => <ResourceSection key={resource.id} resource={resource} />)}
</article>
</>);
<article id="items" className="view" style={{ display: "none" }}>
{Object.values(playbill.items).map((item) => (
<ItemCard key={item.id} item={item} />
))}
</article>
<article id="rules" className="view" style={{ display: "none" }}>
{rulesByOrder.map((rule) => (
<RuleSection key={rule.id} rule={rule} />
))}
</article>
<article id="glossary" className="view" style={{ display: "none" }}>
{<GlossaryTable glossary={playbill.glossary} />}
</article>
<article id="resources" className="view" style={{ display: "none" }}>
{Object.values(playbill.resources).map((resource) => (
<ResourceSection key={resource.id} resource={resource} />
))}
</article>
</>,
);
// Main document
const doc = `

View file

@ -12,7 +12,7 @@
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"allowArbitraryExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
@ -35,5 +35,10 @@
"./util/index.ts"
],
}
}
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.css",
]
}