From 478b32199f9b07fa720b83563df8f0d54b4c8642 Mon Sep 17 00:00:00 2001 From: Will Zakielarz Date: Wed, 8 Apr 2026 15:27:51 -0400 Subject: [PATCH 1/4] Logo + favicon --- frontend/index.html | 8 +-- frontend/public/cardgoose-favicon.svg | 7 +++ frontend/src/App.css | 22 ++++++- frontend/src/assets/cardgoose-mark.svg | 9 +++ frontend/src/assets/cardgoose-square.svg | 8 +++ frontend/src/components/BrandLogo.tsx | 74 +++++++++++++++++++++--- frontend/src/components/StudioAppBar.tsx | 12 ++-- 7 files changed, 120 insertions(+), 20 deletions(-) create mode 100644 frontend/public/cardgoose-favicon.svg create mode 100644 frontend/src/assets/cardgoose-mark.svg create mode 100644 frontend/src/assets/cardgoose-square.svg diff --git a/frontend/index.html b/frontend/index.html index 0ac0493..2f29742 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,8 +8,8 @@ content="Design and print tabletop card games with CardGoose." /> CardGoose - - + + - + @@ -25,7 +25,7 @@ name="twitter:description" content="Design and print tabletop card games with CardGoose." /> - +
diff --git a/frontend/public/cardgoose-favicon.svg b/frontend/public/cardgoose-favicon.svg new file mode 100644 index 0000000..912fbaa --- /dev/null +++ b/frontend/public/cardgoose-favicon.svg @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/App.css b/frontend/src/App.css index 3c7267d..cbe4e2b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1655,8 +1655,17 @@ button.zone-row-body:hover { font-weight: 500; } -.studio-shell-brand-text { - white-space: nowrap; +.studio-shell-brand--mark-only { + gap: 0; + line-height: 0; + padding: 4px 0; +} + +.studio-shell-header--dash .studio-shell-row { + min-height: 56px; + padding-left: 14px; + padding-right: 20px; + align-items: center; } .studio-shell-logo { @@ -1667,6 +1676,15 @@ button.zone-row-body:hover { color: var(--accent); } +.brand-logo-mark svg { + display: block; +} + +/* Defeat black fills from exports (invisible on dark chrome); parent sets color */ +.brand-logo-mark path { + fill: currentColor; +} + .studio-shell-left { display: flex; align-items: center; diff --git a/frontend/src/assets/cardgoose-mark.svg b/frontend/src/assets/cardgoose-mark.svg new file mode 100644 index 0000000..e0b12af --- /dev/null +++ b/frontend/src/assets/cardgoose-mark.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/cardgoose-square.svg b/frontend/src/assets/cardgoose-square.svg new file mode 100644 index 0000000..5c9fc63 --- /dev/null +++ b/frontend/src/assets/cardgoose-square.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/BrandLogo.tsx b/frontend/src/components/BrandLogo.tsx index 888b90a..c85ef7e 100644 --- a/frontend/src/components/BrandLogo.tsx +++ b/frontend/src/components/BrandLogo.tsx @@ -1,5 +1,4 @@ -/** CardGoose mark — served from `public/cardgoose-logo.png`. */ -export const BRAND_LOGO_SRC = '/cardgoose-logo.png'; +import cardgooseMarkSvg from '../assets/cardgoose-mark.svg?raw'; type BrandLogoProps = { /** CSS height in pixels; width follows aspect ratio. */ @@ -9,14 +8,73 @@ type BrandLogoProps = { alt?: string; }; +/** Normalize any exported SVG so it always renders: strip XML PI, replace root <svg>, theme fill. */ +function buildLogoHtml(raw: string, heightPx: number): string { + let s = raw.replace(/^\uFEFF/, '').replace(/<\?xml[^?]*\?>\s*/i, ''); + + const openMatch = s.match(/^]*)>/i); + if (!openMatch) { + return s; + } + + const attrStr = openMatch[1] ?? ''; + let viewBoxStr: string | null = null; + const vb = attrStr.match(/\bviewBox\s*=\s*["']([^"']*)["']/i); + if (vb?.[1]) { + viewBoxStr = vb[1].trim(); + } + + let viewW = 1704; + let viewH = 905; + if (viewBoxStr) { + const p = viewBoxStr.split(/\s+/).map(Number); + if (p.length === 4 && p.every((n) => !Number.isNaN(n))) { + viewW = p[2]; + viewH = p[3]; + } + } else { + const w = attrStr.match(/\bwidth\s*=\s*["']?(\d+(?:\.\d+)?)/i); + const h = attrStr.match(/\bheight\s*=\s*["']?(\d+(?:\.\d+)?)/i); + if (w && h) { + viewW = parseFloat(w[1]); + viewH = parseFloat(h[1]); + viewBoxStr = `0 0 ${viewW} ${viewH}`; + } + } + + if (!viewBoxStr) { + viewBoxStr = `0 0 ${viewW} ${viewH}`; + } + + const widthPx = (heightPx * viewW) / viewH; + + s = s.replace( + /^]*>/i, + `` + ); + + s = s.replace(/\bfill\s*=\s*["']#000000["']/gi, 'fill="currentColor"'); + s = s.replace(/\bfill\s*=\s*["']black["']/gi, 'fill="currentColor"'); + + return s; +} + export function BrandLogo({ heightPx, className, alt = '' }: BrandLogoProps) { + const svgHtml = buildLogoHtml(cardgooseMarkSvg, heightPx); + return ( - {alt} ); } diff --git a/frontend/src/components/StudioAppBar.tsx b/frontend/src/components/StudioAppBar.tsx index e1d4a5e..5469091 100644 --- a/frontend/src/components/StudioAppBar.tsx +++ b/frontend/src/components/StudioAppBar.tsx @@ -144,13 +144,13 @@ function DashboardBar() {
layoutEditor?.onNavigateHomeClick(e)} + aria-label="CardGoose home" > - + - CardGoose
@@ -188,7 +188,7 @@ function ProjectTabsBar() { aria-label="Home" > - + {projectName} @@ -224,7 +224,7 @@ function ProjectLoadingBar() {
- + @@ -306,7 +306,7 @@ function EditorBar() { aria-label="Home" > - + From b575c229ac3ecff8e27e131e8dd732cc4f67d589 Mon Sep 17 00:00:00 2001 From: Will Zakielarz Date: Wed, 8 Apr 2026 15:39:53 -0400 Subject: [PATCH 2/4] banner --- .github/readme-assets/banner.png | Bin 0 -> 38367 bytes README.md | 10 ++++++---- 2 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 .github/readme-assets/banner.png diff --git a/.github/readme-assets/banner.png b/.github/readme-assets/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..6e8de2dc44498b4cb7bc6c2daad1821b3b5a1082 GIT binary patch literal 38367 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 - CardGoose 🃏🪿 - +

+ CardGoose — board games for Print & Play and Tabletop Simulator +

-

A tool to make board games for Print & Play and Tabletop Simulator.

+

+ A tool to make board games for Print & Play and Tabletop Simulator. +

## Prerequisites From 1819cb20ba56d0dc8fe4a49b2b9969263b1f3afe Mon Sep 17 00:00:00 2001 From: Will Zakielarz Date: Wed, 8 Apr 2026 16:01:42 -0400 Subject: [PATCH 3/4] Update README.md --- README.md | 107 +++++++++++++++++++++++++++++------------------------- 1 file changed, 57 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 7745ec6..20395ed 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,29 @@ -

- CardGoose — board games for Print & Play and Tabletop Simulator -

-

- A tool to make board games for Print & Play and Tabletop Simulator. -

+ +--- + +A tool to make board games for **Print & Play** and [Tabletop Simulator](https://www.tabletopsimulator.com/). + +## About + +--- + +Designing board games is hard. It requires **rapid iteration** and the ability to make changes **on-the-fly**. CardGoose makes it easier with data-driven components and custom templates to build your ideas in minutes. + +## Key Features + +--- + +- Import data from Google Sheets & refresh for live updates +- Component layout editor for card design + - Deck preview to evaluate changes +- Custom images +- PDF export +- TTS export (coming soon) + +# Getting started + +--- ## Prerequisites @@ -21,51 +40,40 @@ Pick one path. They differ by **where Postgres and object/queue services run**, not by repo layout. + | Mode | Frontend | API & worker | Database | Buckets & queue | | --------------- | -------- | ------------ | -------- | --------------------- | | **Fully local** | Your Mac | Your Mac | Docker | LocalStack (emulated) | | **Hybrid** | Your Mac | Your Mac | AWS RDS | AWS S3 & SQS | -| **Production** | ECS\* | ECS | AWS RDS | AWS S3 & SQS | +| **Production** | ECS | ECS | AWS RDS | AWS S3 & SQS | + -\*The API container can serve the built SPA in production (`NODE_ENV=production`). Until you add a stable URL (ALB, CloudFront, etc.), you may use the task public IP for smoke tests. +The API container can serve the built SPA in production (`NODE_ENV=production`). Until you add a stable URL (ALB, CloudFront, etc.), you may use the task public IP for smoke tests. ### Fully local (Docker Postgres + LocalStack) Use this when you want **no AWS calls**: everything emulated or on your machine. 1. **Start backing services** (Postgres on host port **5433**, LocalStack on **4566**): - - ```bash + ```bash pnpm docker:up - ``` - + ``` Optional: `docker compose build` if you use the `api` / `worker` compose services. - -2. **Configure** [`.env.local.example`](.env.local.example) → **`.env.local`** at the repo root using the **“full local stack”** block: `DATABASE_URL` pointing at `localhost:5433`, `AWS_ENDPOINT_URL=http://localhost:4566`, and bucket/queue names **`cardgoose-*`** (they must match [`docker-compose.yml`](docker-compose.yml) and [`docker/localstack-ready.d/init-aws.sh`](docker/localstack-ready.d/init-aws.sh)). - +2. **Configure** `[.env.local.example](.env.local.example)` → `**.env.local`** at the repo root using the **“full local stack”** block: `DATABASE_URL` pointing at `localhost:5433`, `AWS_ENDPOINT_URL=http://localhost:4566`, and bucket/queue names `**cardgoose-*`** (they must match `[docker-compose.yml](docker-compose.yml)` and `[docker/localstack-ready.d/init-aws.sh](docker/localstack-ready.d/init-aws.sh)`). 3. **Migrations:** - - ```bash + ```bash pnpm migrate:deploy - ``` - + ``` 4. **Run three processes** (three terminals from the repo root): - - ```bash + ```bash pnpm dev:api pnpm dev:frontend - ``` - - ```bash - cd worker - PYTHONPATH=src python3 -m baker.main - ``` - + ``` 5. Open the URL Vite prints (often `http://localhost:5173`). -Do **not** set `VITE_API_URL` in `frontend/.env.local` if the Vite dev server should proxy `/api` and `/health` to `http://localhost:3001` (see [`frontend/vite.config.ts`](frontend/vite.config.ts)). +Do **not** set `VITE_API_URL` in `frontend/.env.local` if the Vite dev server should proxy `/api` and `/health` to `http://localhost:3001` (see `[frontend/vite.config.ts](frontend/vite.config.ts)`). -**PDF exports:** set **`RENDER_URL`** in `.env.local` to the exact origin Vite prints (including port). If the worker runs in Docker and Vite on the host, use something like `http://host.docker.internal:5173`. Restart the worker after changes. +**PDF exports:** set `**RENDER_URL`** in `.env.local` to the exact origin Vite prints (including port). If the worker runs in Docker and Vite on the host, use something like `http://host.docker.internal:5173`. Restart the worker after changes. --- @@ -73,16 +81,16 @@ Do **not** set `VITE_API_URL` in `frontend/.env.local` if the Vite dev server sh Use this for day-to-day work: **Vite, the API, and the worker on your laptop** with **real RDS, S3, and SQS** in `us-east-1`. You get hot reload without deploying containers. -**1. Terraform (occasional)** — in [`infra/envs/prod`](infra/envs/prod): +**1. Terraform (occasional)** — in `[infra/envs/prod](infra/envs/prod)`: -- Allow your laptop to reach RDS: set `rds_dev_access_cidrs` without committing real IPs — e.g. `export TF_VAR_rds_dev_access_cidrs='["YOUR.PUBLIC.IP/32"]'` before `terraform apply`, or copy [`infra/envs/prod/rds.auto.tfvars.example`](infra/envs/prod/rds.auto.tfvars.example) to **`rds.auto.tfvars`** (gitignored). Update when your IP changes. +- Allow your laptop to reach RDS: set `rds_dev_access_cidrs` without committing real IPs — e.g. `export TF_VAR_rds_dev_access_cidrs='["YOUR.PUBLIC.IP/32"]'` before `terraform apply`, or copy `[infra/envs/prod/rds.auto.tfvars.example](infra/envs/prod/rds.auto.tfvars.example)` to `**rds.auto.tfvars`** (gitignored). Update when your IP changes. - Set `ecs_desired_count = 0` for the **worker** service if you run the Python worker locally (avoids two consumers on the same SQS queue and idle Fargate cost). - Run `terraform apply`. -**2. Root `.env.local`** — copy [`.env.local.example`](.env.local.example) and fill **real** values: +**2. Root `.env.local`** — copy `[.env.local.example](.env.local.example)` and fill **real** values: -- **`DATABASE_URL`** — RDS host, user `forge`, password from `terraform output -raw rds_master_password`, database name from your RDS instance (by default Terraform uses the `project_name` value, e.g. `cardboardforge`). -- **`S3_BUCKET_ASSETS`**, **`S3_BUCKET_EXPORTS`**, **`SQS_QUEUE_URL`** — from `terraform output` (`assets_bucket_name`, `exports_bucket_name`, `pdf_queue_url`). +- `**DATABASE_URL`** — RDS host, user `forge`, password from `terraform output -raw rds_master_password`, database name from your RDS instance (by default Terraform uses the `project_name` value, e.g. `cardboardforge`). +- `**S3_BUCKET_ASSETS**`, `**S3_BUCKET_EXPORTS**`, `**SQS_QUEUE_URL**` — from `terraform output` (`assets_bucket_name`, `exports_bucket_name`, `pdf_queue_url`). Do **not** set `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_ENDPOINT_URL` for real AWS: use the default credential chain (`~/.aws/credentials` or SSO). @@ -98,7 +106,7 @@ pnpm migrate:deploy **Queue contention:** if the **ECS worker** is scaled up, it shares the same SQS URL as your laptop. For predictable local PDF debugging, keep ECS worker desired count at **0** while testing locally. -**Frontend-only against a cloud API:** set `VITE_API_URL=http://:3001` in `frontend/.env.local` and run `pnpm dev:frontend` only. Find the task IP in the ECS console (see [`frontend/.env.local.example`](frontend/.env.local.example)). This is still “hybrid” from the browser’s perspective (local UI, remote API). +**Frontend-only against a cloud API:** set `VITE_API_URL=http://:3001` in `frontend/.env.local` and run `pnpm dev:frontend` only. Find the task IP in the ECS console (see `[frontend/.env.local.example](frontend/.env.local.example)`). This is still “hybrid” from the browser’s perspective (local UI, remote API). **Troubleshooting RDS:** if Prisma cannot connect, confirm your IP is in `rds_dev_access_cidrs`, re-apply Terraform, and that `DATABASE_URL` matches `terraform output -raw rds_endpoint`. @@ -106,34 +114,32 @@ pnpm migrate:deploy ### Production (deployed on AWS) -**Production** means the **API and worker run on ECS Fargate**, with **RDS, S3, and SQS** provisioned by Terraform. CI can build and push images and force new deployments (see [`.github/workflows/ci.yml`](.github/workflows/ci.yml)). - -1. **Bootstrap and apply** — follow [infra/BOOTSTRAP.md](infra/BOOTSTRAP.md) for remote state, then apply [`infra/envs/prod`](infra/envs/prod). Resource names, buckets, queues, and the RDS database name follow your Terraform variables (defaults in this repo still use the historical `cardboardforge` prefix for AWS resources). +**Production** means the **API and worker run on ECS Fargate**, with **RDS, S3, and SQS** provisioned by Terraform. CI can build and push images and force new deployments (see `[.github/workflows/ci.yml](.github/workflows/ci.yml)`). -2. **Runtime config** — ECS tasks receive env vars from Terraform (not from `.env.local`). For **one-off Prisma operations** against production RDS (migrations, introspection), copy [`.env.production.example`](.env.production.example) to **`.env.production`** (never commit) with values that match your live stack: - - ```bash +1. **Bootstrap and apply** — follow [infra/BOOTSTRAP.md](infra/BOOTSTRAP.md) for remote state, then apply `[infra/envs/prod](infra/envs/prod)`. Resource names, buckets, queues, and the RDS database name follow your Terraform variables (defaults in this repo still use the historical `cardboardforge` prefix for AWS resources). +2. **Runtime config** — ECS tasks receive env vars from Terraform (not from `.env.local`). For **one-off Prisma operations** against production RDS (migrations, introspection), copy `[.env.production.example](.env.production.example)` to `**.env.production`** (never commit) with values that match your live stack: + ```bash pnpm migrate:deploy:prod - ``` - + ``` 3. **Deploys** — pushes to `main` run lint, tests, Terraform validate, Docker builds, and (with secrets configured) ECR push + ECS `update-service --force-new-deployment`. Adjust branch rules in the workflow if your default branch differs. - 4. **Smoke checks** — hit `/health` on a running API task; confirm `service` identifies as `cardgoose-api`. Scale worker and API services in ECS per load and cost. --- ## Environment files + | File | Purpose | | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -| [`.env.local.example`](.env.local.example) | Template for **local** dev — copy to **`.env.local`** at the repo root. | -| [`.env.production.example`](.env.production.example) | Template for **production DB / ops** — copy to **`.env.production`** for Prisma and tooling (never commit). | +| `[.env.local.example](.env.local.example)` | Template for **local** dev — copy to `**.env.local`** at the repo root. | +| `[.env.production.example](.env.production.example)` | Template for **production DB / ops** — copy to `**.env.production`** for Prisma and tooling (never commit). | -The API loads **`.env.local`** when `NODE_ENV` is not `production`. Prisma CLI uses `dotenv -e ../.env.local` or `../.env.production` via the `prisma:deploy` scripts. The Baker worker loads the same **`.env.local`** when run locally (`python-dotenv`; existing environment variables win). -If you still have a single **`.env`** from an older setup, merge into `.env.local` and remove `.env`. +The API loads `**.env.local**` when `NODE_ENV` is not `production`. Prisma CLI uses `dotenv -e ../.env.local` or `../.env.production` via the `prisma:deploy` scripts. The Baker worker loads the same `**.env.local**` when run locally (`python-dotenv`; existing environment variables win). -**Docker Compose API:** `docker compose up api` runs `prisma migrate deploy` before `node` (see [`docker-compose.yml`](docker-compose.yml)). +If you still have a single `**.env**` from an older setup, merge into `.env.local` and remove `.env`. + +**Docker Compose API:** `docker compose up api` runs `prisma migrate deploy` before `node` (see `[docker-compose.yml](docker-compose.yml)`). --- @@ -146,3 +152,4 @@ pnpm test:all cd infra && terraform fmt -check -recursive cd infra/envs/prod && terraform init -input=false && terraform validate ``` + From d2420319beebdb9a2838dc45f243c05af8296f3c Mon Sep 17 00:00:00 2001 From: Will Zakielarz Date: Wed, 8 Apr 2026 16:04:33 -0400 Subject: [PATCH 4/4] Update README.md Updated README to use HTML formatting for better styling. --- README.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 20395ed..bf55048 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ +

+ CardGoose — board games for Print & Play and Tabletop Simulator +

+

+ A tool to make board games for Print & Play and Tabletop Simulator. +

--- -A tool to make board games for **Print & Play** and [Tabletop Simulator](https://www.tabletopsimulator.com/). - ## About ---- - Designing board games is hard. It requires **rapid iteration** and the ability to make changes **on-the-fly**. CardGoose makes it easier with data-driven components and custom templates to build your ideas in minutes. ## Key Features ---- - - Import data from Google Sheets & refresh for live updates - Component layout editor for card design - Deck preview to evaluate changes @@ -23,8 +23,6 @@ Designing board games is hard. It requires **rapid iteration** and the ability t # Getting started ---- - ## Prerequisites - Node.js 20+