Initial home page, work on tcmd parser

This commit is contained in:
Emmaline Autumn 2024-02-24 19:31:48 -07:00
parent f2080c751f
commit faad896f7e
8 changed files with 367 additions and 130 deletions

View File

@ -2,32 +2,37 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
:root { @layer base {
--foreground-rgb: 0, 0, 0; * {
--background-start-rgb: 214, 219, 220; @apply text-white
--background-end-rgb: 255, 255, 255;
} }
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body { body {
color: rgb(var(--foreground-rgb)); @apply bg-mixed-100
background: linear-gradient( }
to bottom, input {
transparent, @apply py-2 px-4 rounded-full bg-mixed-200 placeholder:text-dark-500
rgb(var(--background-end-rgb)) }
) h1,h2,h3,h4,h5,h6 {
rgb(var(--background-start-rgb)); @apply font-bold
}
p {
@apply py-1
}
} }
@layer utilities { @layer components {
.text-balance { .strapline {
text-wrap: balance; @apply text-primary-500 uppercase font-bold mb-2 text-lg
}
.card {
@apply bg-mixed-200 rounded-3xl p-6 shadow-2xl
}
.btn-primary {
@apply bg-primary-500 py-4 px-6 text-mixed-100 rounded-full font-bold text-lg
}
.btn-secondary {
@apply text-primary-500 py-4 px-6 font-bold text-lg
} }
} }

View File

@ -1,6 +1,12 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import "./globals.css"; import "./globals.css";
import {
BookOpenIcon,
CircleStackIcon,
Cog8ToothIcon,
PuzzlePieceIcon,
} from "@heroicons/react/24/solid";
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
@ -16,7 +22,30 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en">
<body className={inter.className}>{children}</body> <body className={inter.className + " flex min-h-[100vh]"}>
<nav className="h-[100vh] p-8 rounded-r-3xl bg-mixed-300 w-max shadow-2xl">
<h1 className="text-lg font-bold pb-6 border-b border-dark-500">
Tabletop Commander
</h1>
<ul className="my-6 flex flex-col gap-6">
<li className="flex items-center gap-2">
<CircleStackIcon className="w-6 h-6" />Schemas
</li>
<li className="flex items-center gap-2">
<PuzzlePieceIcon className="w-6 h-6" />Game Systems
</li>
<li className="flex items-center gap-2">
<BookOpenIcon className="w-6 h-6" />Publications
</li>
<li className="flex items-center gap-2">
<Cog8ToothIcon className="w-6 h-6" />Settings
</li>
</ul>
</nav>
<main className="p-8 w-full">
{children}
</main>
</body>
</html> </html>
); );
} }

View File

@ -1,113 +1,149 @@
import { TCMD } from "@/components/tcmd";
import {
BookOpenIcon,
CircleStackIcon,
PuzzlePieceIcon,
} from "@heroicons/react/24/solid";
import Image from "next/image"; import Image from "next/image";
export default function Home() { export default function Home() {
return ( return (
<main className="flex min-h-screen flex-col items-center justify-between p-24"> <>
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex"> <section className="pb-8 border-b border-b-dark-500 min-w-full">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30"> <h2 className="strapline">Tabletop Commander</h2>
Get started by editing&nbsp; <h1 className="text-5xl font-bold">How does it work?</h1>
<code className="font-mono font-bold">app/page.tsx</code> </section>
<section className="w-full my-6">
<div className="card">
<p>
Tabletop Commander (TC) is a rules-and-tools app for tabletop games
- board, card, war, role-playing, you name it! It is the spiritual
successor of Chapter Master by Emmaline Autumn, a Warhammer 40,000
9th Edition rules reference and game helper.
</p> </p>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none"> <p>
<a Emma decided to move on from Chapter Master as her interest in 40k
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0" was supplanted by the greater wargaming hobby after the release of
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app" 10th edition made clear that Chapter Master was too inflexible and
target="_blank" tedious to work on.
rel="noopener noreferrer" </p>
> <p>
By{" "} See, Emma had a vision that anyone could contribute to making rules
<Image corrections so that anyone could have all of the rules as they
src="/vercel.svg" currently exist. This ballooned into the idea that you could have
alt="Vercel Logo" all the rules as they existed at <em>any time</em>
className="dark:invert" </p>
width={100} </div>
height={24} </section>
priority <section className="mt-8 grid grid-cols-3 gap-x-8 gap-y-4">
<div>
<div className="card">
<PuzzlePieceIcon className="w-10 h-10 mb-2" />
<h3 className="text-lg mb-2">Game Systems</h3>
<div className="mb-8">
<p>
The basis of TC is called a Game System Package. This package
includes everything needed for a game system, including schemas,
publications, and tools. Players can follow a Game System to get
consistently updated content publications, or fork it to
maintain it themselves.
</p>
<h4>But who owns a Game System?</h4>
<p>
The neat part is that no one does! You can contribute to any
Game System with updates to publications and schemas through a
community review system. Those with the high enough scores
contribute more towards a total approval score which is used to
determine whether the Game System. The more your contributions
are approved, the higher your score becomes, the more weight
your approval and contributions carry.
</p>
<p>
If your score is high enough, and a contribution request has
enough approvals, you can even be the one to merge it in!
</p>
</div>
<button className="btn-secondary whitespace-nowrap">
Learn More
</button>
</div>
</div>
<div>
<div className="card">
<CircleStackIcon className="w-10 h-10 mb-2" />
<h3 className="text-lg mb-2">Schemas</h3>
<div className="mb-8">
<p>
Those who have studied English or databases, you would know that
a schema is a structural pattern. TC aims to provide a simple,
user-edited and maintained schema system for <em>any</em> game.
</p>
<p>
If that flew over your head, don&apos;t worry. Others can share
the schemas they&apos;ve made with everyone, which come as part
of a Game System package that you can fork or follow to get both
content and schemas ready to use.
</p>
<h4>For the techies:</h4>
<p>
The schema system makes use of a powerful custom query language
(tcQuery) I designed. By writing queries directly into the
schema, we can reduce the amount of re-written content, while
maintaining the presence of data anywhere we need it.
</p>
</div>
<button className="btn-secondary">Learn More</button>
</div>
</div>
<div>
<div className="card">
<BookOpenIcon className="w-10 h-10 mb-2" />
<h3 className="text-lg mb-2">Publications</h3>
<div className="mb-8">
<p>
Publications are the actual content of the rules. They
don&apos;t just contain the content, but also the style in which
the content is shown.
</p>
<p>
Content can include text, images, and even video (through
YouTube links or external embeds). Content can link to other
parts of the publication through context based pop-overs.
</p>
<h4>For the techies (again):</h4>
<p>
Publications use an enhanced markdown syntax (tcMD) that
implements tcQuery, and adds a bit of custom syntax for things
like pop-overs and styling hints for rendering.
</p>
<p>
The styling aspect is similar to a very trimmed down CSS, but
can accomplish quite a lot. For example, this page is actually
built using tcMD!*
</p>
</div>
<button className="btn-secondary">Learn More</button>
</div>
</div>
<cite className="col-span-3">
* not quite yet, this page is currently built with React, but soon it
will be done. If this makes it to production, tell Emma she forgot to
turn the home page into magic
</cite>
</section>
<section>
<TCMD
body={`
# spicy sandwiches
## taste good
### I like them
**A lot**
`}
/> />
</a> </section>
</div> </>
</div>
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-full sm:before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full sm:after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
<Image
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
</div>
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Docs{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Find in-depth information about Next.js features and API.
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Learn{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Learn about Next.js in an interactive course with&nbsp;quizzes!
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Templates{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Explore starter templates for Next.js.
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Deploy{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50 text-balance`}>
Instantly deploy your Next.js site to a shareable URL with Vercel.
</p>
</a>
</div>
</main>
); );
} }

BIN
bun.lockb

Binary file not shown.

123
components/tcmd/index.tsx Normal file
View File

@ -0,0 +1,123 @@
import { zipArrays } from "@/lib/zip";
import { FC, PropsWithChildren, useMemo } from "react";
export const TCMD: FC<{ body: string }> = ({ body }) => {
const elements = useMemo(() => createElements(body), [body]);
return (
<>
<pre>{JSON.stringify(elements,null,2)}</pre>
</>
);
};
const createElements = (body: string) => {
const tokens = tokenize(body);
return tokens;
};
type InlineToken = {
type: "text" | "bold";
content: string;
};
type Line = string | InlineToken[];
type MultilineToken = {
type: "code";
lines: Token[];
};
type Token = {
type: "h1" | "h2" | "h3" | "p";
line: Line;
};
const tokenize = (md: string) => {
const tokens: (Token | MultilineToken)[] = [];
md = md.replace(/(?<=[a-z])\n(?=[a-z])/g, " ");
const lines = md.split("\n");
const multilineFlags = {
heading: 0,
};
const tokenMatches = [
{
rx: /^\s*#\s/,
create: (line: Line) => tokens.push({ type: "h1", line }),
},
{
rx: /^\s*##\s/,
create: (line: Line) => tokens.push({ type: "h2", line }),
},
{
rx: /^\s*###\s/,
create: (line: Line) => tokens.push({ type: "h3", line }),
},
];
for (let line of lines) {
let foundLine = false;
token:
for (const token of tokenMatches) {
if (!token.rx.test(line)) continue token;
foundLine = true;
line = line.replace(token.rx, "").trim();
const lineContent = tokenizeInline(line);
token.create(lineContent);
}
if (foundLine) continue;
tokens.push({
type: "p",
line: tokenizeInline(line),
});
}
console.log(tokens);
return tokens.filter((t) => (t as Token).line || (t as MultilineToken).lines);
};
const tokenizeInline = (line: string) => {
line = line.trim();
const originalLine = line;
const insertMarker = "{^}";
const tokens: InlineToken[] = [];
const tokenMatches = [
{
rx: /\*\*(.*?)\*\*/g,
create: (content: string) =>
tokens.push({
content,
type: "bold",
}),
},
];
for (const token of tokenMatches) {
let match;
let last = 0;
while ((match = token.rx.exec(line)) !== null) {
const tokenStart = match.index;
const tokenEnd = match.index + match[0].length;
console.log(tokenEnd, token.rx.lastIndex);
token.create(line.substring(tokenStart, tokenEnd));
line = line.slice(last, tokenStart) + "{^}" +
line.slice(tokenEnd, line.length);
last = tokenEnd;
}
}
if (tokens.length) {
return zipArrays(
line.split(insertMarker).map((t): InlineToken => ({
content: t,
type: "text",
})),
tokens,
).filter((t) => t.content);
}
return originalLine;
};

17
lib/zip.ts Normal file
View File

@ -0,0 +1,17 @@
export function zipArrays<T, U>(array1: T[], array2: U[]): (T | U)[] {
const zippedArray: (T | U)[] = [];
const minLength = Math.min(array1.length, array2.length);
for (let i = 0; i < minLength; i++) {
zippedArray.push(array1[i], array2[i]);
}
// Append remaining elements of the longer array
if (array1.length > array2.length) {
zippedArray.push(...array1.slice(minLength));
} else if (array2.length > array1.length) {
zippedArray.push(...array2.slice(minLength));
}
return zippedArray;
}

View File

@ -9,9 +9,10 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@heroicons/react": "^2.1.1",
"next": "14.1.0",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18"
"next": "14.1.0"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5", "typescript": "^5",

View File

@ -13,6 +13,32 @@ const config: Config = {
"gradient-conic": "gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
}, },
colors: {
primary: {
"100": "#36005c",
"200": "#4d1f6d",
"300": "#633a7f",
"400": "#795491",
"500": "#8f6ea2",
"600": "#a58ab5",
},
dark: {
"100": "#121212",
"200": "#282828",
"300": "#3f3f3f",
"400": "#575757",
"500": "#717171",
"600": "#8b8b8b",
},
mixed: {
"100": "#1b1220",
"200": "#302735",
"300": "#463e4b",
"400": "#5e5762",
"500": "#77717a",
"600": "#918b93",
},
},
}, },
}, },
plugins: [], plugins: [],