From e1f4f347b027a2f4b65bc4b53627dc95361aef64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=E1=BA=BF=20H=C6=B0ng?= Date: Fri, 1 May 2026 04:26:46 +0700 Subject: [PATCH 01/14] chore: ignore local worktrees directory Keep project-local worktrees out of the repository while using isolated feature branches. Co-authored-by: Claude Opus 4.7 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5b70eb0..194ea45 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ out/ ### VS Code ### .vscode/ +.worktrees/ From 362fc6037c32c3d8a4f4a5c466c7f622a340eea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=E1=BA=BF=20H=C6=B0ng?= Date: Fri, 1 May 2026 04:30:50 +0700 Subject: [PATCH 02/14] fix: remove duplicate register request constructor Unblock compilation by keeping only the zero-arg Lombok constructor on the empty Redis request model. Co-authored-by: Claude Opus 4.7 --- .../june8th/ticketrushserver/data/RegisterAccountRequest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/me/june8th/ticketrushserver/data/RegisterAccountRequest.java b/src/main/java/me/june8th/ticketrushserver/data/RegisterAccountRequest.java index 3889f07..b4fd014 100644 --- a/src/main/java/me/june8th/ticketrushserver/data/RegisterAccountRequest.java +++ b/src/main/java/me/june8th/ticketrushserver/data/RegisterAccountRequest.java @@ -1,6 +1,5 @@ package me.june8th.ticketrushserver.data; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @@ -9,7 +8,6 @@ @RedisHash(value = "register_account_request") @Data @NoArgsConstructor -@AllArgsConstructor @Builder public class RegisterAccountRequest { From cc9e811614a123e39c065a4b1e8c1463dc9f0c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=E1=BA=BF=20H=C6=B0ng?= Date: Fri, 1 May 2026 04:32:06 +0700 Subject: [PATCH 03/14] chore: snapshot email design references Copy the stable TicketRush branding component and theme source into the server repo so email theming can build without sibling-repo coupling. Co-authored-by: Claude Opus 4.7 --- emails/reference/ticketrush/Branding.tsx | 32 +++++++ emails/reference/ticketrush/globals.css | 104 +++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 emails/reference/ticketrush/Branding.tsx create mode 100644 emails/reference/ticketrush/globals.css diff --git a/emails/reference/ticketrush/Branding.tsx b/emails/reference/ticketrush/Branding.tsx new file mode 100644 index 0000000..82ec5d1 --- /dev/null +++ b/emails/reference/ticketrush/Branding.tsx @@ -0,0 +1,32 @@ +export function Logo({ height = 40, accentColor = true }: { height?: number, accentColor?: boolean } = {}) { + return ( +
+
+ ticket + rush +
+ +
+ ); +} + +export function SquareLogo({ height = 40, accentColor = true }: { height?: number, accentColor?: boolean } = {}) { + return ( + + + + ); +} diff --git a/emails/reference/ticketrush/globals.css b/emails/reference/ticketrush/globals.css new file mode 100644 index 0000000..98e6f45 --- /dev/null +++ b/emails/reference/ticketrush/globals.css @@ -0,0 +1,104 @@ +@import "tailwindcss"; +@import "@heroui/styles"; + +html, +body, +#root { + width: 100%; + overflow-x: hidden; +} + +:root { + --radius: 0.5rem; + --field-radius: 0.5rem; + + font-family: + "Inter", + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + Oxygen, + Ubuntu, + Cantarell, + "Open Sans", + "Helvetica Neue", + sans-serif; + background-color: var(--background); + color: var(--foreground); + color-scheme: light dark; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + /* Theme Colors (Light Mode, default) */ + --accent: oklch(83.77% 0.1655 81.92); + --accent-foreground: oklch(15% 0.03 81.92); + --background: oklch(97.02% 0 81.92); + --border: oklch(90% 0 81.92); + --danger: oklch(65.32% 0.2328 5.11); + --danger-foreground: oklch(99.11% 0 0); + --default: oklch(94% 0 81.92); + --default-foreground: oklch(21.03% 0.0059 81.92); + --field-background: oklch(100% 0 81.92); + --field-foreground: oklch(21.03% 0 81.92); + --field-placeholder: oklch(55.17% 0 81.92); + --focus: oklch(83.77% 0.1655 81.92); + --foreground: oklch(21.03% 0 81.92); + --muted: oklch(55.17% 0 81.92); + --overlay: oklch(100% 0 81.92); + --overlay-foreground: oklch(21.03% 0 81.92); + --scrollbar: oklch(87.1% 0 81.92); + --segment: oklch(100% 0 81.92); + --segment-foreground: oklch(21.03% 0 81.92); + --separator: oklch(92% 0 81.92); + --success: oklch(73.29% 0.1935 130.18); + --success-foreground: oklch(21.03% 0.0059 130.18); + --surface: oklch(100% 0 81.92); + --surface-foreground: oklch(21.03% 0 81.92); + --surface-secondary: oklch(95.24% 0 81.92); + --surface-secondary-foreground: oklch(21.03% 0 81.92); + --surface-tertiary: oklch(93.73% 0 81.92); + --surface-tertiary-foreground: oklch(21.03% 0 81.92); + --warning: oklch(78.19% 0.1585 51.7); + --warning-foreground: oklch(21.03% 0.0059 51.7); +} + +@media (prefers-color-scheme: dark) { + :root { + /* Theme Colors (Dark Mode) */ + --accent: oklch(83.77% 0.1655 81.92); + --accent-foreground: oklch(15% 0.03 81.92); + --background: oklch(12% 0 81.92); + --border: oklch(28% 0 81.92); + --danger: oklch(59.4% 0.1967 4); + --danger-foreground: oklch(99.11% 0 0); + --default: oklch(27.4% 0 81.92); + --default-foreground: oklch(99.11% 0 0); + --field-background: oklch(21.03% 0 81.92); + --field-foreground: oklch(99.11% 0 81.92); + --field-placeholder: oklch(70.5% 0 81.92); + --focus: oklch(83.77% 0.1655 81.92); + --foreground: oklch(99.11% 0 81.92); + --muted: oklch(70.5% 0 81.92); + --overlay: oklch(21.03% 0 81.92); + --overlay-foreground: oklch(99.11% 0 81.92); + --scrollbar: oklch(70.5% 0 81.92); + --segment: oklch(39.64% 0 81.92); + --segment-foreground: oklch(99.11% 0 81.92); + --separator: oklch(25% 0 81.92); + --success: oklch(73.29% 0.1935 130.18); + --success-foreground: oklch(21.03% 0.0059 130.18); + --surface: oklch(21.03% 0 81.92); + --surface-foreground: oklch(99.11% 0 81.92); + --surface-secondary: oklch(25.7% 0 81.92); + --surface-secondary-foreground: oklch(99.11% 0 81.92); + --surface-tertiary: oklch(27.21% 0 81.92); + --surface-tertiary-foreground: oklch(99.11% 0 81.92); + --warning: oklch(82.03% 0.1388 55.71); + --warning-foreground: oklch(21.03% 0.0059 55.71); + } +} From a5090785b9111dbc50f322776cc74b6458b37270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=E1=BA=BF=20H=C6=B0ng?= Date: Fri, 1 May 2026 04:34:07 +0700 Subject: [PATCH 04/14] feat: scaffold email builder workspace Add an isolated Vite 8 and React workspace for static email authoring, with its own dependencies, build entrypoints, and local artifact ignore rules. Co-authored-by: Claude Opus 4.7 --- .gitignore | 2 + emails/package-lock.json | 1969 ++++++++++++++++++++++++++++++ emails/package.json | 25 + emails/scripts/render-emails.mts | 1 + emails/src/main.css | 1 + emails/src/preview.tsx | 5 + emails/tsconfig.json | 16 + emails/vite.config.ts | 17 + 8 files changed, 2036 insertions(+) create mode 100644 emails/package-lock.json create mode 100644 emails/package.json create mode 100644 emails/scripts/render-emails.mts create mode 100644 emails/src/main.css create mode 100644 emails/src/preview.tsx create mode 100644 emails/tsconfig.json create mode 100644 emails/vite.config.ts diff --git a/.gitignore b/.gitignore index 194ea45..273d819 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ out/ ### VS Code ### .vscode/ .worktrees/ +emails/node_modules/ +emails/dist/ diff --git a/emails/package-lock.json b/emails/package-lock.json new file mode 100644 index 0000000..6cf8303 --- /dev/null +++ b/emails/package-lock.json @@ -0,0 +1,1969 @@ +{ + "name": "ticketrush-email-builder", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ticketrush-email-builder", + "version": "1.0.0", + "dependencies": { + "juice": "^11.0.3", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "tsx": "^4.20.6", + "typescript": "~6.0.2", + "vite": "^8.0.4" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/juice": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/juice/-/juice-11.1.1.tgz", + "integrity": "sha512-4SBfZqKcc6DrIS+5b/WiGoWaZsdUPBH+e6SbRlNjJpaIRtfoBhYReAtobIEW6mcLeFFDXLBJMuZwkJLkBJjs2w==", + "license": "MIT", + "dependencies": { + "cheerio": "1.0.0", + "commander": "^12.1.0", + "entities": "^7.0.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^8.0.0" + }, + "bin": { + "juice": "bin/juice" + }, + "engines": { + "node": ">=18.17" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==", + "license": "MIT" + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", + "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", + "license": "MIT (http://mootools.net/license.txt)", + "engines": { + "node": "*" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/vite": { + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/web-resource-inliner": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-8.0.0.tgz", + "integrity": "sha512-Ezr98sqXW/+OCGoUEXuOKVR+oVFlSdn1tIySEEJdiSAw4IjrW8hQkwARSSBJTSB5Us5dnytDgL0ZDliAYBhaNA==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^9.1.0", + "mime": "^2.4.6", + "valid-data-url": "^3.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/emails/package.json b/emails/package.json new file mode 100644 index 0000000..2544130 --- /dev/null +++ b/emails/package.json @@ -0,0 +1,25 @@ +{ + "name": "ticketrush-email-builder", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "build:assets": "vite build", + "render": "tsx scripts/render-emails.mts", + "build": "npm run build:assets && npm run render" + }, + "dependencies": { + "juice": "^11.0.3", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "tsx": "^4.20.6", + "typescript": "~6.0.2", + "vite": "^8.0.4" + } +} diff --git a/emails/scripts/render-emails.mts b/emails/scripts/render-emails.mts new file mode 100644 index 0000000..db05bcf --- /dev/null +++ b/emails/scripts/render-emails.mts @@ -0,0 +1 @@ +console.log("Email render pipeline scaffolded. Templates not implemented yet."); diff --git a/emails/src/main.css b/emails/src/main.css new file mode 100644 index 0000000..d3c1078 --- /dev/null +++ b/emails/src/main.css @@ -0,0 +1 @@ +/* Email workspace styles arrive in later steps. */ diff --git a/emails/src/preview.tsx b/emails/src/preview.tsx new file mode 100644 index 0000000..c6a2afb --- /dev/null +++ b/emails/src/preview.tsx @@ -0,0 +1,5 @@ +import "./main.css"; + +export function PreviewEntrypoint() { + return null; +} diff --git a/emails/tsconfig.json b/emails/tsconfig.json new file mode 100644 index 0000000..999eab3 --- /dev/null +++ b/emails/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["src", "scripts", "vite.config.ts"] +} diff --git a/emails/vite.config.ts b/emails/vite.config.ts new file mode 100644 index 0000000..49e4081 --- /dev/null +++ b/emails/vite.config.ts @@ -0,0 +1,17 @@ +import { resolve } from "node:path"; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: "dist", + emptyOutDir: true, + cssCodeSplit: false, + lib: { + entry: resolve(__dirname, "src/preview.tsx"), + formats: ["es"], + fileName: () => "preview.js", + }, + }, +}); From e3d60b53d82dd3e9c77ac6b2160005fb1d38dbe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=E1=BA=BF=20H=C6=B0ng?= Date: Fri, 1 May 2026 04:35:13 +0700 Subject: [PATCH 05/14] feat: add email theme tokens Define email-safe TicketRush colors, typography, spacing, and dark-mode selectors so the rendered templates share one consistent brand system across inbox themes. Co-authored-by: Claude Opus 4.7 --- emails/src/theme/dark-mode.css | 118 +++++++++++++++++++++++++++++++++ emails/src/theme/tokens.ts | 51 ++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 emails/src/theme/dark-mode.css create mode 100644 emails/src/theme/tokens.ts diff --git a/emails/src/theme/dark-mode.css b/emails/src/theme/dark-mode.css new file mode 100644 index 0000000..d759e3c --- /dev/null +++ b/emails/src/theme/dark-mode.css @@ -0,0 +1,118 @@ +@media (prefers-color-scheme: dark) { + body[data-email-root="true"] { + background-color: #16120d !important; + color: #f6efe4 !important; + } + + .email-shell { + background: #16120d !important; + } + + .email-card { + background: #211a13 !important; + border-color: #3a2f20 !important; + box-shadow: 0 16px 44px rgba(0, 0, 0, 0.35) !important; + } + + .email-card-secondary { + background: #2a2118 !important; + border-color: #3a2f20 !important; + } + + .email-text, + .email-title, + .brand-primary, + .otp-caption, + .meta-text { + color: #f6efe4 !important; + } + + .email-muted, + .email-caption, + .meta-muted, + .brand-secondary { + color: #c2b3a0 !important; + } + + .accent-text, + .brand-accent, + .otp-code { + color: #e3b64f !important; + } + + .accent-surface { + background: #553c08 !important; + border-color: #826423 !important; + } + + .accent-pill { + background: #312619 !important; + border-color: #5b4622 !important; + color: #fde7ab !important; + } +} + +[data-ogsc] body[data-email-root="true"], +[data-ogsb] body[data-email-root="true"] { + background-color: #16120d !important; + color: #f6efe4 !important; +} + +[data-ogsc] .email-card, +[data-ogsb] .email-card { + background: #211a13 !important; + border-color: #3a2f20 !important; + box-shadow: 0 16px 44px rgba(0, 0, 0, 0.35) !important; +} + +[data-ogsc] .email-card-secondary, +[data-ogsb] .email-card-secondary { + background: #2a2118 !important; + border-color: #3a2f20 !important; +} + +[data-ogsc] .email-text, +[data-ogsb] .email-text, +[data-ogsc] .email-title, +[data-ogsb] .email-title, +[data-ogsc] .brand-primary, +[data-ogsb] .brand-primary, +[data-ogsc] .otp-caption, +[data-ogsb] .otp-caption, +[data-ogsc] .meta-text, +[data-ogsb] .meta-text { + color: #f6efe4 !important; +} + +[data-ogsc] .email-muted, +[data-ogsb] .email-muted, +[data-ogsc] .email-caption, +[data-ogsb] .email-caption, +[data-ogsc] .meta-muted, +[data-ogsb] .meta-muted, +[data-ogsc] .brand-secondary, +[data-ogsb] .brand-secondary { + color: #c2b3a0 !important; +} + +[data-ogsc] .accent-text, +[data-ogsb] .accent-text, +[data-ogsc] .brand-accent, +[data-ogsb] .brand-accent, +[data-ogsc] .otp-code, +[data-ogsb] .otp-code { + color: #e3b64f !important; +} + +[data-ogsc] .accent-surface, +[data-ogsb] .accent-surface { + background: #553c08 !important; + border-color: #826423 !important; +} + +[data-ogsc] .accent-pill, +[data-ogsb] .accent-pill { + background: #312619 !important; + border-color: #5b4622 !important; + color: #fde7ab !important; +} diff --git a/emails/src/theme/tokens.ts b/emails/src/theme/tokens.ts new file mode 100644 index 0000000..e6c201a --- /dev/null +++ b/emails/src/theme/tokens.ts @@ -0,0 +1,51 @@ +export const emailTheme = { + font: { + body: 'Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + brand: 'Nunito, Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + }, + color: { + light: { + accent: "#d4a63c", + accentForeground: "#34230a", + background: "#f6f1e8", + border: "#e7dcc9", + card: "#fffdfa", + cardSecondary: "#f8f1e5", + foreground: "#1f1912", + muted: "#6d6458", + outline: "#f0d79a", + otpBackground: "#f6e3a9", + otpForeground: "#704d00", + pillBackground: "#f3ead8", + shadow: "rgba(54, 36, 10, 0.12)", + subtleShadow: "rgba(54, 36, 10, 0.08)", + }, + dark: { + accent: "#e3b64f", + accentForeground: "#281b07", + background: "#16120d", + border: "#3a2f20", + card: "#211a13", + cardSecondary: "#2a2118", + foreground: "#f6efe4", + muted: "#c2b3a0", + outline: "#5b4622", + otpBackground: "#553c08", + otpForeground: "#fde7ab", + pillBackground: "#312619", + shadow: "rgba(0, 0, 0, 0.35)", + subtleShadow: "rgba(0, 0, 0, 0.2)", + }, + }, + radius: { + shell: 32, + card: 24, + badge: 999, + otp: 18, + }, + spacing: { + shellInset: 24, + cardInsetX: 28, + cardInsetY: 32, + }, +} as const; From a4d226f285fd375cca712869a2884c6b23bf8a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=E1=BA=BF=20H=C6=B0ng?= Date: Fri, 1 May 2026 04:37:03 +0700 Subject: [PATCH 06/14] feat: add reusable email components Build shared React shells for branded OTP emails so both flows can reuse the same email-safe structure, preview text handling, and dark-mode hooks. Co-authored-by: Claude Opus 4.7 --- emails/src/components/BrandHeader.tsx | 113 ++++++++++++++++++++++++++ emails/src/components/EmailShell.tsx | 79 ++++++++++++++++++ emails/src/components/OtpCard.tsx | 81 ++++++++++++++++++ emails/src/vite-env.d.ts | 1 + 4 files changed, 274 insertions(+) create mode 100644 emails/src/components/BrandHeader.tsx create mode 100644 emails/src/components/EmailShell.tsx create mode 100644 emails/src/components/OtpCard.tsx create mode 100644 emails/src/vite-env.d.ts diff --git a/emails/src/components/BrandHeader.tsx b/emails/src/components/BrandHeader.tsx new file mode 100644 index 0000000..5478d3b --- /dev/null +++ b/emails/src/components/BrandHeader.tsx @@ -0,0 +1,113 @@ +import { emailTheme } from "../theme/tokens"; + +type BrandHeaderProps = { + badge: string; + title: string; + body: string; +}; + +export function BrandHeader({ badge, title, body }: BrandHeaderProps) { + return ( + <> + + + + + + + + + + + + + + + +
+ + + + + + + +
+ ticket + rush + + +
+
+ + {badge} + +
+

+ {title} +

+
+

+ {body} +

+
+ + ); +} + +function BrandSquare() { + return ( + + ); +} + +const wordmarkStyle = { + color: emailTheme.color.light.foreground, + fontFamily: emailTheme.font.brand, + fontSize: "30px", + fontWeight: 900, + lineHeight: 1, + letterSpacing: "-0.05em", +} as const; diff --git a/emails/src/components/EmailShell.tsx b/emails/src/components/EmailShell.tsx new file mode 100644 index 0000000..c7c3e63 --- /dev/null +++ b/emails/src/components/EmailShell.tsx @@ -0,0 +1,79 @@ +import type { ReactNode } from "react"; + +import { emailTheme } from "../theme/tokens"; + +type EmailShellProps = { + title: string; + previewText: string; + darkModeCss: string; + children: ReactNode; +}; + +export function EmailShell({ title, previewText, darkModeCss, children }: EmailShellProps) { + return ( + + + + + + + {title} + + + +
+ {previewText} +
+ + + + + + + +
+ + + + + + +
{children}
+
+ + + ); +} diff --git a/emails/src/components/OtpCard.tsx b/emails/src/components/OtpCard.tsx new file mode 100644 index 0000000..943565b --- /dev/null +++ b/emails/src/components/OtpCard.tsx @@ -0,0 +1,81 @@ +import { emailTheme } from "../theme/tokens"; + +type OtpCardProps = { + label: string; + code: string; + hint: string; +}; + +export function OtpCard({ label, code, hint }: OtpCardProps) { + return ( + + + + + + +
+

+ {label} +

+ +
+
+ {code} +
+
+ +

+ {hint} +

+
+ ); +} diff --git a/emails/src/vite-env.d.ts b/emails/src/vite-env.d.ts new file mode 100644 index 0000000..cbe652d --- /dev/null +++ b/emails/src/vite-env.d.ts @@ -0,0 +1 @@ +declare module "*.css"; From 0ca2e767782135589e4cf6333303f48d67c405a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=E1=BA=BF=20H=C6=B0ng?= Date: Fri, 1 May 2026 04:38:22 +0700 Subject: [PATCH 07/14] feat: add OTP email templates Create shared register-confirmation and password-reset email templates with branded copy, preview text, and placeholder hooks for current Thymeleaf and future token replacement flows. Co-authored-by: Claude Opus 4.7 --- emails/src/components/OtpCard.tsx | 4 +- emails/src/templates/PasswordResetEmail.tsx | 70 +++++++++++++++++++ .../templates/RegisterConfirmationEmail.tsx | 70 +++++++++++++++++++ 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 emails/src/templates/PasswordResetEmail.tsx create mode 100644 emails/src/templates/RegisterConfirmationEmail.tsx diff --git a/emails/src/components/OtpCard.tsx b/emails/src/components/OtpCard.tsx index 943565b..47399f7 100644 --- a/emails/src/components/OtpCard.tsx +++ b/emails/src/components/OtpCard.tsx @@ -1,8 +1,10 @@ +import type { ReactNode } from "react"; + import { emailTheme } from "../theme/tokens"; type OtpCardProps = { label: string; - code: string; + code: ReactNode; hint: string; }; diff --git a/emails/src/templates/PasswordResetEmail.tsx b/emails/src/templates/PasswordResetEmail.tsx new file mode 100644 index 0000000..16f1465 --- /dev/null +++ b/emails/src/templates/PasswordResetEmail.tsx @@ -0,0 +1,70 @@ +import { BrandHeader } from "../components/BrandHeader"; +import { EmailShell } from "../components/EmailShell"; +import { OtpCard } from "../components/OtpCard"; +import { emailTheme } from "../theme/tokens"; + +type PasswordResetEmailProps = { + darkModeCss: string; + userName: string; + otpCode: string; +}; + +export function PasswordResetEmail({ darkModeCss, userName, otpCode }: PasswordResetEmailProps) { + return ( + + + + + + + + + + + + + + + +
+

+ Hi {userName}, +

+
+ {otpCode}} + hint="Enter this passcode in the app to continue resetting your password. If you did not request this, you can ignore this email and keep your current password." + /> +
+

+ For security, only use the latest code you requested. +

+
+
+ ); +} diff --git a/emails/src/templates/RegisterConfirmationEmail.tsx b/emails/src/templates/RegisterConfirmationEmail.tsx new file mode 100644 index 0000000..52ae9b4 --- /dev/null +++ b/emails/src/templates/RegisterConfirmationEmail.tsx @@ -0,0 +1,70 @@ +import { BrandHeader } from "../components/BrandHeader"; +import { EmailShell } from "../components/EmailShell"; +import { OtpCard } from "../components/OtpCard"; +import { emailTheme } from "../theme/tokens"; + +type RegisterConfirmationEmailProps = { + darkModeCss: string; + userName: string; + otpCode: string; +}; + +export function RegisterConfirmationEmail({ darkModeCss, userName, otpCode }: RegisterConfirmationEmailProps) { + return ( + + + + + + + + + + + + + + + +
+

+ Hi {userName}, +

+
+ {otpCode}} + hint="Enter this passcode in the app to finish registration. If this was not you, you can safely ignore this email." + /> +
+

+ Need help? Reply to this email and the TicketRush team can help you finish setup. +

+
+
+ ); +} From 832125e40523c3079e02ccccdb93658e521b07bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=E1=BA=BF=20H=C6=B0ng?= Date: Fri, 1 May 2026 04:40:25 +0700 Subject: [PATCH 08/14] feat: render static email HTML Compile base email CSS with Vite, inline it into React-rendered HTML, and keep dark-mode overrides in the document head for inbox compatibility. Co-authored-by: Claude Opus 4.7 --- emails/scripts/render-emails.mts | 86 +++++++++++++++++++++++++++++++- emails/src/main.css | 49 +++++++++++++++++- emails/src/theme/tokens.ts | 4 +- 3 files changed, 135 insertions(+), 4 deletions(-) diff --git a/emails/scripts/render-emails.mts b/emails/scripts/render-emails.mts index db05bcf..885e18f 100644 --- a/emails/scripts/render-emails.mts +++ b/emails/scripts/render-emails.mts @@ -1 +1,85 @@ -console.log("Email render pipeline scaffolded. Templates not implemented yet."); +import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import juice from "juice"; +import { createElement, type ReactElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { build as viteBuild } from "vite"; + +import { PasswordResetEmail } from "../src/templates/PasswordResetEmail"; +import { RegisterConfirmationEmail } from "../src/templates/RegisterConfirmationEmail"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const rootDir = resolve(__dirname, ".."); +const distDir = join(rootDir, "dist"); +const renderedDir = join(distDir, "rendered"); +const darkModeCssPath = join(rootDir, "src", "theme", "dark-mode.css"); + +const placeholderUserName = "__USER_NAME__"; +const placeholderOtpCode = "__OTP_CODE__"; + +async function main() { + await viteBuild({ + configFile: join(rootDir, "vite.config.ts"), + logLevel: "error", + }); + + const [baseCss, darkModeCss] = await Promise.all([ + readCompiledCss(), + readFile(darkModeCssPath, "utf8"), + ]); + + await rm(renderedDir, { recursive: true, force: true }); + await mkdir(renderedDir, { recursive: true }); + + const files = [ + { + fileName: "register-confirmation-email.html", + html: renderDocument( + createElement(RegisterConfirmationEmail, { + darkModeCss, + userName: placeholderUserName, + otpCode: placeholderOtpCode, + }), + ), + }, + { + fileName: "password-reset-email.html", + html: renderDocument( + createElement(PasswordResetEmail, { + darkModeCss, + userName: placeholderUserName, + otpCode: placeholderOtpCode, + }), + ), + }, + ]; + + await Promise.all( + files.map(async ({ fileName, html }) => { + const outputPath = join(renderedDir, fileName); + const finalHtml = juice.inlineContent(html, baseCss); + await writeFile(outputPath, finalHtml, "utf8"); + }), + ); + + console.log(`Rendered ${files.length} email templates to ${renderedDir}`); +} + +function renderDocument(markup: ReactElement) { + return `${renderToStaticMarkup(markup)}`; +} + +async function readCompiledCss() { + const entries = await readdir(distDir, { withFileTypes: true }); + const cssFile = entries.find((entry) => entry.isFile() && entry.name.endsWith(".css")); + + if (!cssFile) { + throw new Error(`No compiled CSS asset found in ${distDir}`); + } + + return readFile(join(distDir, cssFile.name), "utf8"); +} + +await main(); diff --git a/emails/src/main.css b/emails/src/main.css index d3c1078..84482d3 100644 --- a/emails/src/main.css +++ b/emails/src/main.css @@ -1 +1,48 @@ -/* Email workspace styles arrive in later steps. */ +.email-shell { + background: #f6f1e8; +} + +.email-card { + background: #fffdfa; + border: 1px solid #e7dcc9; + border-radius: 32px; + box-shadow: 0 18px 48px rgba(54, 36, 10, 0.12); +} + +.email-card-secondary { + background: #f8f1e5; + border: 1px solid #e7dcc9; + border-radius: 24px; +} + +.email-title, +.email-text, +.meta-text, +.brand-primary, +.otp-caption { + color: #1f1912; +} + +.email-muted, +.email-caption, +.meta-muted, +.brand-secondary { + color: #6d6458; +} + +.brand-accent, +.accent-text, +.otp-code { + color: #d4a63c; +} + +.accent-surface { + background: #f6e3a9; + border: 1px solid #f0d79a; +} + +.accent-pill { + background: #f3ead8; + border: 1px solid #f0d79a; + color: #34230a; +} diff --git a/emails/src/theme/tokens.ts b/emails/src/theme/tokens.ts index e6c201a..7e8e1dd 100644 --- a/emails/src/theme/tokens.ts +++ b/emails/src/theme/tokens.ts @@ -1,7 +1,7 @@ export const emailTheme = { font: { - body: 'Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', - brand: 'Nunito, Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + body: "Inter, Arial, Helvetica, sans-serif", + brand: "Nunito, Inter, Arial, Helvetica, sans-serif", }, color: { light: { From ad0ef6d1fde1d513eb1a077aeb1616c6c4841fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=E1=BA=BF=20H=C6=B0ng?= Date: Fri, 1 May 2026 04:41:58 +0700 Subject: [PATCH 09/14] feat: sync generated email templates Hook the React email builder into Gradle so processResources regenerates and copies the latest branded HTML into Spring template resources automatically. Co-authored-by: Claude Opus 4.7 --- build.gradle | 36 +++++ .../templates/password-reset-email.html | 148 ++++++++++++++---- .../register-confirmation-email.html | 148 ++++++++++++++---- 3 files changed, 274 insertions(+), 58 deletions(-) diff --git a/build.gradle b/build.gradle index 76dae54..e106e9b 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,11 @@ plugins { id 'io.spring.dependency-management' version '1.1.7' } +def emailsDir = layout.projectDirectory.dir('emails') +def renderedEmailsDir = layout.projectDirectory.dir('emails/dist/rendered') +def templatesDir = layout.projectDirectory.dir('src/main/resources/templates') +def npmCommand = System.getProperty('os.name').toLowerCase().contains('windows') ? 'npm.cmd' : 'npm' + group = 'me.june8th' version = '1.0.0-dev' description = 'TicketRushServer' @@ -51,3 +56,34 @@ dependencies { tasks.withType(Test).configureEach { useJUnitPlatform() } + +tasks.register('buildEmailTemplates', Exec) { + group = 'build' + description = 'Builds static email HTML from the React email workspace.' + workingDir = emailsDir.asFile + commandLine npmCommand, 'run', 'build' + + inputs.files( + fileTree(dir: 'emails/src'), + fileTree(dir: 'emails/scripts'), + 'emails/package.json', + 'emails/package-lock.json', + 'emails/tsconfig.json', + 'emails/vite.config.ts' + ) + outputs.dir(renderedEmailsDir) +} + +tasks.register('syncEmailTemplates', Sync) { + group = 'build' + description = 'Copies rendered email HTML into Spring template resources.' + dependsOn tasks.named('buildEmailTemplates') + + from(renderedEmailsDir) + include '*.html' + into templatesDir +} + +tasks.named('processResources') { + dependsOn tasks.named('syncEmailTemplates') +} diff --git a/src/main/resources/templates/password-reset-email.html b/src/main/resources/templates/password-reset-email.html index 62c62d3..91bb17d 100644 --- a/src/main/resources/templates/password-reset-email.html +++ b/src/main/resources/templates/password-reset-email.html @@ -1,29 +1,119 @@ - - - - - Reset your password - - -
-
-

Hello User,

-

- Please enter this one-time passcode to reset your account password: -

-
-
- 123456 -
-
-

- If you did not make this request, please ignore this email. -

-
-
- - +Reset your TicketRush password
Reset your TicketRush password with your one-time passcode.
\ No newline at end of file diff --git a/src/main/resources/templates/register-confirmation-email.html b/src/main/resources/templates/register-confirmation-email.html index b5cc562..e086059 100644 --- a/src/main/resources/templates/register-confirmation-email.html +++ b/src/main/resources/templates/register-confirmation-email.html @@ -1,29 +1,119 @@ - - - - - Confirm your registration - - -
-
-

Hello User,

-

- Please enter this one-time passcode to confirm your email address and complete your registration: -

-
-
- 123456 -
-
-

- If you did not make this request, please ignore this email. -

-
-
- - +Confirm your TicketRush account
Confirm your TicketRush account with your one-time passcode.
\ No newline at end of file From 2b120b224898c72d052e06008778445a0215714a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=E1=BA=BF=20H=C6=B0ng?= Date: Fri, 1 May 2026 04:54:38 +0700 Subject: [PATCH 10/14] style: refine email template typography Co-authored-by: Claude Opus 4.7 --- emails/scripts/render-emails.mts | 4 +- emails/src/components/BrandHeader.tsx | 42 +++------- emails/src/components/EmailShell.tsx | 2 +- emails/src/components/OtpCard.tsx | 31 ++------ emails/src/main.css | 35 ++++----- emails/src/templates/PasswordResetEmail.tsx | 18 ++--- .../templates/RegisterConfirmationEmail.tsx | 18 ++--- emails/src/theme/dark-mode.css | 76 ++++++++---------- emails/src/theme/tokens.ts | 64 +++++++-------- .../templates/password-reset-email.html | 78 ++++++++----------- .../register-confirmation-email.html | 78 ++++++++----------- 11 files changed, 182 insertions(+), 264 deletions(-) diff --git a/emails/scripts/render-emails.mts b/emails/scripts/render-emails.mts index 885e18f..ba5d086 100644 --- a/emails/scripts/render-emails.mts +++ b/emails/scripts/render-emails.mts @@ -16,8 +16,8 @@ const distDir = join(rootDir, "dist"); const renderedDir = join(distDir, "rendered"); const darkModeCssPath = join(rootDir, "src", "theme", "dark-mode.css"); -const placeholderUserName = "__USER_NAME__"; -const placeholderOtpCode = "__OTP_CODE__"; +const placeholderUserName = "there"; +const placeholderOtpCode = "000000"; async function main() { await viteBuild({ diff --git a/emails/src/components/BrandHeader.tsx b/emails/src/components/BrandHeader.tsx index 5478d3b..ccc1ffc 100644 --- a/emails/src/components/BrandHeader.tsx +++ b/emails/src/components/BrandHeader.tsx @@ -1,12 +1,11 @@ import { emailTheme } from "../theme/tokens"; type BrandHeaderProps = { - badge: string; title: string; body: string; }; -export function BrandHeader({ badge, title, body }: BrandHeaderProps) { +export function BrandHeader({ title, body }: BrandHeaderProps) { return ( <> @@ -29,36 +28,15 @@ export function BrandHeader({ badge, title, body }: BrandHeaderProps) { - - - -
- - {badge} - -
+

{title} @@ -72,8 +50,8 @@ export function BrandHeader({ badge, title, body }: BrandHeaderProps) { style={{ margin: 0, color: emailTheme.color.light.muted, - fontSize: "16px", - lineHeight: 1.7, + fontSize: "15px", + lineHeight: 1.6, }} > {body} @@ -89,8 +67,8 @@ export function BrandHeader({ badge, title, body }: BrandHeaderProps) { function BrandSquare() { return ( -
-

- {label} -

- +
@@ -69,7 +54,7 @@ export function OtpCard({ label, code, hint }: OtpCardProps) { style={{ margin: "14px 0 0", color: emailTheme.color.light.muted, - fontSize: "14px", + fontSize: "13px", lineHeight: 1.6, }} > diff --git a/emails/src/main.css b/emails/src/main.css index 84482d3..4999a06 100644 --- a/emails/src/main.css +++ b/emails/src/main.css @@ -1,48 +1,41 @@ .email-shell { - background: #f6f1e8; + background: #ffffff; } .email-card { - background: #fffdfa; - border: 1px solid #e7dcc9; - border-radius: 32px; - box-shadow: 0 18px 48px rgba(54, 36, 10, 0.12); + background: #ffffff; + border: 1px solid #dadce0; + border-radius: 12px; + box-shadow: none; } .email-card-secondary { - background: #f8f1e5; - border: 1px solid #e7dcc9; - border-radius: 24px; + background: #f8f9fa; + border: 1px solid #dadce0; + border-radius: 10px; } .email-title, .email-text, .meta-text, -.brand-primary, -.otp-caption { - color: #1f1912; +.brand-primary { + color: #202124; } .email-muted, .email-caption, .meta-muted, .brand-secondary { - color: #6d6458; + color: #5f6368; } .brand-accent, .accent-text, .otp-code { - color: #d4a63c; + color: #c48a12; } .accent-surface { - background: #f6e3a9; - border: 1px solid #f0d79a; -} - -.accent-pill { - background: #f3ead8; - border: 1px solid #f0d79a; - color: #34230a; + background: #ffffff; + border: 1px solid #e8eaed; } diff --git a/emails/src/templates/PasswordResetEmail.tsx b/emails/src/templates/PasswordResetEmail.tsx index 16f1465..00784a5 100644 --- a/emails/src/templates/PasswordResetEmail.tsx +++ b/emails/src/templates/PasswordResetEmail.tsx @@ -17,9 +17,8 @@ export function PasswordResetEmail({ darkModeCss, userName, otpCode }: PasswordR darkModeCss={darkModeCss} > @@ -31,8 +30,8 @@ export function PasswordResetEmail({ darkModeCss, userName, otpCode }: PasswordR style={{ margin: 0, color: emailTheme.color.light.foreground, - fontSize: "16px", - lineHeight: 1.7, + fontSize: "15px", + lineHeight: 1.6, }} > Hi {userName}, @@ -42,9 +41,8 @@ export function PasswordResetEmail({ darkModeCss, userName, otpCode }: PasswordR @@ -55,11 +53,11 @@ export function PasswordResetEmail({ darkModeCss, userName, otpCode }: PasswordR style={{ margin: 0, color: emailTheme.color.light.foreground, - fontSize: "14px", - lineHeight: 1.7, + fontSize: "13px", + lineHeight: 1.6, }} > - For security, only use the latest code you requested. + This code only starts the password reset flow. If you did not request it, ignore this message and keep your current password. For security, only use the latest code you requested.

diff --git a/emails/src/templates/RegisterConfirmationEmail.tsx b/emails/src/templates/RegisterConfirmationEmail.tsx index 52ae9b4..7336596 100644 --- a/emails/src/templates/RegisterConfirmationEmail.tsx +++ b/emails/src/templates/RegisterConfirmationEmail.tsx @@ -17,9 +17,8 @@ export function RegisterConfirmationEmail({ darkModeCss, userName, otpCode }: Re darkModeCss={darkModeCss} >
{otpCode}} - hint="Enter this passcode in the app to continue resetting your password. If you did not request this, you can ignore this email and keep your current password." + hint="Enter this code in TicketRush to continue. Do not share it with anyone." />
@@ -31,8 +30,8 @@ export function RegisterConfirmationEmail({ darkModeCss, userName, otpCode }: Re style={{ margin: 0, color: emailTheme.color.light.foreground, - fontSize: "16px", - lineHeight: 1.7, + fontSize: "15px", + lineHeight: 1.6, }} > Hi {userName}, @@ -42,9 +41,8 @@ export function RegisterConfirmationEmail({ darkModeCss, userName, otpCode }: Re @@ -55,11 +53,11 @@ export function RegisterConfirmationEmail({ darkModeCss, userName, otpCode }: Re style={{ margin: 0, color: emailTheme.color.light.foreground, - fontSize: "14px", - lineHeight: 1.7, + fontSize: "13px", + lineHeight: 1.6, }} > - Need help? Reply to this email and the TicketRush team can help you finish setup. + This code only confirms this email address. If you did not create a TicketRush account, ignore this message. Need help? Reply to this email and the TicketRush team can help you finish setup.

diff --git a/emails/src/theme/dark-mode.css b/emails/src/theme/dark-mode.css index d759e3c..fadca9f 100644 --- a/emails/src/theme/dark-mode.css +++ b/emails/src/theme/dark-mode.css @@ -1,74 +1,70 @@ @media (prefers-color-scheme: dark) { body[data-email-root="true"] { - background-color: #16120d !important; - color: #f6efe4 !important; + background-color: #000000 !important; + color: #f1f3f4 !important; } .email-shell { - background: #16120d !important; + background: #000000 !important; } .email-card { - background: #211a13 !important; - border-color: #3a2f20 !important; - box-shadow: 0 16px 44px rgba(0, 0, 0, 0.35) !important; + background: #111111 !important; + border-color: #2f3033 !important; + box-shadow: none !important; } .email-card-secondary { - background: #2a2118 !important; - border-color: #3a2f20 !important; + background: #181818 !important; + border-color: #2f3033 !important; } .email-text, .email-title, .brand-primary, - .otp-caption, .meta-text { - color: #f6efe4 !important; + color: #f1f3f4 !important; } .email-muted, .email-caption, .meta-muted, .brand-secondary { - color: #c2b3a0 !important; + color: #bdc1c6 !important; } .accent-text, - .brand-accent, - .otp-code { - color: #e3b64f !important; + .brand-accent { + color: #d6a22d !important; } - .accent-surface { - background: #553c08 !important; - border-color: #826423 !important; + .otp-code { + color: #ffffff !important; } - .accent-pill { - background: #312619 !important; - border-color: #5b4622 !important; - color: #fde7ab !important; + .accent-surface { + background: #101010 !important; + border-color: #3c4043 !important; } } [data-ogsc] body[data-email-root="true"], [data-ogsb] body[data-email-root="true"] { - background-color: #16120d !important; - color: #f6efe4 !important; + background-color: #000000 !important; + color: #f1f3f4 !important; } [data-ogsc] .email-card, [data-ogsb] .email-card { - background: #211a13 !important; - border-color: #3a2f20 !important; - box-shadow: 0 16px 44px rgba(0, 0, 0, 0.35) !important; + background: #111111 !important; + border-color: #2f3033 !important; + box-shadow: none !important; } [data-ogsc] .email-card-secondary, [data-ogsb] .email-card-secondary { - background: #2a2118 !important; - border-color: #3a2f20 !important; + background: #181818 !important; + border-color: #2f3033 !important; } [data-ogsc] .email-text, @@ -77,11 +73,9 @@ [data-ogsb] .email-title, [data-ogsc] .brand-primary, [data-ogsb] .brand-primary, -[data-ogsc] .otp-caption, -[data-ogsb] .otp-caption, [data-ogsc] .meta-text, [data-ogsb] .meta-text { - color: #f6efe4 !important; + color: #f1f3f4 !important; } [data-ogsc] .email-muted, @@ -92,27 +86,23 @@ [data-ogsb] .meta-muted, [data-ogsc] .brand-secondary, [data-ogsb] .brand-secondary { - color: #c2b3a0 !important; + color: #bdc1c6 !important; } [data-ogsc] .accent-text, [data-ogsb] .accent-text, [data-ogsc] .brand-accent, -[data-ogsb] .brand-accent, +[data-ogsb] .brand-accent { + color: #d6a22d !important; +} + [data-ogsc] .otp-code, [data-ogsb] .otp-code { - color: #e3b64f !important; + color: #ffffff !important; } [data-ogsc] .accent-surface, [data-ogsb] .accent-surface { - background: #553c08 !important; - border-color: #826423 !important; -} - -[data-ogsc] .accent-pill, -[data-ogsb] .accent-pill { - background: #312619 !important; - border-color: #5b4622 !important; - color: #fde7ab !important; + background: #101010 !important; + border-color: #3c4043 !important; } diff --git a/emails/src/theme/tokens.ts b/emails/src/theme/tokens.ts index 7e8e1dd..55c2471 100644 --- a/emails/src/theme/tokens.ts +++ b/emails/src/theme/tokens.ts @@ -1,47 +1,43 @@ export const emailTheme = { font: { - body: "Inter, Arial, Helvetica, sans-serif", - brand: "Nunito, Inter, Arial, Helvetica, sans-serif", + body: "Google Sans, Roboto, Inter, Segoe UI, Arial, Helvetica, sans-serif", + brand: "Nunito, Google Sans, Roboto, Inter, Segoe UI, Arial, Helvetica, sans-serif", + code: "Google Sans Code, Roboto Mono, SFMono-Regular, Consolas, Liberation Mono, monospace", }, color: { light: { - accent: "#d4a63c", - accentForeground: "#34230a", - background: "#f6f1e8", - border: "#e7dcc9", - card: "#fffdfa", - cardSecondary: "#f8f1e5", - foreground: "#1f1912", - muted: "#6d6458", - outline: "#f0d79a", - otpBackground: "#f6e3a9", - otpForeground: "#704d00", - pillBackground: "#f3ead8", - shadow: "rgba(54, 36, 10, 0.12)", - subtleShadow: "rgba(54, 36, 10, 0.08)", + accent: "#c48a12", + background: "#ffffff", + border: "#dadce0", + card: "#ffffff", + cardSecondary: "#f8f9fa", + foreground: "#202124", + muted: "#5f6368", + outline: "#e8eaed", + otpBackground: "#ffffff", + otpForeground: "#202124", + shadow: "none", + subtleShadow: "none", }, dark: { - accent: "#e3b64f", - accentForeground: "#281b07", - background: "#16120d", - border: "#3a2f20", - card: "#211a13", - cardSecondary: "#2a2118", - foreground: "#f6efe4", - muted: "#c2b3a0", - outline: "#5b4622", - otpBackground: "#553c08", - otpForeground: "#fde7ab", - pillBackground: "#312619", - shadow: "rgba(0, 0, 0, 0.35)", - subtleShadow: "rgba(0, 0, 0, 0.2)", + accent: "#d6a22d", + background: "#000000", + border: "#2f3033", + card: "#111111", + cardSecondary: "#181818", + foreground: "#f1f3f4", + muted: "#bdc1c6", + outline: "#3c4043", + otpBackground: "#101010", + otpForeground: "#ffffff", + shadow: "none", + subtleShadow: "none", }, }, radius: { - shell: 32, - card: 24, - badge: 999, - otp: 18, + shell: 12, + card: 10, + otp: 8, }, spacing: { shellInset: 24, diff --git a/src/main/resources/templates/password-reset-email.html b/src/main/resources/templates/password-reset-email.html index 91bb17d..ddaaa3e 100644 --- a/src/main/resources/templates/password-reset-email.html +++ b/src/main/resources/templates/password-reset-email.html @@ -1,74 +1,70 @@ Reset your TicketRush password
Reset your TicketRush password with your one-time passcode.
{otpCode}} - hint="Enter this passcode in the app to finish registration. If this was not you, you can safely ignore this email." + hint="Enter this code in TicketRush to confirm your email address." />
\ No newline at end of file +
Reset your TicketRush password with your one-time passcode.
\ No newline at end of file diff --git a/src/main/resources/templates/register-confirmation-email.html b/src/main/resources/templates/register-confirmation-email.html index e086059..d570430 100644 --- a/src/main/resources/templates/register-confirmation-email.html +++ b/src/main/resources/templates/register-confirmation-email.html @@ -1,74 +1,70 @@ Confirm your TicketRush account
Confirm your TicketRush account with your one-time passcode.
\ No newline at end of file +
Confirm your TicketRush account with your one-time passcode.
\ No newline at end of file From 6fcc1103eb28ab5235a557ae833e9581da8e5477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=E1=BA=BF=20H=C6=B0ng?= Date: Fri, 1 May 2026 05:00:43 +0700 Subject: [PATCH 11/14] fix: correct email template rendering Co-authored-by: Claude Opus 4.7 --- emails/src/components/BrandHeader.tsx | 18 +--------- emails/src/components/OtpCard.tsx | 15 +------- emails/src/templates/PasswordResetEmail.tsx | 27 +++++++++----- .../templates/RegisterConfirmationEmail.tsx | 27 +++++++++----- .../services/EmailService.java | 35 ++++++++++++------- .../templates/password-reset-email.html | 2 +- .../register-confirmation-email.html | 2 +- 7 files changed, 62 insertions(+), 64 deletions(-) diff --git a/emails/src/components/BrandHeader.tsx b/emails/src/components/BrandHeader.tsx index ccc1ffc..e5efcfb 100644 --- a/emails/src/components/BrandHeader.tsx +++ b/emails/src/components/BrandHeader.tsx @@ -2,10 +2,9 @@ import { emailTheme } from "../theme/tokens"; type BrandHeaderProps = { title: string; - body: string; }; -export function BrandHeader({ title, body }: BrandHeaderProps) { +export function BrandHeader({ title }: BrandHeaderProps) { return ( <> @@ -43,21 +42,6 @@ export function BrandHeader({ title, body }: BrandHeaderProps) { - - -
-

- {body} -

-
diff --git a/emails/src/components/OtpCard.tsx b/emails/src/components/OtpCard.tsx index 40d2db9..24bef1a 100644 --- a/emails/src/components/OtpCard.tsx +++ b/emails/src/components/OtpCard.tsx @@ -4,10 +4,9 @@ import { emailTheme } from "../theme/tokens"; type OtpCardProps = { code: ReactNode; - hint: string; }; -export function OtpCard({ code, hint }: OtpCardProps) { +export function OtpCard({ code }: OtpCardProps) { return ( - -

- {hint} -

diff --git a/emails/src/templates/PasswordResetEmail.tsx b/emails/src/templates/PasswordResetEmail.tsx index 00784a5..a2a1948 100644 --- a/emails/src/templates/PasswordResetEmail.tsx +++ b/emails/src/templates/PasswordResetEmail.tsx @@ -16,10 +16,7 @@ export function PasswordResetEmail({ darkModeCss, userName, otpCode }: PasswordR previewText="Reset your TicketRush password with your one-time passcode." darkModeCss={darkModeCss} > - +
@@ -38,12 +35,24 @@ export function PasswordResetEmail({ darkModeCss, userName, otpCode }: PasswordR

+ + + @@ -57,7 +66,7 @@ export function PasswordResetEmail({ darkModeCss, userName, otpCode }: PasswordR lineHeight: 1.6, }} > - This code only starts the password reset flow. If you did not request it, ignore this message and keep your current password. For security, only use the latest code you requested. + Do not share it with anyone, including TicketRush staff. If you did not request it, ignore this message and keep your current password.

diff --git a/emails/src/templates/RegisterConfirmationEmail.tsx b/emails/src/templates/RegisterConfirmationEmail.tsx index 7336596..ae30999 100644 --- a/emails/src/templates/RegisterConfirmationEmail.tsx +++ b/emails/src/templates/RegisterConfirmationEmail.tsx @@ -16,10 +16,7 @@ export function RegisterConfirmationEmail({ darkModeCss, userName, otpCode }: Re previewText="Confirm your TicketRush account with your one-time passcode." darkModeCss={darkModeCss} > - +
+

+ Use this code to continue resetting your password. Keep it private and enter it only in TicketRush. +

+
- {otpCode}} - hint="Enter this code in TicketRush to continue. Do not share it with anyone." - /> + {otpCode}} />
@@ -38,12 +35,24 @@ export function RegisterConfirmationEmail({ darkModeCss, userName, otpCode }: Re

+ + + @@ -57,7 +66,7 @@ export function RegisterConfirmationEmail({ darkModeCss, userName, otpCode }: Re lineHeight: 1.6, }} > - This code only confirms this email address. If you did not create a TicketRush account, ignore this message. Need help? Reply to this email and the TicketRush team can help you finish setup. + Do not share it with anyone, including TicketRush staff. If you did not create a TicketRush account, ignore this message.

diff --git a/src/main/java/me/june8th/ticketrushserver/services/EmailService.java b/src/main/java/me/june8th/ticketrushserver/services/EmailService.java index c04cee0..423c073 100644 --- a/src/main/java/me/june8th/ticketrushserver/services/EmailService.java +++ b/src/main/java/me/june8th/ticketrushserver/services/EmailService.java @@ -1,15 +1,20 @@ package me.june8th.ticketrushserver.services; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; import me.june8th.ticketrushserver.utils.Validator; import org.jspecify.annotations.NullMarked; import org.springframework.beans.factory.annotation.Value; import org.springframework.mail.MailException; -import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.MailPreparationException; import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; +import java.nio.charset.StandardCharsets; + @Service public class EmailService { @@ -38,8 +43,8 @@ public void sendRegisterConfirmationEmail(String toAddress, String userName, Str ctx.setVariable("userName", userName); ctx.setVariable("otpCode", otpCode); - String emailContent = templateEngine.process("register_confirmation_email", ctx); - sendText(toAddress, REGISTER_CONFIRMATION_SUBJECT, emailContent); + String emailContent = templateEngine.process("register-confirmation-email", ctx); + sendHtml(toAddress, REGISTER_CONFIRMATION_SUBJECT, emailContent); } @NullMarked @@ -54,19 +59,23 @@ public void sendPasswordResetEmail(String toAddress, String userName, String otp ctx.setVariable("userName", userName); ctx.setVariable("otpCode", otpCode); - String emailContent = templateEngine.process("password_reset_email", ctx); - sendText(toAddress, PASSWORD_RESET_SUBJECT, emailContent); + String emailContent = templateEngine.process("password-reset-email", ctx); + sendHtml(toAddress, PASSWORD_RESET_SUBJECT, emailContent); } @NullMarked - private void sendText(String toAddress, String subject, String otpCode) throws MailException { - SimpleMailMessage message = new SimpleMailMessage(); - message.setFrom(fromAddress); - message.setTo(toAddress); - message.setSubject(subject); - message.setText(otpCode); - mailSender.send(message); + private void sendHtml(String toAddress, String subject, String emailContent) throws MailException { + try { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, false, StandardCharsets.UTF_8.name()); + helper.setFrom(fromAddress); + helper.setTo(toAddress); + helper.setSubject(subject); + helper.setText(emailContent, true); + mailSender.send(message); + } catch (MessagingException e) { + throw new MailPreparationException("Failed to prepare email message", e); + } } } - diff --git a/src/main/resources/templates/password-reset-email.html b/src/main/resources/templates/password-reset-email.html index ddaaa3e..d7c321a 100644 --- a/src/main/resources/templates/password-reset-email.html +++ b/src/main/resources/templates/password-reset-email.html @@ -106,4 +106,4 @@ background: #101010 !important; border-color: #3c4043 !important; } -
Reset your TicketRush password with your one-time passcode.
+

+ Use this code to verify your email address and finish setting up your account. +

+
- {otpCode}} - hint="Enter this code in TicketRush to confirm your email address." - /> + {otpCode}} />
\ No newline at end of file +
Reset your TicketRush password with your one-time passcode.
\ No newline at end of file diff --git a/src/main/resources/templates/register-confirmation-email.html b/src/main/resources/templates/register-confirmation-email.html index d570430..95df441 100644 --- a/src/main/resources/templates/register-confirmation-email.html +++ b/src/main/resources/templates/register-confirmation-email.html @@ -106,4 +106,4 @@ background: #101010 !important; border-color: #3c4043 !important; } -
Confirm your TicketRush account with your one-time passcode.
\ No newline at end of file +
Confirm your TicketRush account with your one-time passcode.
\ No newline at end of file From cd58837ce00ca3b3bd4aebe8f635d8232aec0571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=E1=BA=BF=20H=C6=B0ng?= <42248228+teppyboy@users.noreply.github.com> Date: Fri, 1 May 2026 05:22:04 +0700 Subject: [PATCH 12/14] chore: update emails/src/main.css It was the leftover from the first sloppy design, shameful GPT-5.4 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- emails/src/main.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/emails/src/main.css b/emails/src/main.css index 4999a06..bc09db5 100644 --- a/emails/src/main.css +++ b/emails/src/main.css @@ -30,8 +30,7 @@ } .brand-accent, -.accent-text, -.otp-code { +.accent-text { color: #c48a12; } From 4c061cfe82de2d9175fa5982107924d1396a78fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=E1=BA=BF=20H=C6=B0ng?= <42248228+teppyboy@users.noreply.github.com> Date: Fri, 1 May 2026 05:23:14 +0700 Subject: [PATCH 13/14] chore: update emails/package.json Sure if it even works :) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- emails/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/emails/package.json b/emails/package.json index 2544130..437752f 100644 --- a/emails/package.json +++ b/emails/package.json @@ -6,7 +6,7 @@ "scripts": { "build:assets": "vite build", "render": "tsx scripts/render-emails.mts", - "build": "npm run build:assets && npm run render" + "build": "npm run render" }, "dependencies": { "juice": "^11.0.3", From 915b4ae51751885c00d81459d9322d01880e0da0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:42:47 +0000 Subject: [PATCH 14/14] fix: install Node.js LTS in CI and Docker for email build step - CI workflow: use actions/setup-node@v4 (node-version: lts/*) with npm cache for the emails workspace before the Gradle build - Dockerfile builder: install nvm v0.40.3 + Node.js LTS on Alpine via bash/curl and symlink node/npm/npx into /usr/local/bin - build.gradle: add npmInstallEmails task (npm ci) that buildEmailTemplates now depends on, so node_modules are always installed in a clean env Agent-Logs-Url: https://github.com/im-yuuki/TicketRushServer/sessions/45571202-7c05-490a-b99a-81d9bc63dfed Co-authored-by: teppyboy <42248228+teppyboy@users.noreply.github.com> --- .github/workflows/build-jar.yml | 7 +++++++ build.gradle | 10 ++++++++++ dockerfile | 11 +++++++++++ 3 files changed, 28 insertions(+) diff --git a/.github/workflows/build-jar.yml b/.github/workflows/build-jar.yml index ac2f90d..5239fe2 100644 --- a/.github/workflows/build-jar.yml +++ b/.github/workflows/build-jar.yml @@ -22,6 +22,13 @@ jobs: with: java-version: '25' + - name: Setup Node.js LTS + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + cache: 'npm' + cache-dependency-path: emails/package-lock.json + - name: Build JAR run: | chmod +x ./gradlew diff --git a/build.gradle b/build.gradle index e106e9b..7d42a14 100644 --- a/build.gradle +++ b/build.gradle @@ -57,9 +57,19 @@ tasks.withType(Test).configureEach { useJUnitPlatform() } +tasks.register('npmInstallEmails', Exec) { + group = 'build' + description = 'Installs npm dependencies for the email workspace.' + workingDir = emailsDir.asFile + commandLine npmCommand, 'ci' + inputs.file('emails/package-lock.json') + outputs.dir('emails/node_modules') +} + tasks.register('buildEmailTemplates', Exec) { group = 'build' description = 'Builds static email HTML from the React email workspace.' + dependsOn tasks.named('npmInstallEmails') workingDir = emailsDir.asFile commandLine npmCommand, 'run', 'build' diff --git a/dockerfile b/dockerfile index 12321a0..623a592 100644 --- a/dockerfile +++ b/dockerfile @@ -1,5 +1,16 @@ FROM eclipse-temurin:25-jdk-alpine AS builder +ENV NVM_DIR=/root/.nvm + +RUN apk add --no-cache bash curl && \ + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash && \ + bash -c ". \"$NVM_DIR/nvm.sh\" && \ + nvm install --lts && \ + nvm use --lts && \ + ln -sf \"\$(nvm which current)\" /usr/local/bin/node && \ + ln -sf \"\$(dirname \"\$(nvm which current)\")/npm\" /usr/local/bin/npm && \ + ln -sf \"\$(dirname \"\$(nvm which current)\")/npx\" /usr/local/bin/npx" + WORKDIR /app COPY . . RUN ./gradlew bootJar -x test --no-daemon