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": { "files": {
"ignoreUnknown": false, "ignoreUnknown": false,
"ignore": ["*.d.ts", "*.json"], "ignore": [
"include": ["./src/**/*.ts"] "*.d.ts",
"*.json"
],
"include": [
"./src/**/*.ts",
"./src/**/*.tsx",
"./src/**/*.js",
"./src/**/*.jsx",
"./src/**/*.css"
]
}, },
"formatter": { "formatter": {
"enabled": true, "enabled": true,
@ -24,6 +33,9 @@
"recommended": true, "recommended": true,
"suspicious": { "suspicious": {
"noArrayIndexKey": "off" "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"), $define: z.literal("method"),
curator: z.string().default("Someone"), curator: z.string().default("Someone"),
abilities: z.array(z.array(z.string())).default([]), 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", [ const ResourceSchema = z.discriminatedUnion("type", [
@ -265,13 +274,40 @@ function parseItemDefinition(obj: unknown): ParsedComponent {
function parseMethodDefinition(obj: unknown): ParsedComponent { function parseMethodDefinition(obj: unknown): ParsedComponent {
const parsed = MethodSchema.parse(obj); 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 = { const method = {
id: parsed.id, id: parsed.id,
name: parsed.name, name: parsed.name,
description: parsed.description, description: parsed.description,
categories: parsed.categories, categories: parsed.categories,
curator: parsed.curator, curator: parsed.curator,
abilities: parsed.abilities, abilities: abilities,
}; };
return { return {

View file

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

View file

@ -9,31 +9,59 @@ export function AbilityCard({ ability }: AbilityCardProps) {
const costs: React.ReactNode[] = []; const costs: React.ReactNode[] = [];
if (ability.ap > 0) { 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) { 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) { 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 = const damage =
costs.length > 0 ability.damage !== null ? (
? <ul className="ability-costs">{costs}</ul> <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}`; const classList = `ability ability-${ability.type}`;
return <div className={classList} data-component-id={ability.id}> return (
<div className="ability-header"> <div className={classList} data-component-id={ability.id}>
<h4>{ability.name}</h4> <div className="ability-header">
<span className="ability-type">{ability.type}</span> <h4>{ability.name}</h4>
<span className="ability-cost-xp">{ability.xp} XP</span> <div className="ability-details">
{costList} {type}
{roll}
{damage}
{costs}
</div>
</div>
<HTML className="ability-content" html={ability.description} />
</div> </div>
<HTML className="ability-content" html={ability.description} /> );
</div>
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -9,25 +9,36 @@ interface MethodSectionProps {
export function MethodSection({ method, playbill }: MethodSectionProps) { export function MethodSection({ method, playbill }: MethodSectionProps) {
const ranks = method.abilities.map((rank, i) => { const ranks = method.abilities.map((rank, i) => {
return <div className="method-rank" key={i}> const gridTemplateColumns = new Array(Math.min(rank.length, 3))
<h3>Rank {i + 1}</h3> .fill("1fr")
<div className="method-rank-content"> .join(" ");
{rank.map((abilityId) => { return (
const ability = playbill.abilities[abilityId]; <div className="method-rank" key={i}>
return <AbilityCard ability={ability} key={abilityId} />; <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>
</div> );
}); });
return <Section return (
type="method" <Section
componentId={method.id} type="method"
title={method.name} componentId={method.id}
preInfo={<p className="method-curator">{method.curator}'s Method of</p>} title={method.name}
info={<p className="method-description" dangerouslySetInnerHTML={{ __html: method.description }}></p>} preInfo={<p className="method-curator">{method.curator}'s Method of</p>}
> info={
<div className="method-ranks">{ranks}</div> <p
</Section> 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) { function TextResourceSection({ resource }: TextResourceSectionProps) {
return <Section return (
type="resource" <Section
componentId={resource.id} type="resource"
title={resource.name} componentId={resource.id}
info={<ul className="resource-categories">{ title={resource.name}
resource.categories.map((category) => <li key={category}>{category}</li>) info={
}</ul>} <ul className="resource-categories">
><HTML html={resource.description} /></Section> {resource.categories.map((category) => (
<li key={category}>{category}</li>
))}
</ul>
}
>
<HTML html={resource.description} />
</Section>
);
} }
interface ImageResourceSectionProps { interface ImageResourceSectionProps {
@ -27,19 +35,27 @@ interface ImageResourceSectionProps {
} }
function ImageResourceSection({ resource }: ImageResourceSectionProps) { function ImageResourceSection({ resource }: ImageResourceSectionProps) {
return <Section return (
type="resource" <Section
componentId={resource.id} type="resource"
title={resource.name} componentId={resource.id}
info={<ul className="resource-categories">{ title={resource.name}
resource.categories.map((category) => <li key={category}>{category}</li>) info={
}</ul>} <ul className="resource-categories">
> {resource.categories.map((category) => (
<figure className="resource-image"> <li key={category}>{category}</li>
<img src={resource.url} alt={resource.name} /> ))}
<figcaption dangerouslySetInnerHTML={{ __html: resource.description }} /> </ul>
</figure> }
</Section> >
<figure className="resource-image">
<img src={resource.url} alt={resource.name} />
<figcaption
dangerouslySetInnerHTML={{ __html: resource.description }}
/>
</figure>
</Section>
);
} }
interface TableResourceSectionProps { interface TableResourceSectionProps {
@ -48,27 +64,39 @@ interface TableResourceSectionProps {
function TableResourceSection({ resource }: TableResourceSectionProps) { function TableResourceSection({ resource }: TableResourceSectionProps) {
const [headers, ...rows] = resource.data; const [headers, ...rows] = resource.data;
return <Section return (
type="resource" <Section
componentId={resource.id} type="resource"
title={resource.name} componentId={resource.id}
info={<ul className="resource-categories">{ title={resource.name}
resource.categories.map((category) => <li key={category}>{category}</li>) info={
}</ul>} <ul className="resource-categories">
> {resource.categories.map((category) => (
<table className="resource-table"> <li key={category}>{category}</li>
<thead> ))}
<tr> </ul>
{headers.map((header) => <th key={header} dangerouslySetInnerHTML={{ __html: header }} />)} }
</tr> >
</thead> <table className="resource-table">
<tbody> <thead>
{rows.map((row, i) => <tr key={i}>{ <tr>
row.map((cell, j) => <td key={j} dangerouslySetInnerHTML={{ __html: cell }} />) {headers.map((header) => (
}</tr>)} <th key={header} dangerouslySetInnerHTML={{ __html: header }} />
</tbody> ))}
</table> </tr>
</Section> </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 { interface ResourceSectionProps {

View file

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

View file

@ -15,77 +15,105 @@ export function SpeciesSection({ species, playbill }: SpeciesSectionProps) {
const hasAbilities = species.abilities.length > 0; const hasAbilities = species.abilities.length > 0;
const abilitySection = hasAbilities const abilitySection = hasAbilities ? (
? (<div className="species-abilities"> <div className="species-abilities">
<h3>Innate Abilities</h3> <h3>Innate Abilities</h3>
{species.abilities.map((abilityId) => { {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> </div>
<HTML className="species-content" html={species.description} /> ) : (
{abilitySection} ""
</> );
return <Section const sectionContent = (
type="species" <>
componentId={species.id} <div className="species-stats">
title={species.name} <div className="species-stat-hp">
info={<p>Species</p>} <h4 className="hp species-stat-hp-title" title="Health Points">
>{sectionContent}</Section> 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 { RuleSection } from "./component/rule";
import { SpeciesSection } from "./component/species"; import { SpeciesSection } from "./component/species";
import { renderToString } from "react-dom/server"; import { renderToString } from "react-dom/server";
import defaultCSS from "./default.css" with { type: "text" };
export async function renderPlaybillToHTML( export async function renderPlaybillToHTML(
boundPlaybill: BoundPlaybill, boundPlaybill: BoundPlaybill,
@ -22,7 +23,9 @@ export async function renderPlaybillToHTML(
// Define a processor function to iterate over all components and process their markdown // Define a processor function to iterate over all components and process their markdown
const processMarkdownInComponent = async (entry: TaggedComponent) => { 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") { if (entry.type === "resource" && entry.component.type === "table") {
const newData: string[][] = []; const newData: string[][] = [];
@ -35,7 +38,7 @@ export async function renderPlaybillToHTML(
} }
entry.component.data = newData; entry.component.data = newData;
} }
} };
// Process all components // Process all components
await playbill.processComponents(processMarkdownInComponent); await playbill.processComponents(processMarkdownInComponent);
@ -44,33 +47,59 @@ export async function renderPlaybillToHTML(
const rulesByOrder = sortBy(Object.values(playbill.rules), "order"); const rulesByOrder = sortBy(Object.values(playbill.rules), "order");
// Prepare stylesheet // Prepare stylesheet
const css = `<style>${boundPlaybill.styles}</style>`; const cssParts: string[] = [];
const body = renderToString(<> if (!boundPlaybill.omitDefaultStyles) {
<article id="species" className="view"> cssParts.push(`<style>${defaultCSS}</style>`);
{Object.values(playbill.species).map((species) => <SpeciesSection key={species.id} species={species} playbill={playbill} />)} }
</article>
<article id="methods" className="view" style={{ display: "none" }}> if (boundPlaybill.styles.length > 0) {
{Object.values(playbill.methods).map((method) => <MethodSection key={method.id} method={method} playbill={playbill} />)} cssParts.push(`<style>${boundPlaybill.styles}</style>`);
</article> }
<article id="items" className="view" style={{ display: "none" }}> const css = cssParts.join("\n");
{Object.values(playbill.items).map((item) => <ItemCard key={item.id} item={item} />)}
</article>
<article id="rules" className="view" style={{ display: "none" }}> const body = renderToString(
{rulesByOrder.map((rule) => <RuleSection key={rule.id} rule={rule} />)} <>
</article> <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" }}> <article id="methods" className="view" style={{ display: "none" }}>
{<GlossaryTable glossary={playbill.glossary} />} {Object.values(playbill.methods).map((method) => (
</article> <MethodSection key={method.id} method={method} playbill={playbill} />
))}
</article>
<article id="resources" className="view" style={{ display: "none" }}> <article id="items" className="view" style={{ display: "none" }}>
{Object.values(playbill.resources).map((resource) => <ResourceSection key={resource.id} resource={resource} />)} {Object.values(playbill.items).map((item) => (
</article> <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 // Main document
const doc = ` const doc = `

View file

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