remix-validated-form 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- package/.eslintignore +1 -0
- package/.eslintrc.js +46 -0
- package/.github/workflows/test.yml +35 -0
- package/.husky/pre-commit +4 -0
- package/.prettierignore +8 -0
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/jest.config.js +5 -0
- package/package.json +73 -0
- package/src/ValidatedForm.tsx +130 -0
- package/src/hooks.ts +27 -0
- package/src/index.ts +5 -0
- package/src/internal/formContext.ts +18 -0
- package/src/internal/util.ts +23 -0
- package/src/server.ts +5 -0
- package/src/validation/types.ts +12 -0
- package/src/validation/validation.test.ts +76 -0
- package/src/validation/withYup.ts +37 -0
- package/test-app/README.md +53 -0
- package/test-app/app/components/Input.tsx +24 -0
- package/test-app/app/components/SubmitButton.tsx +18 -0
- package/test-app/app/entry.client.tsx +4 -0
- package/test-app/app/entry.server.tsx +21 -0
- package/test-app/app/root.tsx +246 -0
- package/test-app/app/routes/default-values.tsx +34 -0
- package/test-app/app/routes/index.tsx +100 -0
- package/test-app/app/routes/noscript.tsx +10 -0
- package/test-app/app/routes/submission.alt.tsx +6 -0
- package/test-app/app/routes/submission.fetcher.tsx +6 -0
- package/test-app/app/routes/submission.tsx +47 -0
- package/test-app/app/routes/validation.tsx +40 -0
- package/test-app/app/styles/dark.css +7 -0
- package/test-app/app/styles/demos/about.css +26 -0
- package/test-app/app/styles/demos/remix.css +120 -0
- package/test-app/app/styles/global.css +98 -0
- package/test-app/cypress/fixtures/example.json +5 -0
- package/test-app/cypress/integration/default-values.ts +15 -0
- package/test-app/cypress/integration/sanity.ts +19 -0
- package/test-app/cypress/integration/submission.ts +26 -0
- package/test-app/cypress/integration/validation.ts +70 -0
- package/test-app/cypress/plugins/config.ts +38 -0
- package/test-app/cypress/plugins/index.ts +9 -0
- package/test-app/cypress/support/commands/index.ts +13 -0
- package/test-app/cypress/support/commands/types.d.ts +11 -0
- package/test-app/cypress/support/index.ts +20 -0
- package/test-app/cypress/tsconfig.json +11 -0
- package/test-app/cypress.json +3 -0
- package/test-app/package-lock.json +11675 -0
- package/test-app/package.json +40 -0
- package/test-app/public/favicon.ico +0 -0
- package/test-app/remix.config.js +10 -0
- package/test-app/remix.env.d.ts +2 -0
- package/test-app/tsconfig.json +18 -0
- package/tsconfig.json +15 -0
@@ -0,0 +1,246 @@
|
|
1
|
+
import * as React from "react";
|
2
|
+
import {
|
3
|
+
Link,
|
4
|
+
Links,
|
5
|
+
LiveReload,
|
6
|
+
Meta,
|
7
|
+
Outlet,
|
8
|
+
Scripts,
|
9
|
+
ScrollRestoration,
|
10
|
+
useCatch,
|
11
|
+
useLocation,
|
12
|
+
} from "remix";
|
13
|
+
import type { LinksFunction } from "remix";
|
14
|
+
import darkStylesUrl from "~/styles/dark.css";
|
15
|
+
import deleteMeRemixStyles from "~/styles/demos/remix.css";
|
16
|
+
import globalStylesUrl from "~/styles/global.css";
|
17
|
+
|
18
|
+
/**
|
19
|
+
* The `links` export is a function that returns an array of objects that map to
|
20
|
+
* the attributes for an HTML `<link>` element. These will load `<link>` tags on
|
21
|
+
* every route in the app, but individual routes can include their own links
|
22
|
+
* that are automatically unloaded when a user navigates away from the route.
|
23
|
+
*
|
24
|
+
* https://remix.run/api/app#links
|
25
|
+
*/
|
26
|
+
export let links: LinksFunction = () => {
|
27
|
+
return [
|
28
|
+
{ rel: "stylesheet", href: globalStylesUrl },
|
29
|
+
{
|
30
|
+
rel: "stylesheet",
|
31
|
+
href: darkStylesUrl,
|
32
|
+
media: "(prefers-color-scheme: dark)",
|
33
|
+
},
|
34
|
+
{ rel: "stylesheet", href: deleteMeRemixStyles },
|
35
|
+
];
|
36
|
+
};
|
37
|
+
|
38
|
+
/**
|
39
|
+
* The root module's default export is a component that renders the current
|
40
|
+
* route via the `<Outlet />` component. Think of this as the global layout
|
41
|
+
* component for your app.
|
42
|
+
*/
|
43
|
+
export default function App() {
|
44
|
+
return (
|
45
|
+
<Document>
|
46
|
+
<Layout>
|
47
|
+
<Outlet />
|
48
|
+
</Layout>
|
49
|
+
</Document>
|
50
|
+
);
|
51
|
+
}
|
52
|
+
|
53
|
+
function Document({
|
54
|
+
children,
|
55
|
+
title,
|
56
|
+
}: {
|
57
|
+
children: React.ReactNode;
|
58
|
+
title?: string;
|
59
|
+
}) {
|
60
|
+
return (
|
61
|
+
<html lang="en">
|
62
|
+
<head>
|
63
|
+
<meta charSet="utf-8" />
|
64
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
65
|
+
{title ? <title>{title}</title> : null}
|
66
|
+
<Meta />
|
67
|
+
<Links />
|
68
|
+
</head>
|
69
|
+
<body>
|
70
|
+
{children}
|
71
|
+
<RouteChangeAnnouncement />
|
72
|
+
<ScrollRestoration />
|
73
|
+
<Scripts />
|
74
|
+
{process.env.NODE_ENV === "development" && <LiveReload />}
|
75
|
+
</body>
|
76
|
+
</html>
|
77
|
+
);
|
78
|
+
}
|
79
|
+
|
80
|
+
function Layout({ children }: React.PropsWithChildren<{}>) {
|
81
|
+
return (
|
82
|
+
<div className="remix-app">
|
83
|
+
<header className="remix-app__header">
|
84
|
+
<div className="container remix-app__header-content">
|
85
|
+
<Link to="/" title="Remix" className="remix-app__header-home-link">
|
86
|
+
<RemixLogo />
|
87
|
+
</Link>
|
88
|
+
<nav aria-label="Main navigation" className="remix-app__header-nav">
|
89
|
+
<ul>
|
90
|
+
<li>
|
91
|
+
<Link to="/">Home</Link>
|
92
|
+
</li>
|
93
|
+
<li>
|
94
|
+
<a href="https://remix.run/docs">Remix Docs</a>
|
95
|
+
</li>
|
96
|
+
<li>
|
97
|
+
<a href="https://github.com/remix-run/remix">GitHub</a>
|
98
|
+
</li>
|
99
|
+
</ul>
|
100
|
+
</nav>
|
101
|
+
</div>
|
102
|
+
</header>
|
103
|
+
<div className="remix-app__main">
|
104
|
+
<div className="container remix-app__main-content">{children}</div>
|
105
|
+
</div>
|
106
|
+
<footer className="remix-app__footer">
|
107
|
+
<div className="container remix-app__footer-content">
|
108
|
+
<p>© You!</p>
|
109
|
+
</div>
|
110
|
+
</footer>
|
111
|
+
</div>
|
112
|
+
);
|
113
|
+
}
|
114
|
+
|
115
|
+
export function CatchBoundary() {
|
116
|
+
let caught = useCatch();
|
117
|
+
|
118
|
+
let message;
|
119
|
+
switch (caught.status) {
|
120
|
+
case 401:
|
121
|
+
message = (
|
122
|
+
<p>
|
123
|
+
Oops! Looks like you tried to visit a page that you do not have access
|
124
|
+
to.
|
125
|
+
</p>
|
126
|
+
);
|
127
|
+
break;
|
128
|
+
case 404:
|
129
|
+
message = (
|
130
|
+
<p>Oops! Looks like you tried to visit a page that does not exist.</p>
|
131
|
+
);
|
132
|
+
break;
|
133
|
+
|
134
|
+
default:
|
135
|
+
throw new Error(caught.data || caught.statusText);
|
136
|
+
}
|
137
|
+
|
138
|
+
return (
|
139
|
+
<Document title={`${caught.status} ${caught.statusText}`}>
|
140
|
+
<Layout>
|
141
|
+
<h1>
|
142
|
+
{caught.status}: {caught.statusText}
|
143
|
+
</h1>
|
144
|
+
{message}
|
145
|
+
</Layout>
|
146
|
+
</Document>
|
147
|
+
);
|
148
|
+
}
|
149
|
+
|
150
|
+
export function ErrorBoundary({ error }: { error: Error }) {
|
151
|
+
console.error(error);
|
152
|
+
return (
|
153
|
+
<Document title="Error!">
|
154
|
+
<Layout>
|
155
|
+
<div>
|
156
|
+
<h1>There was an error</h1>
|
157
|
+
<p>{error.message}</p>
|
158
|
+
<hr />
|
159
|
+
<p>
|
160
|
+
Hey, developer, you should replace this with what you want your
|
161
|
+
users to see.
|
162
|
+
</p>
|
163
|
+
</div>
|
164
|
+
</Layout>
|
165
|
+
</Document>
|
166
|
+
);
|
167
|
+
}
|
168
|
+
|
169
|
+
function RemixLogo(props: React.ComponentPropsWithoutRef<"svg">) {
|
170
|
+
return (
|
171
|
+
<svg
|
172
|
+
viewBox="0 0 659 165"
|
173
|
+
version="1.1"
|
174
|
+
xmlns="http://www.w3.org/2000/svg"
|
175
|
+
xmlnsXlink="http://www.w3.org/1999/xlink"
|
176
|
+
aria-labelledby="remix-run-logo-title"
|
177
|
+
role="img"
|
178
|
+
width="106"
|
179
|
+
height="30"
|
180
|
+
fill="currentColor"
|
181
|
+
{...props}
|
182
|
+
>
|
183
|
+
<title id="remix-run-logo-title">Remix Logo</title>
|
184
|
+
<path d="M0 161V136H45.5416C53.1486 136 54.8003 141.638 54.8003 145V161H0Z M133.85 124.16C135.3 142.762 135.3 151.482 135.3 161H92.2283C92.2283 158.927 92.2653 157.03 92.3028 155.107C92.4195 149.128 92.5411 142.894 91.5717 130.304C90.2905 111.872 82.3473 107.776 67.7419 107.776H54.8021H0V74.24H69.7918C88.2407 74.24 97.4651 68.632 97.4651 53.784C97.4651 40.728 88.2407 32.816 69.7918 32.816H0V0H77.4788C119.245 0 140 19.712 140 51.2C140 74.752 125.395 90.112 105.665 92.672C122.32 96 132.057 105.472 133.85 124.16Z" />
|
185
|
+
<path d="M229.43 120.576C225.59 129.536 218.422 133.376 207.158 133.376C194.614 133.376 184.374 126.72 183.35 112.64H263.478V101.12C263.478 70.1437 243.254 44.0317 205.11 44.0317C169.526 44.0317 142.902 69.8877 142.902 105.984C142.902 142.336 169.014 164.352 205.622 164.352C235.83 164.352 256.822 149.76 262.71 123.648L229.43 120.576ZM183.862 92.6717C185.398 81.9197 191.286 73.7277 204.598 73.7277C216.886 73.7277 223.542 82.4317 224.054 92.6717H183.862Z" />
|
186
|
+
<path d="M385.256 66.5597C380.392 53.2477 369.896 44.0317 349.672 44.0317C332.52 44.0317 320.232 51.7117 314.088 64.2557V47.1037H272.616V161.28H314.088V105.216C314.088 88.0638 318.952 76.7997 332.52 76.7997C345.064 76.7997 348.136 84.9917 348.136 100.608V161.28H389.608V105.216C389.608 88.0638 394.216 76.7997 408.04 76.7997C420.584 76.7997 423.4 84.9917 423.4 100.608V161.28H464.872V89.5997C464.872 65.7917 455.656 44.0317 424.168 44.0317C404.968 44.0317 391.4 53.7597 385.256 66.5597Z" />
|
187
|
+
<path d="M478.436 47.104V161.28H519.908V47.104H478.436ZM478.18 36.352H520.164V0H478.18V36.352Z" />
|
188
|
+
<path d="M654.54 47.1035H611.788L592.332 74.2395L573.388 47.1035H527.564L568.78 103.168L523.98 161.28H566.732L589.516 130.304L612.3 161.28H658.124L613.068 101.376L654.54 47.1035Z" />
|
189
|
+
</svg>
|
190
|
+
);
|
191
|
+
}
|
192
|
+
|
193
|
+
/**
|
194
|
+
* Provides an alert for screen reader users when the route changes.
|
195
|
+
*/
|
196
|
+
const RouteChangeAnnouncement = React.memo(() => {
|
197
|
+
let [hydrated, setHydrated] = React.useState(false);
|
198
|
+
let [innerHtml, setInnerHtml] = React.useState("");
|
199
|
+
let location = useLocation();
|
200
|
+
|
201
|
+
React.useEffect(() => {
|
202
|
+
setHydrated(true);
|
203
|
+
}, []);
|
204
|
+
|
205
|
+
let firstRenderRef = React.useRef(true);
|
206
|
+
React.useEffect(() => {
|
207
|
+
// Skip the first render because we don't want an announcement on the
|
208
|
+
// initial page load.
|
209
|
+
if (firstRenderRef.current) {
|
210
|
+
firstRenderRef.current = false;
|
211
|
+
return;
|
212
|
+
}
|
213
|
+
|
214
|
+
let pageTitle = location.pathname === "/" ? "Home page" : document.title;
|
215
|
+
setInnerHtml(`Navigated to ${pageTitle}`);
|
216
|
+
}, [location.pathname]);
|
217
|
+
|
218
|
+
// Render nothing on the server. The live region provides no value unless
|
219
|
+
// scripts are loaded and the browser takes over normal routing.
|
220
|
+
if (!hydrated) {
|
221
|
+
return null;
|
222
|
+
}
|
223
|
+
|
224
|
+
return (
|
225
|
+
<div
|
226
|
+
aria-live="assertive"
|
227
|
+
aria-atomic
|
228
|
+
id="route-change-region"
|
229
|
+
style={{
|
230
|
+
border: "0",
|
231
|
+
clipPath: "inset(100%)",
|
232
|
+
clip: "rect(0 0 0 0)",
|
233
|
+
height: "1px",
|
234
|
+
margin: "-1px",
|
235
|
+
overflow: "hidden",
|
236
|
+
padding: "0",
|
237
|
+
position: "absolute",
|
238
|
+
width: "1px",
|
239
|
+
whiteSpace: "nowrap",
|
240
|
+
wordWrap: "normal",
|
241
|
+
}}
|
242
|
+
>
|
243
|
+
{innerHtml}
|
244
|
+
</div>
|
245
|
+
);
|
246
|
+
});
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import { LoaderFunction, useLoaderData } from "remix";
|
2
|
+
import * as yup from "yup";
|
3
|
+
import { Input } from "~/components/Input";
|
4
|
+
import { SubmitButton } from "~/components/SubmitButton";
|
5
|
+
import { ValidatedForm, withYup } from "../../remix-validated-form";
|
6
|
+
|
7
|
+
const schema = yup.object({});
|
8
|
+
const validator = withYup(schema);
|
9
|
+
|
10
|
+
export const loader: LoaderFunction = () => {
|
11
|
+
return {
|
12
|
+
defaultValues: {
|
13
|
+
firstName: "Jane",
|
14
|
+
lastName: "Doe",
|
15
|
+
email: "jane.doe@example.com",
|
16
|
+
},
|
17
|
+
};
|
18
|
+
};
|
19
|
+
|
20
|
+
export default function DefaultValues() {
|
21
|
+
const { defaultValues } = useLoaderData();
|
22
|
+
return (
|
23
|
+
<ValidatedForm
|
24
|
+
validator={validator}
|
25
|
+
method="post"
|
26
|
+
defaultValues={defaultValues}
|
27
|
+
>
|
28
|
+
<Input name="firstName" label="First Name" validateOnBlur />
|
29
|
+
<Input name="lastName" label="Last Name" validateOnBlur />
|
30
|
+
<Input name="email" label="Email" validateOnBlur />
|
31
|
+
<SubmitButton />
|
32
|
+
</ValidatedForm>
|
33
|
+
);
|
34
|
+
}
|
@@ -0,0 +1,100 @@
|
|
1
|
+
import type { MetaFunction, LoaderFunction } from "remix";
|
2
|
+
import { useLoaderData, json, Link } from "remix";
|
3
|
+
|
4
|
+
type IndexData = {
|
5
|
+
resources: Array<{ name: string; url: string }>;
|
6
|
+
demos: Array<{ name: string; to: string }>;
|
7
|
+
};
|
8
|
+
|
9
|
+
// Loaders provide data to components and are only ever called on the server, so
|
10
|
+
// you can connect to a database or run any server side code you want right next
|
11
|
+
// to the component that renders it.
|
12
|
+
// https://remix.run/api/conventions#loader
|
13
|
+
export let loader: LoaderFunction = () => {
|
14
|
+
let data: IndexData = {
|
15
|
+
resources: [
|
16
|
+
{
|
17
|
+
name: "Remix Docs",
|
18
|
+
url: "https://remix.run/docs",
|
19
|
+
},
|
20
|
+
{
|
21
|
+
name: "React Router Docs",
|
22
|
+
url: "https://reactrouter.com/docs",
|
23
|
+
},
|
24
|
+
{
|
25
|
+
name: "Remix Discord",
|
26
|
+
url: "https://discord.gg/VBePs6d",
|
27
|
+
},
|
28
|
+
],
|
29
|
+
demos: [
|
30
|
+
{
|
31
|
+
to: "demos/actions",
|
32
|
+
name: "Actions",
|
33
|
+
},
|
34
|
+
{
|
35
|
+
to: "demos/about",
|
36
|
+
name: "Nested Routes, CSS loading/unloading",
|
37
|
+
},
|
38
|
+
{
|
39
|
+
to: "demos/params",
|
40
|
+
name: "URL Params and Error Boundaries",
|
41
|
+
},
|
42
|
+
],
|
43
|
+
};
|
44
|
+
|
45
|
+
// https://remix.run/api/remix#json
|
46
|
+
return json(data);
|
47
|
+
};
|
48
|
+
|
49
|
+
// https://remix.run/api/conventions#meta
|
50
|
+
export let meta: MetaFunction = () => {
|
51
|
+
return {
|
52
|
+
title: "Remix Starter",
|
53
|
+
description: "Welcome to remix!",
|
54
|
+
};
|
55
|
+
};
|
56
|
+
|
57
|
+
// https://remix.run/guides/routing#index-routes
|
58
|
+
export default function Index() {
|
59
|
+
let data = useLoaderData<IndexData>();
|
60
|
+
|
61
|
+
return (
|
62
|
+
<div className="remix__page">
|
63
|
+
<main>
|
64
|
+
<h2>Welcome to Remix!</h2>
|
65
|
+
<p>We're stoked that you're here. 🥳</p>
|
66
|
+
<p>
|
67
|
+
Feel free to take a look around the code to see how Remix does things,
|
68
|
+
it might be a bit different than what you’re used to. When you're
|
69
|
+
ready to dive deeper, we've got plenty of resources to get you
|
70
|
+
up-and-running quickly.
|
71
|
+
</p>
|
72
|
+
<p>
|
73
|
+
Check out all the demos in this starter, and then just delete the{" "}
|
74
|
+
<code>app/routes/demos</code> and <code>app/styles/demos</code>{" "}
|
75
|
+
folders when you're ready to turn this into your next project.
|
76
|
+
</p>
|
77
|
+
</main>
|
78
|
+
<aside>
|
79
|
+
<h2>Demos In This App</h2>
|
80
|
+
<ul>
|
81
|
+
{data.demos.map((demo) => (
|
82
|
+
<li key={demo.to} className="remix__page__resource">
|
83
|
+
<Link to={demo.to} prefetch="intent">
|
84
|
+
{demo.name}
|
85
|
+
</Link>
|
86
|
+
</li>
|
87
|
+
))}
|
88
|
+
</ul>
|
89
|
+
<h2>Resources</h2>
|
90
|
+
<ul>
|
91
|
+
{data.resources.map((resource) => (
|
92
|
+
<li key={resource.url} className="remix__page__resource">
|
93
|
+
<a href={resource.url}>{resource.name}</a>
|
94
|
+
</li>
|
95
|
+
))}
|
96
|
+
</ul>
|
97
|
+
</aside>
|
98
|
+
</div>
|
99
|
+
);
|
100
|
+
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import { ActionFunction, json, useFetcher } from "remix";
|
2
|
+
import * as yup from "yup";
|
3
|
+
import { SubmitButton } from "~/components/SubmitButton";
|
4
|
+
import { ValidatedForm, withYup } from "../../remix-validated-form";
|
5
|
+
|
6
|
+
const schema = yup.object({});
|
7
|
+
const validator = withYup(schema);
|
8
|
+
|
9
|
+
export const action: ActionFunction = async () => {
|
10
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
11
|
+
return json({ message: "Submitted!" });
|
12
|
+
};
|
13
|
+
|
14
|
+
export default function FrontendValidation() {
|
15
|
+
const fetcher = useFetcher();
|
16
|
+
return (
|
17
|
+
<>
|
18
|
+
<ValidatedForm validator={validator} method="post">
|
19
|
+
<SubmitButton
|
20
|
+
label="Submit main form"
|
21
|
+
submittingLabel="Submitting main form"
|
22
|
+
/>
|
23
|
+
</ValidatedForm>
|
24
|
+
<ValidatedForm
|
25
|
+
validator={validator}
|
26
|
+
method="post"
|
27
|
+
action="/submission/alt"
|
28
|
+
>
|
29
|
+
<SubmitButton
|
30
|
+
label="Submit alt form"
|
31
|
+
submittingLabel="Submitting alt form"
|
32
|
+
/>
|
33
|
+
</ValidatedForm>
|
34
|
+
<ValidatedForm
|
35
|
+
validator={validator}
|
36
|
+
method="post"
|
37
|
+
action="/submission/fetcher"
|
38
|
+
fetcher={fetcher}
|
39
|
+
>
|
40
|
+
<SubmitButton
|
41
|
+
label="Submit fetcher form"
|
42
|
+
submittingLabel="Submitting fetcher form"
|
43
|
+
/>
|
44
|
+
</ValidatedForm>
|
45
|
+
</>
|
46
|
+
);
|
47
|
+
}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import { ActionFunction, useActionData } from "remix";
|
2
|
+
import * as yup from "yup";
|
3
|
+
import { Input } from "~/components/Input";
|
4
|
+
import { SubmitButton } from "~/components/SubmitButton";
|
5
|
+
import {
|
6
|
+
fieldErrors,
|
7
|
+
ValidatedForm,
|
8
|
+
withYup,
|
9
|
+
} from "../../remix-validated-form";
|
10
|
+
|
11
|
+
const schema = yup.object({
|
12
|
+
firstName: yup.string().label("First Name").required(),
|
13
|
+
lastName: yup.string().label("Last Name").required(),
|
14
|
+
email: yup.string().label("Email").email().required(),
|
15
|
+
});
|
16
|
+
|
17
|
+
const validator = withYup(schema);
|
18
|
+
|
19
|
+
export const action: ActionFunction = async ({ request }) => {
|
20
|
+
const result = validator.validate(
|
21
|
+
Object.fromEntries(await request.formData())
|
22
|
+
);
|
23
|
+
if (result.error) return fieldErrors(result.error);
|
24
|
+
const { firstName, lastName } = result.data;
|
25
|
+
|
26
|
+
return { message: `Submitted for ${firstName} ${lastName}!` };
|
27
|
+
};
|
28
|
+
|
29
|
+
export default function FrontendValidation() {
|
30
|
+
const actionData = useActionData();
|
31
|
+
return (
|
32
|
+
<ValidatedForm validator={validator} method="post">
|
33
|
+
{actionData && <h1>{actionData.message}</h1>}
|
34
|
+
<Input name="firstName" label="First Name" validateOnBlur />
|
35
|
+
<Input name="lastName" label="Last Name" validateOnBlur />
|
36
|
+
<Input name="email" label="Email" validateOnBlur />
|
37
|
+
<SubmitButton />
|
38
|
+
</ValidatedForm>
|
39
|
+
);
|
40
|
+
}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
/*
|
2
|
+
* Whoa whoa whoa, wait a sec...why are we overriding global CSS selectors?
|
3
|
+
* Isn't that kind of scary? How do we know this won't have side effects?
|
4
|
+
*
|
5
|
+
* In Remix, CSS that is included in a route file will *only* show up on that
|
6
|
+
* route (and for nested routes, its children). When the user navigates away
|
7
|
+
* from that route the CSS files linked from those routes will be automatically
|
8
|
+
* unloaded, making your styles much easier to predict and control.
|
9
|
+
*
|
10
|
+
* Read more about styling routes in the docs:
|
11
|
+
* https://remix.run/guides/styling
|
12
|
+
*/
|
13
|
+
|
14
|
+
:root {
|
15
|
+
--color-foreground: hsl(0, 0%, 7%);
|
16
|
+
--color-background: hsl(56, 100%, 50%);
|
17
|
+
--color-links: hsl(345, 56%, 39%);
|
18
|
+
--color-links-hover: hsl(345, 51%, 49%);
|
19
|
+
--color-border: rgb(184, 173, 20);
|
20
|
+
--font-body: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
21
|
+
Liberation Mono, Courier New, monospace;
|
22
|
+
}
|
23
|
+
|
24
|
+
.about__intro {
|
25
|
+
max-width: 500px;
|
26
|
+
}
|
@@ -0,0 +1,120 @@
|
|
1
|
+
/*
|
2
|
+
* You probably want to just delete this file; it's just for the demo pages.
|
3
|
+
*/
|
4
|
+
.remix-app {
|
5
|
+
display: flex;
|
6
|
+
flex-direction: column;
|
7
|
+
min-height: 100vh;
|
8
|
+
min-height: calc(100vh - env(safe-area-inset-bottom));
|
9
|
+
}
|
10
|
+
|
11
|
+
.remix-app > * {
|
12
|
+
width: 100%;
|
13
|
+
}
|
14
|
+
|
15
|
+
.remix-app__header {
|
16
|
+
padding-top: 1rem;
|
17
|
+
padding-bottom: 1rem;
|
18
|
+
border-bottom: 1px solid var(--color-border);
|
19
|
+
}
|
20
|
+
|
21
|
+
.remix-app__header-content {
|
22
|
+
display: flex;
|
23
|
+
justify-content: space-between;
|
24
|
+
align-items: center;
|
25
|
+
}
|
26
|
+
|
27
|
+
.remix-app__header-home-link {
|
28
|
+
width: 106px;
|
29
|
+
height: 30px;
|
30
|
+
color: var(--color-foreground);
|
31
|
+
}
|
32
|
+
|
33
|
+
.remix-app__header-nav ul {
|
34
|
+
list-style: none;
|
35
|
+
margin: 0;
|
36
|
+
display: flex;
|
37
|
+
align-items: center;
|
38
|
+
gap: 1.5em;
|
39
|
+
}
|
40
|
+
|
41
|
+
.remix-app__header-nav li {
|
42
|
+
font-weight: bold;
|
43
|
+
}
|
44
|
+
|
45
|
+
.remix-app__main {
|
46
|
+
flex: 1 1 100%;
|
47
|
+
}
|
48
|
+
|
49
|
+
.remix-app__footer {
|
50
|
+
padding-top: 1rem;
|
51
|
+
padding-bottom: 1rem;
|
52
|
+
border-top: 1px solid var(--color-border);
|
53
|
+
}
|
54
|
+
|
55
|
+
.remix-app__footer-content {
|
56
|
+
display: flex;
|
57
|
+
justify-content: center;
|
58
|
+
align-items: center;
|
59
|
+
}
|
60
|
+
|
61
|
+
.remix__page {
|
62
|
+
--gap: 1rem;
|
63
|
+
--space: 2rem;
|
64
|
+
display: grid;
|
65
|
+
grid-auto-rows: min-content;
|
66
|
+
gap: var(--gap);
|
67
|
+
padding-top: var(--space);
|
68
|
+
padding-bottom: var(--space);
|
69
|
+
}
|
70
|
+
|
71
|
+
@media print, screen and (min-width: 640px) {
|
72
|
+
.remix__page {
|
73
|
+
--gap: 2rem;
|
74
|
+
grid-auto-rows: unset;
|
75
|
+
grid-template-columns: repeat(2, 1fr);
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
@media screen and (min-width: 1024px) {
|
80
|
+
.remix__page {
|
81
|
+
--gap: 4rem;
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
.remix__page > main > :first-child {
|
86
|
+
margin-top: 0;
|
87
|
+
}
|
88
|
+
|
89
|
+
.remix__page > main > :last-child {
|
90
|
+
margin-bottom: 0;
|
91
|
+
}
|
92
|
+
|
93
|
+
.remix__page > aside {
|
94
|
+
margin: 0;
|
95
|
+
padding: 1.5ch 2ch;
|
96
|
+
border: solid 1px var(--color-border);
|
97
|
+
border-radius: 0.5rem;
|
98
|
+
}
|
99
|
+
|
100
|
+
.remix__page > aside > :first-child {
|
101
|
+
margin-top: 0;
|
102
|
+
}
|
103
|
+
|
104
|
+
.remix__page > aside > :last-child {
|
105
|
+
margin-bottom: 0;
|
106
|
+
}
|
107
|
+
|
108
|
+
.remix__form {
|
109
|
+
display: flex;
|
110
|
+
flex-direction: column;
|
111
|
+
gap: 1rem;
|
112
|
+
padding: 1rem;
|
113
|
+
border: 1px solid var(--color-border);
|
114
|
+
border-radius: 0.5rem;
|
115
|
+
}
|
116
|
+
|
117
|
+
.remix__form > * {
|
118
|
+
margin-top: 0;
|
119
|
+
margin-bottom: 0;
|
120
|
+
}
|