Style updates
This commit is contained in:
parent
a9a979c5f8
commit
e7218143ec
18 changed files with 928 additions and 230 deletions
16
biome.json
16
biome.json
|
@ -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
2
spires.json
Normal file
File diff suppressed because one or more lines are too long
2
src/css.d.ts
vendored
Normal file
2
src/css.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
declare module '*.css' {
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 }} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
505
src/render/html/default.css
Normal 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;
|
||||
}
|
|
@ -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 = `
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue