├── .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 |
--------------------------------------------------------------------------------