diff --git a/README.md b/README.md index 0d6babe..2a4ef48 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,17 @@ # React + TypeScript + Vite +## Installation + +`npm install` + +## Create a theme + +`npm run create-theme` + +## Run dev theme + +`npm run dev:` + This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. Currently, two official plugins are available: diff --git a/package-lock.json b/package-lock.json index 6eddad5..16056af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "zod": "^3.22.4" }, "devDependencies": { + "@types/chance": "^1.1.6", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", "@typescript-eslint/eslint-plugin": "^6.10.0", @@ -27,6 +28,7 @@ "husky": "^8.0.3", "lint-staged": "^15.1.0", "prettier": "^3.1.0", + "ts-node": "^10.9.2", "typescript": "^5.2.2", "vite": "^5.0.0" } @@ -398,6 +400,28 @@ "node": ">=6.9.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", @@ -1108,6 +1132,30 @@ "win32" ] }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1149,12 +1197,28 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/chance": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@types/chance/-/chance-1.1.6.tgz", + "integrity": "sha512-V+pm3stv1Mvz8fSKJJod6CglNGVqEQ6OyuqitoDkWywEODM/eJd1eSuIp9xt6DrX8BWZ2eDSIzbw1tPCUTvGbQ==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/node": { + "version": "20.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", + "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", + "dev": true, + "peer": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", @@ -1428,6 +1492,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1492,6 +1565,12 @@ "node": ">=4" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1688,6 +1767,12 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1731,6 +1816,15 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2753,6 +2847,12 @@ "yallist": "^3.0.2" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -3535,6 +3635,49 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -3577,6 +3720,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "peer": true + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -3616,6 +3766,12 @@ "punycode": "^2.1.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/vite": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.3.tgz", @@ -3763,6 +3919,15 @@ "node": ">= 14" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 8161f7d..23f82cf 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,15 @@ "name": "chat-theme", "private": true, "version": "0.0.0", - "type": "module", "scripts": { - "dev": "vite", + "create-theme": "npx ts-node --skip-project src/utils/create-theme.ts", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "prepare": "husky install", - "prettier": "prettier --write ." + "prettier": "prettier --write .", + "dev:gummybear": "cd src/themes/gummybear && npx vite", + "dev:example": "cd src/themes/example && npx vite" }, "dependencies": { "chance": "^1.1.11", @@ -20,6 +21,7 @@ "zod": "^3.22.4" }, "devDependencies": { + "@types/chance": "^1.1.6", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", "@typescript-eslint/eslint-plugin": "^6.10.0", @@ -31,6 +33,7 @@ "husky": "^8.0.3", "lint-staged": "^15.1.0", "prettier": "^3.1.0", + "ts-node": "^10.9.2", "typescript": "^5.2.2", "vite": "^5.0.0" }, diff --git a/src/app.tsx b/src/app.tsx deleted file mode 100644 index 5abbf29..0000000 --- a/src/app.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { ChatDemo } from './components/chat-demo' -import { Container } from './theme/container' -import { themeSettings } from './theme/settings' -import { generateTwitchMessage } from './utils/generate-chat-message' - -export function App() { - return ( -
-
- -
-
- -
-
- ) -} diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx new file mode 100644 index 0000000..1363962 --- /dev/null +++ b/src/components/Settings.tsx @@ -0,0 +1,20 @@ +import React from 'react' + +export function Settings() { + return ( +
+ +
+ ) +} diff --git a/src/components/chat-demo.tsx b/src/components/chat-demo.tsx deleted file mode 100644 index fead5bc..0000000 --- a/src/components/chat-demo.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useEffect, useState } from 'react' -import { Container } from '../theme/container' -import { generateTwitchMessage } from '../utils/generate-chat-message' -import { TwitchMessage } from '../types' -import { themeSettings } from '../theme/settings' - -export function ChatDemo() { - const [messages, setMessages] = useState([]) - - useEffect(() => { - setMessages((d) => { - if (d.length >= 50) d.shift() - const newMessage = generateTwitchMessage('twitch') - return [...d, newMessage] as TwitchMessage[] - }) - - const interval = setInterval(() => { - setMessages((d) => { - if (d.length >= 50) d.shift() - const newMessage = generateTwitchMessage('twitch') - return [...d, newMessage] as TwitchMessage[] - }) - }, 1250) - - return () => { - clearInterval(interval) - } - }, []) - - return ( -
- -
- ) -} diff --git a/src/index.css b/src/index.css index 12a1f40..3588201 100644 --- a/src/index.css +++ b/src/index.css @@ -132,6 +132,13 @@ main { align-items: center; justify-content: center; height: 100vh; + background-color: #ffffff; + transition: background-color 0.3s ease; +} + +main.dark-mode { + background-color: #212328; + color: white; } .message-presentation { @@ -142,13 +149,25 @@ main { flex: 1; } -.message-demo { +.chat-demo { flex: 1; height: 100dvh; background-color: #f5f5f5; - padding: 20px; + transition: background-color 0.3s ease; } -.chat-demo { - height: 100%; +.chat-demo.dark-mode { + background-color: #17191d; +} + +.settings { + position: absolute; + top: 20px; + left: 20px; +} + +.theme-toggle { + font-family: sans-serif; + cursor: pointer; + user-select: none; } diff --git a/src/theme/settings.ts b/src/theme/settings.ts deleted file mode 100644 index 691e320..0000000 --- a/src/theme/settings.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ChatSettings } from '../types' - -export type CustomSettings = {} - -export const themeSettings: ChatSettings & CustomSettings = { - isDemo: true, - alignment: 'left', - fontSize: 16, - scrollAnimation: true, - animation: true, -} diff --git a/src/theme/style.css b/src/theme/style.css deleted file mode 100644 index d715451..0000000 --- a/src/theme/style.css +++ /dev/null @@ -1,69 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap'); - -.container { - display: flex; - flex: 1; - flex-direction: column; - align-items: flex-end; - justify-content: flex-end; - overflow: hidden; - gap: 2em; - height: 100%; -} - -.message { - width: 100%; - position: relative; -} - -.message__inner { - width: 100%; - display: flex; - flex-direction: column; - gap: 0.5em; - font-family: 'Inter', sans-serif; - position: relative; -} - -.message__username { - padding: 0 0.3em; - height: 1.9em; - border-radius: 0.2em; - display: inline-flex; - align-items: center; - font-weight: 700; - background-color: white; - width: fit-content; - border: 0.1em solid black; - gap: 0.2em; -} - -.message__username--badges { - display: inline-flex; - align-items: center; - gap: 0.15em; -} - -.message__username--badges img { - width: 1.1em; - height: 1.1em; - display: block; - flex-shrink: 0; -} - -.message__content { - line-height: 1.3; - padding: 0.3em 0.5em 0.2em 0.5em; - border-radius: 0.2em; - vertical-align: top; - overflow-wrap: break-word; - background: white; - width: fit-content; - border: 0.1em solid black; - display: inline; -} - -.message__content img { - width: 1.25em; - display: inline-flex; -} diff --git a/src/themes/example/app.tsx b/src/themes/example/app.tsx new file mode 100644 index 0000000..3da0094 --- /dev/null +++ b/src/themes/example/app.tsx @@ -0,0 +1,60 @@ +import React, { useEffect, useState } from 'react' +import { Settings } from '../../components/Settings' +import { TwitchMessage } from '../../types' +import { generateTwitchMessage } from '../../utils/generate-chat-message' +import { Container } from './theme/container' +import { themeSettings } from './theme/settings' + +function ChatDemo({ darkMode }: { darkMode?: boolean }) { + const [messages, setMessages] = useState([]) + + useEffect(() => { + setMessages((d) => { + if (d.length >= 50) d.shift() + const newMessage = generateTwitchMessage('twitch') + return [...d, newMessage] as TwitchMessage[] + }) + + const interval = setInterval(() => { + setMessages((d) => { + if (d.length >= 50) d.shift() + const newMessage = generateTwitchMessage('twitch') + return [...d, newMessage] as TwitchMessage[] + }) + }, 1250) + + return () => { + clearInterval(interval) + } + }, []) + + return ( +
+ +
+ ) +} + +export function App() { + const [darkMode, setDarkMode] = useState(false) + + useEffect(() => { + window.addEventListener('dark-mode', (event) => { + const darkMode: boolean = (event as CustomEvent).detail + setDarkMode(darkMode) + }) + }, []) + + return ( +
+ +
+ +
+ +
+ ) +} diff --git a/public/badges/admin.png b/src/themes/example/badges/admin.png similarity index 100% rename from public/badges/admin.png rename to src/themes/example/badges/admin.png diff --git a/public/badges/artist.png b/src/themes/example/badges/artist.png similarity index 100% rename from public/badges/artist.png rename to src/themes/example/badges/artist.png diff --git a/public/badges/broadcaster.png b/src/themes/example/badges/broadcaster.png similarity index 100% rename from public/badges/broadcaster.png rename to src/themes/example/badges/broadcaster.png diff --git a/public/badges/moderator.png b/src/themes/example/badges/moderator.png similarity index 100% rename from public/badges/moderator.png rename to src/themes/example/badges/moderator.png diff --git a/public/badges/partner.png b/src/themes/example/badges/partner.png similarity index 100% rename from public/badges/partner.png rename to src/themes/example/badges/partner.png diff --git a/public/badges/prime.png b/src/themes/example/badges/prime.png similarity index 100% rename from public/badges/prime.png rename to src/themes/example/badges/prime.png diff --git a/public/badges/staff.png b/src/themes/example/badges/staff.png similarity index 100% rename from public/badges/staff.png rename to src/themes/example/badges/staff.png diff --git a/public/badges/turbo.png b/src/themes/example/badges/turbo.png similarity index 100% rename from public/badges/turbo.png rename to src/themes/example/badges/turbo.png diff --git a/public/badges/vip.png b/src/themes/example/badges/vip.png similarity index 100% rename from public/badges/vip.png rename to src/themes/example/badges/vip.png diff --git a/index.html b/src/themes/example/index.html similarity index 84% rename from index.html rename to src/themes/example/index.html index e4b78ea..fa0d4de 100644 --- a/index.html +++ b/src/themes/example/index.html @@ -6,8 +6,9 @@ Vite + React + TS +
- + diff --git a/src/main.tsx b/src/themes/example/main.tsx similarity index 89% rename from src/main.tsx rename to src/themes/example/main.tsx index 1eafaef..11accaa 100644 --- a/src/main.tsx +++ b/src/themes/example/main.tsx @@ -1,6 +1,6 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import './index.css' +import '../../index.css' import { App } from './app' ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/src/theme/container.tsx b/src/themes/example/theme/container.tsx similarity index 70% rename from src/theme/container.tsx rename to src/themes/example/theme/container.tsx index 26b51f7..55edb84 100644 --- a/src/theme/container.tsx +++ b/src/themes/example/theme/container.tsx @@ -1,8 +1,9 @@ import { AnimatePresence } from 'framer-motion' -import type { ChatSettings, TwitchMessage } from '../types' +import React from 'react' +import { ChatSettings, TwitchMessage } from '../../../types' import { Message } from './message' -import './style.css' import { CustomSettings } from './settings' +import './style.css' type Props = { messages: TwitchMessage[] @@ -13,13 +14,7 @@ export function Container(props: Props) { const { messages, settings } = props return ( -
+
{messages.map((message) => ( diff --git a/src/theme/message.tsx b/src/themes/example/theme/message.tsx similarity index 51% rename from src/theme/message.tsx rename to src/themes/example/theme/message.tsx index bd27890..c4f95a8 100644 --- a/src/theme/message.tsx +++ b/src/themes/example/theme/message.tsx @@ -1,5 +1,6 @@ -import { ChatSettings, TwitchMessage } from '../types' -import { motion } from 'framer-motion' +import { Variants, motion } from 'framer-motion' +import React from 'react' +import { ChatSettings, TwitchMessage } from '../../../types' import { CustomSettings } from './settings' type Props = { @@ -10,25 +11,7 @@ type Props = { export function Message(props: Props) { const { message, settings } = props - const hideAnimation = { - initial: { - opacity: 1, - }, - in: { - opacity: 0, - transition: { - duration: 0.3, - delay: - settings?.hideTime === 0 || !settings?.hideTime - ? 100000 - : settings?.hideTime, - ease: 'easeInOut', - type: 'spring', - }, - }, - } - - const displayAnimation = { + const displayAnimation: Variants = { initial: { [settings?.alignment === 'left' ? 'right' : 'left']: 50, }, @@ -47,38 +30,30 @@ export function Message(props: Props) { return (
-
- {message.badges.map((badge) => ( - {badge} - ))} -
-

{message.username}

+ {settings?.badges && ( +
+ {message.badges.map((badge) => ( + {badge} + ))} +
+ )} +
{message.username}
-
+ />
) diff --git a/src/themes/example/theme/settings.ts b/src/themes/example/theme/settings.ts new file mode 100644 index 0000000..1898355 --- /dev/null +++ b/src/themes/example/theme/settings.ts @@ -0,0 +1,16 @@ +import { ChatSettings } from '../../../types' + +export type CustomSettings = { + messageRotationRadius: number +} + +export const themeSettings: ChatSettings & CustomSettings = { + isDemo: true, + fontSize: 16, + alignment: 'left', + scrollAnimation: true, + animation: true, + badges: true, + // Custom settings + messageRotationRadius: 0, +} diff --git a/src/themes/example/theme/style.css b/src/themes/example/theme/style.css new file mode 100644 index 0000000..00ea1cf --- /dev/null +++ b/src/themes/example/theme/style.css @@ -0,0 +1,63 @@ +@import url('https://fonts.googleapis.com/css2?family=Varela+Round&display=swap'); + +.container { + display: flex; + flex: 1; + flex-direction: column; + align-items: flex-start; + justify-content: flex-end; + overflow: hidden; + height: 100%; + gap: 0.5em; + padding: 2em; + color: rgb(0 0 0); +} + +.message__inner { + display: flex; + flex-direction: column; + font-family: 'Varela Round', sans-serif; + position: relative; + font-size: 0.95em; + align-items: flex-start; + padding: 1em; + background-color: #fff; + border-radius: 0.6em; + box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.1); + gap: 0.5em; +} + +.message__username { + border-radius: 1em; + display: inline-flex; + position: relative; + font-weight: bold; + display: flex; + gap: 0.5em; +} + +.message__username--badges { + display: inline-flex; + align-items: center; + gap: 0.2em; +} + +.message__username--badges img { + --size: 0.9em; + width: var(--size); + height: var(--size); + display: block; + border-radius: 10em; +} + +.message__content { + line-height: 1.3; + position: relative; + color: rgb(0 0 0 / 80%); +} + +.message__content img { + width: 1em; + display: inline-flex; + position: relative; +} diff --git a/vite.config.ts b/src/themes/example/vite.config.ts similarity index 100% rename from vite.config.ts rename to src/themes/example/vite.config.ts diff --git a/src/utils/create-theme.ts b/src/utils/create-theme.ts new file mode 100644 index 0000000..2a65a3b --- /dev/null +++ b/src/utils/create-theme.ts @@ -0,0 +1,45 @@ +import { exec } from 'node:child_process' +import * as fs from 'node:fs' +import * as readline from 'node:readline' + +// ask for theme name +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}) + +rl.question('New theme name: ', (themeName) => { + const newThemePath = `src/themes/${themeName}` + + // create new theme folder + fs.mkdir(newThemePath, { recursive: true }, (err) => { + if (err) throw err + }) + + // copy template files from src/gummybear to src/themes/themeName + fs.cp( + 'src/utils/theme-template', + `${newThemePath}`, + { recursive: true }, + (err) => { + if (err) throw err + } + ) + + // run npm command to add a script for the new theme + + exec( + `npm pkg set scripts.dev:${themeName}="cd src/themes/${themeName} && npx vite"`, + (err, stdout) => { + if (err) { + console.error(err) + return + } + console.log(stdout) + } + ) + + console.log(`New theme '${themeName}' created, happy theming !`) + + rl.close() +}) diff --git a/src/utils/generate-chat-message.ts b/src/utils/generate-chat-message.ts index 93d9e27..f75d1a1 100644 --- a/src/utils/generate-chat-message.ts +++ b/src/utils/generate-chat-message.ts @@ -2,7 +2,7 @@ import chance from 'chance' import { parse } from 'simple-tmi-emotes' import { TwitchMessage } from '../types' -const kBadgeWhitelist = [ +const kBadgeWhitelist: string[] = [ 'admin', 'broadcaster', 'vip', @@ -10,9 +10,12 @@ const kBadgeWhitelist = [ 'partner', 'artist', ] -const kBadgeWhiteListKick = ['moderator', 'verified', 'vip'] +const kBadgeWhiteListKick: string[] = ['moderator', 'verified', 'vip'] -const exampleMessages = [ +const exampleMessages: { + message: string + emotes?: Record +}[] = [ { message: ':):)', emotes: { @@ -97,7 +100,7 @@ const exampleMessages = [ }, ] -const exampleUsernames = [ +const exampleUsernames: string[] = [ 'xX_d4rK_1337_Xx', 'John', 'willtraore', @@ -110,7 +113,7 @@ const exampleUsernames = [ 'romainlanz', ] -const exampleColors = [ +const exampleColors: string[] = [ '#15e64c', '#15b8e6', '#151ce6', @@ -123,12 +126,12 @@ const exampleColors = [ '#e66f15', ] -function randomizeBadges(provider: string) { - const badgeWhitelist = +function randomizeBadges(provider: string): string[] { + const badgeWhitelist: string[] = provider === 'twitch' ? kBadgeWhitelist : kBadgeWhiteListKick - const badges = new Set() - const badgesCount = chance().integer({ min: 0, max: 3 }) + const badges = new Set() + const badgesCount: number = chance().integer({ min: 0, max: 3 }) for (let i = 0; i < badgesCount; i++) { badges.add( @@ -151,13 +154,12 @@ export function generateTwitchMessage(provider = 'twitch'): TwitchMessage { return { username, - emotes, + emotes: emotes ?? {}, color, badges, id: chance().guid(), twitch: chance().guid(), - date: new Date(), - // @ts-ignore + date: new Date().toDateString(), message: parse(message, emotes ?? {}, { format: 'default', themeMode: 'light', diff --git a/src/utils/theme-template/app.tsx b/src/utils/theme-template/app.tsx new file mode 100644 index 0000000..3da0094 --- /dev/null +++ b/src/utils/theme-template/app.tsx @@ -0,0 +1,60 @@ +import React, { useEffect, useState } from 'react' +import { Settings } from '../../components/Settings' +import { TwitchMessage } from '../../types' +import { generateTwitchMessage } from '../../utils/generate-chat-message' +import { Container } from './theme/container' +import { themeSettings } from './theme/settings' + +function ChatDemo({ darkMode }: { darkMode?: boolean }) { + const [messages, setMessages] = useState([]) + + useEffect(() => { + setMessages((d) => { + if (d.length >= 50) d.shift() + const newMessage = generateTwitchMessage('twitch') + return [...d, newMessage] as TwitchMessage[] + }) + + const interval = setInterval(() => { + setMessages((d) => { + if (d.length >= 50) d.shift() + const newMessage = generateTwitchMessage('twitch') + return [...d, newMessage] as TwitchMessage[] + }) + }, 1250) + + return () => { + clearInterval(interval) + } + }, []) + + return ( +
+ +
+ ) +} + +export function App() { + const [darkMode, setDarkMode] = useState(false) + + useEffect(() => { + window.addEventListener('dark-mode', (event) => { + const darkMode: boolean = (event as CustomEvent).detail + setDarkMode(darkMode) + }) + }, []) + + return ( +
+ +
+ +
+ +
+ ) +} diff --git a/src/utils/theme-template/badges/admin.png b/src/utils/theme-template/badges/admin.png new file mode 100644 index 0000000..a5dcb13 Binary files /dev/null and b/src/utils/theme-template/badges/admin.png differ diff --git a/src/utils/theme-template/badges/artist.png b/src/utils/theme-template/badges/artist.png new file mode 100644 index 0000000..3de1e70 Binary files /dev/null and b/src/utils/theme-template/badges/artist.png differ diff --git a/src/utils/theme-template/badges/broadcaster.png b/src/utils/theme-template/badges/broadcaster.png new file mode 100644 index 0000000..ee1c18c Binary files /dev/null and b/src/utils/theme-template/badges/broadcaster.png differ diff --git a/src/utils/theme-template/badges/moderator.png b/src/utils/theme-template/badges/moderator.png new file mode 100644 index 0000000..b418088 Binary files /dev/null and b/src/utils/theme-template/badges/moderator.png differ diff --git a/src/utils/theme-template/badges/partner.png b/src/utils/theme-template/badges/partner.png new file mode 100644 index 0000000..582f7db Binary files /dev/null and b/src/utils/theme-template/badges/partner.png differ diff --git a/src/utils/theme-template/badges/prime.png b/src/utils/theme-template/badges/prime.png new file mode 100644 index 0000000..21e442b Binary files /dev/null and b/src/utils/theme-template/badges/prime.png differ diff --git a/src/utils/theme-template/badges/staff.png b/src/utils/theme-template/badges/staff.png new file mode 100644 index 0000000..d2d2574 Binary files /dev/null and b/src/utils/theme-template/badges/staff.png differ diff --git a/src/utils/theme-template/badges/turbo.png b/src/utils/theme-template/badges/turbo.png new file mode 100644 index 0000000..12bc1bd Binary files /dev/null and b/src/utils/theme-template/badges/turbo.png differ diff --git a/src/utils/theme-template/badges/vip.png b/src/utils/theme-template/badges/vip.png new file mode 100644 index 0000000..43b18b1 Binary files /dev/null and b/src/utils/theme-template/badges/vip.png differ diff --git a/src/utils/theme-template/index.html b/src/utils/theme-template/index.html new file mode 100644 index 0000000..fa0d4de --- /dev/null +++ b/src/utils/theme-template/index.html @@ -0,0 +1,14 @@ + + + + + + + Vite + React + TS + + + +
+ + + diff --git a/src/utils/theme-template/main.tsx b/src/utils/theme-template/main.tsx new file mode 100644 index 0000000..11accaa --- /dev/null +++ b/src/utils/theme-template/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import '../../index.css' +import { App } from './app' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +) diff --git a/src/utils/theme-template/theme/container.tsx b/src/utils/theme-template/theme/container.tsx new file mode 100644 index 0000000..55edb84 --- /dev/null +++ b/src/utils/theme-template/theme/container.tsx @@ -0,0 +1,25 @@ +import { AnimatePresence } from 'framer-motion' +import React from 'react' +import { ChatSettings, TwitchMessage } from '../../../types' +import { Message } from './message' +import { CustomSettings } from './settings' +import './style.css' + +type Props = { + messages: TwitchMessage[] + settings: ChatSettings & CustomSettings +} + +export function Container(props: Props) { + const { messages, settings } = props + + return ( +
+ {messages.map((message) => ( + + + + ))} +
+ ) +} diff --git a/src/utils/theme-template/theme/message.tsx b/src/utils/theme-template/theme/message.tsx new file mode 100644 index 0000000..c4f95a8 --- /dev/null +++ b/src/utils/theme-template/theme/message.tsx @@ -0,0 +1,60 @@ +import { Variants, motion } from 'framer-motion' +import React from 'react' +import { ChatSettings, TwitchMessage } from '../../../types' +import { CustomSettings } from './settings' + +type Props = { + message: TwitchMessage + settings: ChatSettings & CustomSettings +} + +export function Message(props: Props) { + const { message, settings } = props + + const displayAnimation: Variants = { + initial: { + [settings?.alignment === 'left' ? 'right' : 'left']: 50, + }, + in: { + [settings?.alignment === 'left' ? 'right' : 'left']: 0, + transition: { + duration: 1, + ease: 'easeInOut', + type: 'spring', + stiffness: 260, + damping: 20, + }, + }, + } + + return ( + + +
+ {settings?.badges && ( +
+ {message.badges.map((badge) => ( + {badge} + ))} +
+ )} +
{message.username}
+
+
+ + + ) +} diff --git a/src/utils/theme-template/theme/settings.ts b/src/utils/theme-template/theme/settings.ts new file mode 100644 index 0000000..1898355 --- /dev/null +++ b/src/utils/theme-template/theme/settings.ts @@ -0,0 +1,16 @@ +import { ChatSettings } from '../../../types' + +export type CustomSettings = { + messageRotationRadius: number +} + +export const themeSettings: ChatSettings & CustomSettings = { + isDemo: true, + fontSize: 16, + alignment: 'left', + scrollAnimation: true, + animation: true, + badges: true, + // Custom settings + messageRotationRadius: 0, +} diff --git a/src/utils/theme-template/theme/style.css b/src/utils/theme-template/theme/style.css new file mode 100644 index 0000000..00ea1cf --- /dev/null +++ b/src/utils/theme-template/theme/style.css @@ -0,0 +1,63 @@ +@import url('https://fonts.googleapis.com/css2?family=Varela+Round&display=swap'); + +.container { + display: flex; + flex: 1; + flex-direction: column; + align-items: flex-start; + justify-content: flex-end; + overflow: hidden; + height: 100%; + gap: 0.5em; + padding: 2em; + color: rgb(0 0 0); +} + +.message__inner { + display: flex; + flex-direction: column; + font-family: 'Varela Round', sans-serif; + position: relative; + font-size: 0.95em; + align-items: flex-start; + padding: 1em; + background-color: #fff; + border-radius: 0.6em; + box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.1); + gap: 0.5em; +} + +.message__username { + border-radius: 1em; + display: inline-flex; + position: relative; + font-weight: bold; + display: flex; + gap: 0.5em; +} + +.message__username--badges { + display: inline-flex; + align-items: center; + gap: 0.2em; +} + +.message__username--badges img { + --size: 0.9em; + width: var(--size); + height: var(--size); + display: block; + border-radius: 10em; +} + +.message__content { + line-height: 1.3; + position: relative; + color: rgb(0 0 0 / 80%); +} + +.message__content img { + width: 1em; + display: inline-flex; + position: relative; +} diff --git a/src/utils/theme-template/vite.config.ts b/src/utils/theme-template/vite.config.ts new file mode 100644 index 0000000..5a33944 --- /dev/null +++ b/src/utils/theme-template/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/tsconfig.json b/tsconfig.json index a7fc6fb..dab3ea8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, @@ -20,6 +20,6 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": ["src"], + "include": ["src", "vite-env.d.ts", "src/types.ts"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/tsconfig.node.json b/tsconfig.node.json index 42872c5..ee6346c 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,10 +1,13 @@ { "compilerOptions": { + "jsx": "react", "composite": true, "skipLibCheck": true, "module": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], "moduleResolution": "bundler", + "esModuleInterop": true, "allowSyntheticDefaultImports": true }, - "include": ["vite.config.ts"] + "include": ["src", "src/types.ts"] } diff --git a/src/vite-env.d.ts b/vite-env.d.ts similarity index 100% rename from src/vite-env.d.ts rename to vite-env.d.ts