diff --git a/app/globals.css b/app/globals.css index 875c01e..a5a2a84 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,32 +2,37 @@ @tailwind components; @tailwind utilities; -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --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; +@layer base { + * { + @apply text-white + } + body { + @apply bg-mixed-100 + } + input { + @apply py-2 px-4 rounded-full bg-mixed-200 placeholder:text-dark-500 + } + h1,h2,h3,h4,h5,h6 { + @apply font-bold + } + p { + @apply py-1 } } -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} +@layer components { + .strapline { + @apply text-primary-500 uppercase font-bold mb-2 text-lg + } -@layer utilities { - .text-balance { - text-wrap: balance; + .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 } } diff --git a/app/layout.tsx b/app/layout.tsx index 3314e47..cb9e8bd 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,12 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; +import { + BookOpenIcon, + CircleStackIcon, + Cog8ToothIcon, + PuzzlePieceIcon, +} from "@heroicons/react/24/solid"; const inter = Inter({ subsets: ["latin"] }); @@ -16,7 +22,30 @@ export default function RootLayout({ }>) { return ( - {children} + + +
+ {children} +
+ ); } diff --git a/app/page.tsx b/app/page.tsx index dc191aa..1987037 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,113 +1,149 @@ +import { TCMD } from "@/components/tcmd"; +import { + BookOpenIcon, + CircleStackIcon, + PuzzlePieceIcon, +} from "@heroicons/react/24/solid"; import Image from "next/image"; export default function Home() { return ( -
-
-

- Get started by editing  - app/page.tsx -

-
- - By{" "} - Vercel Logo - + <> +
+

Tabletop Commander

+

How does it work?

+
+
+
+

+ 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. +

+

+ Emma decided to move on from Chapter Master as her interest in 40k + was supplanted by the greater wargaming hobby after the release of + 10th edition made clear that Chapter Master was too inflexible and + tedious to work on. +

+

+ See, Emma had a vision that anyone could contribute to making rules + corrections so that anyone could have all of the rules as they + currently exist. This ballooned into the idea that you could have + all the rules as they existed at any time +

+
+
+
+
+
+ +

Game Systems

+
+

+ 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. +

+

But who owns a Game System?

+

+ 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. +

+

+ If your score is high enough, and a contribution request has + enough approvals, you can even be the one to merge it in! +

+
+ +
-
-
- Next.js Logo +
+ +

Schemas

+
+

+ 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 any game. +

+

+ If that flew over your head, don't worry. Others can share + the schemas they'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. +

+

For the techies:

+

+ 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. +

+
+ +
+
+ +
+
+ +

Publications

+
+

+ Publications are the actual content of the rules. They + don't just contain the content, but also the style in which + the content is shown. +

+

+ 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. +

+

For the techies (again):

+

+ 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. +

+

+ 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!* +

+
+ +
+
+ + * 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 + + +
+ -
- -
- -

- Docs{" "} - - -> - -

-

- Find in-depth information about Next.js features and API. -

-
- - -

- Learn{" "} - - -> - -

-

- Learn about Next.js in an interactive course with quizzes! -

-
- - -

- Templates{" "} - - -> - -

-

- Explore starter templates for Next.js. -

-
- - -

- Deploy{" "} - - -> - -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-
-
-
+ + ); } diff --git a/bun.lockb b/bun.lockb index 243a034..ac17f92 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/tcmd/index.tsx b/components/tcmd/index.tsx new file mode 100644 index 0000000..2680dfe --- /dev/null +++ b/components/tcmd/index.tsx @@ -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 ( + <> +
{JSON.stringify(elements,null,2)}
+ + ); +}; + +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; +}; diff --git a/lib/zip.ts b/lib/zip.ts new file mode 100644 index 0000000..3e09e27 --- /dev/null +++ b/lib/zip.ts @@ -0,0 +1,17 @@ +export function zipArrays(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; +} diff --git a/package.json b/package.json index 408e236..93df53d 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,10 @@ "lint": "next lint" }, "dependencies": { + "@heroicons/react": "^2.1.1", + "next": "14.1.0", "react": "^18", - "react-dom": "^18", - "next": "14.1.0" + "react-dom": "^18" }, "devDependencies": { "typescript": "^5", diff --git a/tailwind.config.ts b/tailwind.config.ts index 7e4bd91..98472cc 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -13,6 +13,32 @@ const config: Config = { "gradient-conic": "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: [],