├── .eslintrc.cjs ├── .gitattributes ├── .gitignore ├── README.md ├── app ├── entry.client.tsx ├── entry.server.tsx ├── root.tsx ├── routes │ └── _index.tsx └── tailwind.css ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is intended to be a basic starting point for linting in your app. 3 | * It relies on recommended configs out of the box for simplicity, but you can 4 | * and should modify this configuration to best suit your team's needs. 5 | */ 6 | 7 | /** @type {import('eslint').Linter.Config} */ 8 | module.exports = { 9 | root: true, 10 | parserOptions: { 11 | ecmaVersion: "latest", 12 | sourceType: "module", 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | }, 17 | env: { 18 | browser: true, 19 | commonjs: true, 20 | es6: true, 21 | }, 22 | 23 | // Base config 24 | extends: ["eslint:recommended"], 25 | 26 | overrides: [ 27 | // React 28 | { 29 | files: ["**/*.{js,jsx,ts,tsx}"], 30 | plugins: ["react", "jsx-a11y"], 31 | extends: [ 32 | "plugin:react/recommended", 33 | "plugin:react/jsx-runtime", 34 | "plugin:react-hooks/recommended", 35 | "plugin:jsx-a11y/recommended", 36 | ], 37 | settings: { 38 | react: { 39 | version: "detect", 40 | }, 41 | formComponents: ["Form"], 42 | linkComponents: [ 43 | { name: "Link", linkAttribute: "to" }, 44 | { name: "NavLink", linkAttribute: "to" }, 45 | ], 46 | "import/resolver": { 47 | typescript: {}, 48 | }, 49 | }, 50 | }, 51 | 52 | // Typescript 53 | { 54 | files: ["**/*.{ts,tsx}"], 55 | plugins: ["@typescript-eslint", "import"], 56 | parser: "@typescript-eslint/parser", 57 | settings: { 58 | "import/internal-regex": "^~/", 59 | "import/resolver": { 60 | node: { 61 | extensions: [".ts", ".tsx"], 62 | }, 63 | typescript: { 64 | alwaysTryTypes: true, 65 | }, 66 | }, 67 | }, 68 | extends: [ 69 | "plugin:@typescript-eslint/recommended", 70 | "plugin:import/recommended", 71 | "plugin:import/typescript", 72 | ], 73 | }, 74 | 75 | // Node 76 | { 77 | files: [".eslintrc.cjs"], 78 | env: { 79 | node: true, 80 | }, 81 | }, 82 | ], 83 | }; 84 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | .env 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remix + Vite + Tailwind v4 + Open Props! 2 | 3 | I'm sharing my local sandbox for integrating Open Props and Tailwind v4 in this [tailwind.css](https://github.com/argyleink/twop/blob/main/app/tailwind.css) file. 4 | 5 |


6 | 7 | 📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/future/vite) for details on supported features. 8 | 9 | ## Development 10 | 11 | Run the Vite dev server: 12 | 13 | ```shellscript 14 | npm run dev 15 | ``` 16 | 17 | ## Deployment 18 | 19 | First, build your app for production: 20 | 21 | ```sh 22 | npm run build 23 | ``` 24 | 25 | Then run the app in production mode: 26 | 27 | ```sh 28 | npm start 29 | ``` 30 | 31 | Now you'll need to pick a host to deploy it to. 32 | 33 | ### DIY 34 | 35 | If you're familiar with deploying Node applications, the built-in Remix app server is production-ready. 36 | 37 | Make sure to deploy the output of `npm run build` 38 | 39 | - `build/server` 40 | - `build/client` 41 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle hydrating your app on the client for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.client 5 | */ 6 | 7 | import { RemixBrowser } from "@remix-run/react"; 8 | import { startTransition, StrictMode } from "react"; 9 | import { hydrateRoot } from "react-dom/client"; 10 | 11 | startTransition(() => { 12 | hydrateRoot( 13 | document, 14 | 15 | 16 | 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle generating the HTTP Response for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.server 5 | */ 6 | 7 | import { PassThrough } from "node:stream"; 8 | 9 | import type { AppLoadContext, EntryContext } from "@remix-run/node"; 10 | import { createReadableStreamFromReadable } from "@remix-run/node"; 11 | import { RemixServer } from "@remix-run/react"; 12 | import { isbot } from "isbot"; 13 | import { renderToPipeableStream } from "react-dom/server"; 14 | 15 | const ABORT_DELAY = 5_000; 16 | 17 | export default function handleRequest( 18 | request: Request, 19 | responseStatusCode: number, 20 | responseHeaders: Headers, 21 | remixContext: EntryContext, 22 | // This is ignored so we can keep it in the template for visibility. Feel 23 | // free to delete this parameter in your app if you're not using it! 24 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 25 | loadContext: AppLoadContext 26 | ) { 27 | return isbot(request.headers.get("user-agent") || "") 28 | ? handleBotRequest( 29 | request, 30 | responseStatusCode, 31 | responseHeaders, 32 | remixContext 33 | ) 34 | : handleBrowserRequest( 35 | request, 36 | responseStatusCode, 37 | responseHeaders, 38 | remixContext 39 | ); 40 | } 41 | 42 | function handleBotRequest( 43 | request: Request, 44 | responseStatusCode: number, 45 | responseHeaders: Headers, 46 | remixContext: EntryContext 47 | ) { 48 | return new Promise((resolve, reject) => { 49 | let shellRendered = false; 50 | const { pipe, abort } = renderToPipeableStream( 51 | , 56 | { 57 | onAllReady() { 58 | shellRendered = true; 59 | const body = new PassThrough(); 60 | const stream = createReadableStreamFromReadable(body); 61 | 62 | responseHeaders.set("Content-Type", "text/html"); 63 | 64 | resolve( 65 | new Response(stream, { 66 | headers: responseHeaders, 67 | status: responseStatusCode, 68 | }) 69 | ); 70 | 71 | pipe(body); 72 | }, 73 | onShellError(error: unknown) { 74 | reject(error); 75 | }, 76 | onError(error: unknown) { 77 | responseStatusCode = 500; 78 | // Log streaming rendering errors from inside the shell. Don't log 79 | // errors encountered during initial shell rendering since they'll 80 | // reject and get logged in handleDocumentRequest. 81 | if (shellRendered) { 82 | console.error(error); 83 | } 84 | }, 85 | } 86 | ); 87 | 88 | setTimeout(abort, ABORT_DELAY); 89 | }); 90 | } 91 | 92 | function handleBrowserRequest( 93 | request: Request, 94 | responseStatusCode: number, 95 | responseHeaders: Headers, 96 | remixContext: EntryContext 97 | ) { 98 | return new Promise((resolve, reject) => { 99 | let shellRendered = false; 100 | const { pipe, abort } = renderToPipeableStream( 101 | , 106 | { 107 | onShellReady() { 108 | shellRendered = true; 109 | const body = new PassThrough(); 110 | const stream = createReadableStreamFromReadable(body); 111 | 112 | responseHeaders.set("Content-Type", "text/html"); 113 | 114 | resolve( 115 | new Response(stream, { 116 | headers: responseHeaders, 117 | status: responseStatusCode, 118 | }) 119 | ); 120 | 121 | pipe(body); 122 | }, 123 | onShellError(error: unknown) { 124 | reject(error); 125 | }, 126 | onError(error: unknown) { 127 | responseStatusCode = 500; 128 | // Log streaming rendering errors from inside the shell. Don't log 129 | // errors encountered during initial shell rendering since they'll 130 | // reject and get logged in handleDocumentRequest. 131 | if (shellRendered) { 132 | console.error(error); 133 | } 134 | }, 135 | } 136 | ); 137 | 138 | setTimeout(abort, ABORT_DELAY); 139 | }); 140 | } 141 | -------------------------------------------------------------------------------- /app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Links, 3 | Meta, 4 | Outlet, 5 | Scripts, 6 | ScrollRestoration, 7 | } from "@remix-run/react"; 8 | 9 | import stylesheet from "~/tailwind.css"; 10 | 11 | export const links: LinksFunction = () => [ 12 | { rel: "stylesheet", href: stylesheet }, 13 | ]; 14 | 15 | export function Layout({ children }: { children: React.ReactNode }) { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {children} 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | export default function App() { 34 | return ; 35 | } 36 | -------------------------------------------------------------------------------- /app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | import type { MetaFunction } from "@remix-run/node"; 2 | 3 | export const meta: MetaFunction = () => { 4 | return [ 5 | { title: "New TWOP App" }, 6 | { name: "description", content: "Welcome to Remix!" }, 7 | ]; 8 | }; 9 | 10 | export default function Index() { 11 | return ( 12 |
13 |
14 |
15 |

Remix | Tailwind | Open Props

16 |

TWOP

17 |
18 | 50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /app/tailwind.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "open-props/style"; 3 | 4 | @theme { 5 | --default-font-family: var(--font-sans); 6 | 7 | --font-size-*: initial; 8 | --font-size-00: var(--font-size-00); 9 | --font-size-0: var(--font-size-0); 10 | --font-size-1: var(--font-size-1); 11 | --font-size-2: var(--font-size-2); 12 | --font-size-3: var(--font-size-3); 13 | --font-size-4: var(--font-size-4); 14 | --font-size-5: var(--font-size-5); 15 | --font-size-6: var(--font-size-6); 16 | --font-size-7: var(--font-size-7); 17 | --font-size-8: var(--font-size-8); 18 | 19 | --shadow-*: initial; 20 | --shadow-1: var(--shadow-1); 21 | --shadow-2: var(--shadow-2); 22 | --shadow-3: var(--shadow-3); 23 | --shadow-4: var(--shadow-4); 24 | --shadow-5: var(--shadow-5); 25 | --shadow-6: var(--shadow-6); 26 | 27 | --inset-shadow-*: initial; 28 | --inset-shadow-1: var(--inner-shadow-1); 29 | --inset-shadow-2: var(--inner-shadow-2); 30 | --inset-shadow-3: var(--inner-shadow-3); 31 | --inset-shadow-4: var(--inner-shadow-4); 32 | --inset-shadow-5: var(--inner-shadow-5); 33 | 34 | --transition-timing-function-*: initial; 35 | --transition-timing-function-1: var(--ease-1); 36 | --transition-timing-function-2: var(--ease-2); 37 | --transition-timing-function-3: var(--ease-3); 38 | --transition-timing-function-4: var(--ease-4); 39 | --transition-timing-function-5: var(--ease-5); 40 | --transition-timing-function-in-1: var(--ease-in-1); 41 | --transition-timing-function-in-2: var(--ease-in-2); 42 | --transition-timing-function-in-3: var(--ease-in-3); 43 | --transition-timing-function-in-4: var(--ease-in-4); 44 | --transition-timing-function-in-5: var(--ease-in-5); 45 | --transition-timing-function-out-1: var(--ease-out-1); 46 | --transition-timing-function-out-2: var(--ease-out-2); 47 | --transition-timing-function-out-3: var(--ease-out-3); 48 | --transition-timing-function-out-4: var(--ease-out-4); 49 | --transition-timing-function-out-5: var(--ease-out-5); 50 | --transition-timing-function-in-out-1: var(--ease-in-out-1); 51 | --transition-timing-function-in-out-2: var(--ease-in-out-2); 52 | --transition-timing-function-in-out-3: var(--ease-in-out-3); 53 | --transition-timing-function-in-out-4: var(--ease-in-out-4); 54 | --transition-timing-function-in-out-5: var(--ease-in-out-5); 55 | --transition-timing-function-elastic-out-1: var(--ease-elastic-out-1); 56 | --transition-timing-function-elastic-out-2: var(--ease-elastic-out-2); 57 | --transition-timing-function-elastic-out-3: var(--ease-elastic-out-3); 58 | --transition-timing-function-elastic-out-4: var(--ease-elastic-out-4); 59 | --transition-timing-function-elastic-out-5: var(--ease-elastic-out-5); 60 | --transition-timing-function-elastic-in-1: var(--ease-elastic-in-1); 61 | --transition-timing-function-elastic-in-2: var(--ease-elastic-in-2); 62 | --transition-timing-function-elastic-in-3: var(--ease-elastic-in-3); 63 | --transition-timing-function-elastic-in-4: var(--ease-elastic-in-4); 64 | --transition-timing-function-elastic-in-5: var(--ease-elastic-in-5); 65 | --transition-timing-function-elastic-in-out-1: var(--ease-elastic-in-out-1); 66 | --transition-timing-function-elastic-in-out-2: var(--ease-elastic-in-out-2); 67 | --transition-timing-function-elastic-in-out-3: var(--ease-elastic-in-out-3); 68 | --transition-timing-function-elastic-in-out-4: var(--ease-elastic-in-out-4); 69 | --transition-timing-function-elastic-in-out-5: var(--ease-elastic-in-out-5); 70 | --transition-timing-function-step-1: var(--ease-step-1); 71 | --transition-timing-function-step-2: var(--ease-step-2); 72 | --transition-timing-function-step-3: var(--ease-step-3); 73 | --transition-timing-function-step-4: var(--ease-step-4); 74 | --transition-timing-function-step-5: var(--ease-step-5); 75 | --transition-timing-function-spring-1: var(--ease-spring-1); 76 | --transition-timing-function-spring-2: var(--ease-spring-2); 77 | --transition-timing-function-spring-3: var(--ease-spring-3); 78 | --transition-timing-function-spring-4: var(--ease-spring-4); 79 | --transition-timing-function-spring-5: var(--ease-spring-5); 80 | --transition-timing-function-bounce-1: var(--ease-bounce-1); 81 | --transition-timing-function-bounce-2: var(--ease-bounce-2); 82 | --transition-timing-function-bounce-3: var(--ease-bounce-3); 83 | --transition-timing-function-bounce-4: var(--ease-bounce-4); 84 | --transition-timing-function-bounce-5: var(--ease-bounce-5); 85 | 86 | /* custom open props content sizes */ 87 | --width-content-1: var(--size-content-1); 88 | --width-content-2: var(--size-content-2); 89 | --width-content-3: var(--size-content-3); 90 | --width-header-1: var(--size-header-1); 91 | --width-header-2: var(--size-header-2); 92 | --width-header-3: var(--size-header-3); 93 | 94 | /* custom adaptive (light/dark) prop utilities */ 95 | --color-ink-1: var(--ink-1); 96 | --color-ink-2: var(--ink-2); 97 | --color-surface-1: var(--surface-1); 98 | --color-surface-2: var(--surface-2); 99 | --color-link: var(--link); 100 | 101 | /* gradients */ 102 | --background-image-gradient-1: var(--gradient-1); 103 | --background-image-gradient-2: var(--gradient-2); 104 | --background-image-gradient-3: var(--gradient-3); 105 | --background-image-gradient-4: var(--gradient-4); 106 | --background-image-gradient-5: var(--gradient-5); 107 | --background-image-gradient-6: var(--gradient-6); 108 | --background-image-gradient-7: var(--gradient-7); 109 | --background-image-gradient-8: var(--gradient-8); 110 | --background-image-gradient-9: var(--gradient-9); 111 | --background-image-gradient-10: var(--gradient-10); 112 | --background-image-gradient-11: var(--gradient-11); 113 | --background-image-gradient-12: var(--gradient-12); 114 | --background-image-gradient-13: var(--gradient-13); 115 | --background-image-gradient-14: var(--gradient-14); 116 | --background-image-gradient-15: var(--gradient-15); 117 | --background-image-gradient-16: var(--gradient-16); 118 | --background-image-gradient-17: var(--gradient-17); 119 | --background-image-gradient-18: var(--gradient-18); 120 | --background-image-gradient-19: var(--gradient-19); 121 | --background-image-gradient-20: var(--gradient-20); 122 | --background-image-gradient-21: var(--gradient-21); 123 | --background-image-gradient-22: var(--gradient-22); 124 | --background-image-gradient-23: var(--gradient-23); 125 | --background-image-gradient-24: var(--gradient-24); 126 | --background-image-gradient-25: var(--gradient-25); 127 | --background-image-gradient-26: var(--gradient-26); 128 | --background-image-gradient-27: var(--gradient-27); 129 | --background-image-gradient-28: var(--gradient-28); 130 | --background-image-gradient-29: var(--gradient-29); 131 | --background-image-gradient-30: var(--gradient-30); 132 | } 133 | 134 | :root { 135 | --ink-1: var(--gray-9); 136 | --ink-2: var(--gray-7); 137 | --surface-1: var(--gray-2); 138 | --surface-2: var(--gray-1); 139 | --link: var(--indigo-6); 140 | } 141 | 142 | @media (prefers-color-scheme: dark) { 143 | :root { 144 | --ink-1: var(--gray-1); 145 | --ink-2: var(--gray-5); 146 | --surface-1: var(--gray-11); 147 | --surface-2: var(--gray-10); 148 | --link: var(--indigo-4); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twop", 3 | "private": true, 4 | "sideEffects": false, 5 | "type": "module", 6 | "scripts": { 7 | "build": "remix vite:build", 8 | "dev": "remix vite:dev", 9 | "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", 10 | "start": "remix-serve ./build/server/index.js", 11 | "typecheck": "tsc" 12 | }, 13 | "dependencies": { 14 | "@remix-run/node": "^2.8.1", 15 | "@remix-run/react": "^2.8.1", 16 | "@remix-run/serve": "^2.8.1", 17 | "@tailwindcss/vite": "^4.0.0-alpha.7", 18 | "isbot": "^4.1.0", 19 | "open-props": "^1.6.21", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "tailwindcss": "^4.0.0-alpha.7" 23 | }, 24 | "devDependencies": { 25 | "@remix-run/dev": "^2.8.1", 26 | "@types/react": "^18.2.20", 27 | "@types/react-dom": "^18.2.7", 28 | "@typescript-eslint/eslint-plugin": "^6.7.4", 29 | "@typescript-eslint/parser": "^6.7.4", 30 | "eslint": "^8.38.0", 31 | "eslint-import-resolver-typescript": "^3.6.1", 32 | "eslint-plugin-import": "^2.28.1", 33 | "eslint-plugin-jsx-a11y": "^6.7.1", 34 | "eslint-plugin-react": "^7.33.2", 35 | "eslint-plugin-react-hooks": "^4.6.0", 36 | "typescript": "^5.1.6", 37 | "vite": "^5.1.0", 38 | "vite-tsconfig-paths": "^4.2.1" 39 | }, 40 | "engines": { 41 | "node": ">=18.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/argyleink/twop/9403874abcf3ee52bc2a3bfa37b9e7be55ecd8a7/public/favicon.ico -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | // this file is only for tailwind vscode extension intellisense support 3 | export default { 4 | content: ["./app/**/*.{html,js,tsx}"], 5 | plugins: [], 6 | }; 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*.ts", 4 | "**/*.tsx", 5 | "**/.server/**/*.ts", 6 | "**/.server/**/*.tsx", 7 | "**/.client/**/*.ts", 8 | "**/.client/**/*.tsx" 9 | ], 10 | "compilerOptions": { 11 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 12 | "types": ["@remix-run/node", "vite/client"], 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "jsx": "react-jsx", 16 | "module": "ESNext", 17 | "moduleResolution": "Bundler", 18 | "resolveJsonModule": true, 19 | "target": "ES2022", 20 | "strict": true, 21 | "allowJs": true, 22 | "skipLibCheck": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "baseUrl": ".", 25 | "paths": { 26 | "~/*": ["./app/*"] 27 | }, 28 | 29 | // Vite takes care of building everything, not tsc. 30 | "noEmit": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindcss from '@tailwindcss/vite'; 2 | import { vitePlugin as remix } from "@remix-run/dev"; 3 | import { installGlobals } from "@remix-run/node"; 4 | import { defineConfig } from "vite"; 5 | import tsconfigPaths from "vite-tsconfig-paths"; 6 | 7 | installGlobals(); 8 | 9 | export default defineConfig({ 10 | plugins: [remix(), tsconfigPaths(), tailwindcss()], 11 | }); 12 | --------------------------------------------------------------------------------