@sandboxsonny/ace-kit 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/astro-integration.js +1 -1
- package/dist/chunk-7M26C36M.js +35 -0
- package/dist/cli.js +14 -1179
- package/dist/environment-T5K5ZJ2M.js +222 -0
- package/dist/main-YRQQWLHX.js +1454 -0
- package/dist/vite.js +1 -1
- package/docs/command-line.md +30 -1
- package/docs/getting-started.md +8 -0
- package/package.json +5 -2
- package/templates/astro/.dockerignore +7 -0
- package/templates/astro/.github/dependabot.yml +19 -0
- package/templates/astro/.github/workflows/ci.yml +23 -0
- package/templates/astro/.prettierignore +6 -0
- package/templates/astro/.prettierrc.json +20 -0
- package/templates/astro/.vscode/extensions.json +4 -0
- package/templates/astro/.vscode/launch.json +11 -0
- package/templates/astro/AGENTS.md +22 -0
- package/templates/astro/Dockerfile +17 -0
- package/templates/astro/_gitignore +26 -0
- package/templates/astro/astro.config.mjs +18 -0
- package/templates/astro/eslint.config.js +56 -0
- package/templates/astro/package.json +41 -0
- package/templates/astro/public/favicon.ico +0 -0
- package/templates/astro/public/favicon.svg +9 -0
- package/templates/astro/src/ace-kit.generated.d.ts +24 -0
- package/templates/astro/src/components/PageContent.astro +113 -0
- package/templates/astro/src/components/SiteFooter.astro +19 -0
- package/templates/astro/src/components/SiteHeader.astro +121 -0
- package/templates/astro/src/components/sections/Faq.astro +54 -0
- package/templates/astro/src/components/sections/Faq.schema.ts +17 -0
- package/templates/astro/src/components/sections/Gallery.astro +74 -0
- package/templates/astro/src/components/sections/Gallery.schema.ts +20 -0
- package/templates/astro/src/components/sections/Hero.astro +58 -0
- package/templates/astro/src/components/sections/Hero.schema.ts +14 -0
- package/templates/astro/src/layouts/BaseLayout.astro +36 -0
- package/templates/astro/src/pages/[...slug].astro +31 -0
- package/templates/astro/src/styles/global.css +56 -0
- package/templates/astro/src/types/page.d.ts +4 -0
- package/templates/astro/tsconfig.json +11 -0
- package/templates/astro/vitest.config.ts +4 -0
- package/templates/astro.section-types.json +109 -0
- package/templates/react-router/.prettierignore +5 -0
- package/templates/react-router/.prettierrc.json +11 -0
- package/templates/react-router/_gitignore +7 -0
- package/templates/react-router/app/ace-kit.generated.d.ts +18 -0
- package/templates/react-router/app/app.css +172 -0
- package/templates/react-router/app/components/page-content.tsx +32 -0
- package/templates/react-router/app/components/site-footer.tsx +9 -0
- package/templates/react-router/app/components/site-header.tsx +49 -0
- package/templates/react-router/app/entry.server.tsx +1 -0
- package/templates/react-router/app/root.tsx +75 -0
- package/templates/react-router/app/routes/page.test.tsx +64 -0
- package/templates/react-router/app/routes/page.tsx +22 -0
- package/templates/react-router/app/routes.ts +4 -0
- package/templates/react-router/app/types/page.d.ts +4 -0
- package/templates/react-router/eslint.config.js +76 -0
- package/templates/react-router/package.json +51 -0
- package/templates/react-router/public/favicon.svg +9 -0
- package/templates/react-router/react-router.config.ts +6 -0
- package/templates/react-router/tsconfig.json +22 -0
- package/templates/react-router/vite.config.ts +10 -0
- package/templates/react-router/vitest.config.ts +15 -0
- /package/dist/{chunk-WM5NOQE3.js → chunk-G5FGBOFT.js} +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--colour-background: #ffffff;
|
|
3
|
+
--colour-surface: #f4f6f8;
|
|
4
|
+
--colour-text: #1b2430;
|
|
5
|
+
--colour-muted: #5b6776;
|
|
6
|
+
--colour-accent: #2f5bea;
|
|
7
|
+
--colour-accent-text: #ffffff;
|
|
8
|
+
--content-width: 64rem;
|
|
9
|
+
--font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
*,
|
|
13
|
+
*::before,
|
|
14
|
+
*::after {
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
margin: 0;
|
|
20
|
+
font-family: var(--font-body);
|
|
21
|
+
line-height: 1.6;
|
|
22
|
+
color: var(--colour-text);
|
|
23
|
+
background: var(--colour-background);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
img {
|
|
27
|
+
max-width: 100%;
|
|
28
|
+
height: auto;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
a {
|
|
32
|
+
color: var(--colour-accent);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.container {
|
|
36
|
+
width: min(100% - 2rem, var(--content-width));
|
|
37
|
+
margin-inline: auto;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.button {
|
|
41
|
+
display: inline-block;
|
|
42
|
+
padding: 0.75rem 1.25rem;
|
|
43
|
+
border-radius: 0.5rem;
|
|
44
|
+
background: var(--colour-accent);
|
|
45
|
+
color: var(--colour-accent-text);
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
text-decoration: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.section {
|
|
51
|
+
padding-block: 3rem;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.section--surface {
|
|
55
|
+
background: var(--colour-surface);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* The header */
|
|
59
|
+
.site-header {
|
|
60
|
+
border-bottom: 1px solid var(--colour-surface);
|
|
61
|
+
}
|
|
62
|
+
.site-header__inner {
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
justify-content: space-between;
|
|
66
|
+
padding-block: 1rem;
|
|
67
|
+
}
|
|
68
|
+
.site-header__name {
|
|
69
|
+
font-weight: 700;
|
|
70
|
+
font-size: 1.25rem;
|
|
71
|
+
color: var(--colour-text);
|
|
72
|
+
text-decoration: none;
|
|
73
|
+
}
|
|
74
|
+
.site-header__links {
|
|
75
|
+
display: flex;
|
|
76
|
+
gap: 1.5rem;
|
|
77
|
+
margin: 0;
|
|
78
|
+
padding: 0;
|
|
79
|
+
list-style: none;
|
|
80
|
+
}
|
|
81
|
+
.site-header__links a {
|
|
82
|
+
color: var(--colour-muted);
|
|
83
|
+
text-decoration: none;
|
|
84
|
+
}
|
|
85
|
+
.site-header__links > li {
|
|
86
|
+
position: relative;
|
|
87
|
+
}
|
|
88
|
+
.site-header__sublinks {
|
|
89
|
+
display: none;
|
|
90
|
+
position: absolute;
|
|
91
|
+
z-index: 10;
|
|
92
|
+
top: 100%;
|
|
93
|
+
left: 0;
|
|
94
|
+
min-width: 12rem;
|
|
95
|
+
margin: 0;
|
|
96
|
+
padding: 0.5rem 0;
|
|
97
|
+
list-style: none;
|
|
98
|
+
background: var(--colour-background);
|
|
99
|
+
border: 1px solid var(--colour-surface);
|
|
100
|
+
border-radius: 0.5rem;
|
|
101
|
+
}
|
|
102
|
+
.site-header__sublinks li {
|
|
103
|
+
position: relative;
|
|
104
|
+
}
|
|
105
|
+
.site-header__sublinks a {
|
|
106
|
+
display: block;
|
|
107
|
+
padding: 0.4rem 1rem;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
}
|
|
110
|
+
.site-header__sublinks--deep {
|
|
111
|
+
top: 0;
|
|
112
|
+
left: 100%;
|
|
113
|
+
}
|
|
114
|
+
.site-header__links > li:hover > .site-header__sublinks,
|
|
115
|
+
.site-header__links > li:focus-within > .site-header__sublinks,
|
|
116
|
+
.site-header__sublinks li:hover > .site-header__sublinks,
|
|
117
|
+
.site-header__sublinks li:focus-within > .site-header__sublinks {
|
|
118
|
+
display: block;
|
|
119
|
+
}
|
|
120
|
+
.site-header__links a[aria-current="page"] {
|
|
121
|
+
color: var(--colour-text);
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* The footer */
|
|
126
|
+
.site-footer {
|
|
127
|
+
margin-top: 3rem;
|
|
128
|
+
padding-block: 2rem;
|
|
129
|
+
background: var(--colour-surface);
|
|
130
|
+
color: var(--colour-muted);
|
|
131
|
+
font-size: 0.9rem;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* A page */
|
|
135
|
+
.prose {
|
|
136
|
+
max-width: 42rem;
|
|
137
|
+
}
|
|
138
|
+
.intro {
|
|
139
|
+
color: var(--colour-muted);
|
|
140
|
+
font-size: 1.125rem;
|
|
141
|
+
}
|
|
142
|
+
.prose figure {
|
|
143
|
+
margin: 2rem 0;
|
|
144
|
+
}
|
|
145
|
+
.prose figure img {
|
|
146
|
+
border-radius: 0.5rem;
|
|
147
|
+
}
|
|
148
|
+
.rich-text > * + * {
|
|
149
|
+
margin-top: 1rem;
|
|
150
|
+
}
|
|
151
|
+
.rich-text h2 {
|
|
152
|
+
font-size: 1.75rem;
|
|
153
|
+
}
|
|
154
|
+
.rich-text h3 {
|
|
155
|
+
font-size: 1.4rem;
|
|
156
|
+
}
|
|
157
|
+
.rich-text h4 {
|
|
158
|
+
font-size: 1.15rem;
|
|
159
|
+
}
|
|
160
|
+
.rich-text ul {
|
|
161
|
+
list-style: disc;
|
|
162
|
+
padding-left: 1.5rem;
|
|
163
|
+
}
|
|
164
|
+
.rich-text ol {
|
|
165
|
+
list-style: decimal;
|
|
166
|
+
padding-left: 1.5rem;
|
|
167
|
+
}
|
|
168
|
+
.rich-text blockquote {
|
|
169
|
+
border-left: 4px solid var(--colour-muted);
|
|
170
|
+
padding-left: 1rem;
|
|
171
|
+
font-style: italic;
|
|
172
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AceEntry } from "@sandboxsonny/ace-kit";
|
|
2
|
+
import { AceImage, AceRichText, AceTextArea } from "@sandboxsonny/ace-kit/react-router";
|
|
3
|
+
|
|
4
|
+
import type { PageValues } from "~/types/page";
|
|
5
|
+
|
|
6
|
+
/** An entry of the "Pages" content type: its title (or headline), description, hero image and body. */
|
|
7
|
+
export function PageContent({ entry }: { entry: AceEntry<PageValues> }) {
|
|
8
|
+
const { headline, body, hero } = entry.values;
|
|
9
|
+
return (
|
|
10
|
+
<section className="section">
|
|
11
|
+
<div className="container prose">
|
|
12
|
+
<h1>{headline || entry.title}</h1>
|
|
13
|
+
{entry.description ? <p className="intro">{entry.description}</p> : null}
|
|
14
|
+
{hero ? (
|
|
15
|
+
<figure>
|
|
16
|
+
<AceImage image={hero} sizes="(min-width: 64rem) 64rem, 100vw" lazy={false} />
|
|
17
|
+
</figure>
|
|
18
|
+
) : null}
|
|
19
|
+
{
|
|
20
|
+
// A site made before its Body field was rich text still has plain text, with blank lines between paragraphs.
|
|
21
|
+
typeof body === "string" ? (
|
|
22
|
+
<AceTextArea value={body} />
|
|
23
|
+
) : body ? (
|
|
24
|
+
<div className="rich-text">
|
|
25
|
+
<AceRichText document={body} />
|
|
26
|
+
</div>
|
|
27
|
+
) : null
|
|
28
|
+
}
|
|
29
|
+
</div>
|
|
30
|
+
</section>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { isInternalPath } from "@sandboxsonny/ace-kit";
|
|
2
|
+
import type { AceNavigationLink } from "@sandboxsonny/ace-kit";
|
|
3
|
+
import { NavLink } from "react-router";
|
|
4
|
+
|
|
5
|
+
/** One link: a router link for a page of this site (which marks the current page), a plain one for anywhere else. */
|
|
6
|
+
function HeaderLink({ link }: { link: AceNavigationLink }) {
|
|
7
|
+
return isInternalPath(link.to) ? (
|
|
8
|
+
<NavLink to={link.to} end>
|
|
9
|
+
{link.label}
|
|
10
|
+
</NavLink>
|
|
11
|
+
) : (
|
|
12
|
+
<a href={link.to}>{link.label}</a>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function SubLinks({ links, deep = false }: { links: AceNavigationLink[]; deep?: boolean }) {
|
|
17
|
+
return (
|
|
18
|
+
<ul className={deep ? "site-header__sublinks site-header__sublinks--deep" : "site-header__sublinks"}>
|
|
19
|
+
{links.map((link) => (
|
|
20
|
+
<li key={`${link.to}|${link.label}`}>
|
|
21
|
+
<HeaderLink link={link} />
|
|
22
|
+
{link.children && link.children.length > 0 ? <SubLinks links={link.children} deep /> : null}
|
|
23
|
+
</li>
|
|
24
|
+
))}
|
|
25
|
+
</ul>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function SiteHeader({ links }: { links: AceNavigationLink[] }) {
|
|
30
|
+
return (
|
|
31
|
+
<header className="site-header">
|
|
32
|
+
<div className="container site-header__inner">
|
|
33
|
+
<a href="/" className="site-header__name">
|
|
34
|
+
Example Site
|
|
35
|
+
</a>
|
|
36
|
+
<nav aria-label="Main">
|
|
37
|
+
<ul className="site-header__links">
|
|
38
|
+
{links.map((link) => (
|
|
39
|
+
<li key={`${link.to}|${link.label}`}>
|
|
40
|
+
<HeaderLink link={link} />
|
|
41
|
+
{link.children && link.children.length > 0 ? <SubLinks links={link.children} /> : null}
|
|
42
|
+
</li>
|
|
43
|
+
))}
|
|
44
|
+
</ul>
|
|
45
|
+
</nav>
|
|
46
|
+
</div>
|
|
47
|
+
</header>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { handleRequest as default, streamTimeout } from "@sandboxsonny/ace-kit/react-router/server";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useAceMenu, useAcePreview } from "@sandboxsonny/ace-kit/react-router";
|
|
2
|
+
import { createRootLoader } from "@sandboxsonny/ace-kit/react-router/server";
|
|
3
|
+
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
|
|
4
|
+
|
|
5
|
+
import type { Route } from "./+types/root";
|
|
6
|
+
import { SiteFooter } from "~/components/site-footer";
|
|
7
|
+
import { SiteHeader } from "~/components/site-header";
|
|
8
|
+
import stylesheet from "./app.css?url";
|
|
9
|
+
|
|
10
|
+
export const links: Route.LinksFunction = () => [
|
|
11
|
+
{ rel: "stylesheet", href: stylesheet },
|
|
12
|
+
{ rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
// The header's links come from the CMS menu with the handle "main-menu", or from these if the site has none yet
|
|
16
|
+
// or the CMS can't be reached.
|
|
17
|
+
export const loader = createRootLoader({
|
|
18
|
+
menu: "main-menu",
|
|
19
|
+
fallback: [
|
|
20
|
+
{ label: "Home", to: "/" },
|
|
21
|
+
{ label: "About", to: "/pages/about" },
|
|
22
|
+
{ label: "Contact", to: "/pages/contact" },
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export function Layout({ children }: { children: React.ReactNode }) {
|
|
27
|
+
return (
|
|
28
|
+
<html lang="en">
|
|
29
|
+
<head>
|
|
30
|
+
<meta charSet="utf-8" />
|
|
31
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
32
|
+
<Meta />
|
|
33
|
+
<Links />
|
|
34
|
+
</head>
|
|
35
|
+
<body>
|
|
36
|
+
{children}
|
|
37
|
+
<ScrollRestoration />
|
|
38
|
+
<Scripts />
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function App() {
|
|
45
|
+
useAcePreview(); // in the CMS's preview frame, redraws the page when the editor saves a draft
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<SiteHeader links={useAceMenu()} />
|
|
50
|
+
<main>
|
|
51
|
+
<Outlet />
|
|
52
|
+
</main>
|
|
53
|
+
<SiteFooter />
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
|
59
|
+
const notFound = isRouteErrorResponse(error) && error.status === 404;
|
|
60
|
+
return (
|
|
61
|
+
<main className="section">
|
|
62
|
+
<div className="container">
|
|
63
|
+
<h1>{notFound ? "Page not found" : "Something went wrong"}</h1>
|
|
64
|
+
<p>
|
|
65
|
+
{notFound
|
|
66
|
+
? "There's nothing at this address."
|
|
67
|
+
: "This page can't be shown right now. Please try again shortly."}
|
|
68
|
+
</p>
|
|
69
|
+
<a href="/" className="button">
|
|
70
|
+
Back to the home page
|
|
71
|
+
</a>
|
|
72
|
+
</div>
|
|
73
|
+
</main>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
2
|
+
import { MemoryRouter } from "react-router";
|
|
3
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import Page from "./page";
|
|
6
|
+
|
|
7
|
+
afterEach(cleanup);
|
|
8
|
+
|
|
9
|
+
// The loading, status codes, headers and preview handling are Ace Kit's and are tested there. This is the page's
|
|
10
|
+
// own part: how an entry and the notices look.
|
|
11
|
+
const entry = {
|
|
12
|
+
id: 1,
|
|
13
|
+
content_type_id: 1,
|
|
14
|
+
content_type_handle: "pages",
|
|
15
|
+
handle: "about",
|
|
16
|
+
title: "About",
|
|
17
|
+
description: "About us",
|
|
18
|
+
values: {
|
|
19
|
+
headline: "About this site",
|
|
20
|
+
body: {
|
|
21
|
+
type: "doc",
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: "paragraph",
|
|
25
|
+
content: [
|
|
26
|
+
{ type: "text", text: "Read " },
|
|
27
|
+
{ type: "text", text: "more", marks: [{ type: "link", attrs: { href: "/pages/contact" } }] },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const show = (page: unknown) =>
|
|
36
|
+
render(
|
|
37
|
+
<MemoryRouter>
|
|
38
|
+
<Page loaderData={page as never} params={{ "*": "" }} matches={[] as never} />
|
|
39
|
+
</MemoryRouter>,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
describe("the page", () => {
|
|
43
|
+
it("shows an entry's headline, description and body", () => {
|
|
44
|
+
show({ kind: "entry", status: 200, entry, headers: {} });
|
|
45
|
+
expect(screen.getByRole("heading", { name: "About this site" })).toBeTruthy();
|
|
46
|
+
expect(screen.getByText("About us")).toBeTruthy();
|
|
47
|
+
expect(screen.getByRole("link", { name: "more" }).getAttribute("href")).toBe("/pages/contact");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("shows plain text bodies, from before the field was rich text, as paragraphs", () => {
|
|
51
|
+
show({ kind: "entry", status: 200, entry: { ...entry, values: { body: "One\n\nTwo" } }, headers: {} });
|
|
52
|
+
expect(screen.getByText("One")).toBeTruthy();
|
|
53
|
+
expect(screen.getByText("Two")).toBeTruthy();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it.each([
|
|
57
|
+
["not-found", "Page not found"],
|
|
58
|
+
["unavailable", "Temporarily unavailable"],
|
|
59
|
+
["preview-expired", "Preview expired"],
|
|
60
|
+
])("explains a %s", (kind, heading) => {
|
|
61
|
+
show({ kind, status: 404, headers: {} });
|
|
62
|
+
expect(screen.getByRole("heading", { name: heading })).toBeTruthy();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AceNotice, createMeta } from "@sandboxsonny/ace-kit/react-router";
|
|
2
|
+
import { createPageLoader } from "@sandboxsonny/ace-kit/react-router/server";
|
|
3
|
+
|
|
4
|
+
import type { Route } from "./+types/page";
|
|
5
|
+
import { PageContent } from "~/components/page-content";
|
|
6
|
+
import type { PageValues } from "~/types/page";
|
|
7
|
+
|
|
8
|
+
// Addresses are /{content type}/{entry}, as on Shopify, and "/" is the home entry. The loader works out what to
|
|
9
|
+
// show and answers with the right status and headers (a missing page is a real 404; a preview is never cached).
|
|
10
|
+
export const loader = createPageLoader<PageValues>();
|
|
11
|
+
export { headers } from "@sandboxsonny/ace-kit/react-router/server";
|
|
12
|
+
export const meta = createMeta({ siteName: "Example Site" });
|
|
13
|
+
|
|
14
|
+
export default function Page({ loaderData: page }: Route.ComponentProps) {
|
|
15
|
+
return page.kind === "entry" ? (
|
|
16
|
+
<PageContent entry={page.entry} />
|
|
17
|
+
) : (
|
|
18
|
+
<section className="section">
|
|
19
|
+
<AceNotice page={page} className="container" />
|
|
20
|
+
</section>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import eslint from "@eslint/js";
|
|
2
|
+
import globals from "globals";
|
|
3
|
+
import jsxAccessibility from "eslint-plugin-jsx-a11y";
|
|
4
|
+
import react from "eslint-plugin-react";
|
|
5
|
+
import reactHooks from "eslint-plugin-react-hooks";
|
|
6
|
+
import prettierConfig from "eslint-config-prettier";
|
|
7
|
+
import typescriptEslint from "typescript-eslint";
|
|
8
|
+
|
|
9
|
+
export default typescriptEslint.config(
|
|
10
|
+
{ ignores: ["node_modules", "build", ".react-router", "public", "app/ace-kit.generated.d.ts"] },
|
|
11
|
+
|
|
12
|
+
eslint.configs.recommended,
|
|
13
|
+
...typescriptEslint.configs.recommended,
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
files: ["**/*.{ts,tsx}"],
|
|
17
|
+
languageOptions: {
|
|
18
|
+
globals: { ...globals.browser, ...globals.node },
|
|
19
|
+
},
|
|
20
|
+
plugins: {
|
|
21
|
+
react,
|
|
22
|
+
"react-hooks": reactHooks,
|
|
23
|
+
"jsx-a11y": jsxAccessibility,
|
|
24
|
+
},
|
|
25
|
+
settings: { react: { version: "detect" } },
|
|
26
|
+
rules: {
|
|
27
|
+
...react.configs.recommended.rules,
|
|
28
|
+
...react.configs["jsx-runtime"].rules,
|
|
29
|
+
...reactHooks.configs.recommended.rules,
|
|
30
|
+
...jsxAccessibility.flatConfigs.recommended.rules,
|
|
31
|
+
|
|
32
|
+
// Types are imported with `import type` from ~/types (see README "Conventions").
|
|
33
|
+
"@typescript-eslint/consistent-type-imports": ["error", { fixStyle: "separate-type-imports" }],
|
|
34
|
+
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
|
|
35
|
+
|
|
36
|
+
// No abbreviated names in code. Spell it out: `request`, `response`, `message`...
|
|
37
|
+
"id-denylist": [
|
|
38
|
+
"error",
|
|
39
|
+
"req",
|
|
40
|
+
"res",
|
|
41
|
+
"resp",
|
|
42
|
+
"cfg",
|
|
43
|
+
"msg",
|
|
44
|
+
"btn",
|
|
45
|
+
"tmp",
|
|
46
|
+
"idx",
|
|
47
|
+
"evt",
|
|
48
|
+
"cb",
|
|
49
|
+
"fn",
|
|
50
|
+
"opts",
|
|
51
|
+
"args",
|
|
52
|
+
"val",
|
|
53
|
+
"num",
|
|
54
|
+
"str",
|
|
55
|
+
"obj",
|
|
56
|
+
"arr",
|
|
57
|
+
],
|
|
58
|
+
|
|
59
|
+
// Dialogs move focus to their first field on purpose; the pages never autofocus anything else.
|
|
60
|
+
"jsx-a11y/no-autofocus": "off",
|
|
61
|
+
|
|
62
|
+
eqeqeq: ["error", "always"],
|
|
63
|
+
"no-console": ["warn", { allow: ["error", "warn"] }],
|
|
64
|
+
"prefer-const": "error",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Tests cast framework arguments (route args, contexts) that are impractical to build in full.
|
|
69
|
+
{
|
|
70
|
+
files: ["**/*.test.{ts,tsx}"],
|
|
71
|
+
rules: { "@typescript-eslint/no-explicit-any": "off" },
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Must stay last: turns off any rule that would fight with Prettier.
|
|
75
|
+
prettierConfig,
|
|
76
|
+
);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "example-react-router-site",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22.12.0"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "react-router dev --port 4322",
|
|
10
|
+
"build": "react-router build",
|
|
11
|
+
"start": "react-router-serve ./build/server/index.js",
|
|
12
|
+
"generate": "ace-kit generate --out app/ace-kit.generated.d.ts",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"lint:fix": "eslint . --fix",
|
|
15
|
+
"format": "prettier --write .",
|
|
16
|
+
"format:check": "prettier --check .",
|
|
17
|
+
"typecheck": "react-router typegen && tsc",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"check": "npm run format:check && npm run lint && npm run typecheck && npm test && npm run build"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@sandboxsonny/ace-kit": "file:../ace-kit",
|
|
23
|
+
"@react-router/node": "^7.18.4",
|
|
24
|
+
"@react-router/serve": "^7.18.4",
|
|
25
|
+
"isbot": "^5.2.2",
|
|
26
|
+
"react": "^19.3.0",
|
|
27
|
+
"react-dom": "^19.3.0",
|
|
28
|
+
"react-router": "^7.18.4"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@eslint/js": "^9.39.5",
|
|
32
|
+
"@react-router/dev": "^7.18.4",
|
|
33
|
+
"@testing-library/dom": "^10.4.2",
|
|
34
|
+
"@testing-library/react": "^16.3.3",
|
|
35
|
+
"@types/node": "^22.20.5",
|
|
36
|
+
"@types/react": "^19.3.0",
|
|
37
|
+
"@types/react-dom": "^19.3.0",
|
|
38
|
+
"eslint": "^9.39.5",
|
|
39
|
+
"eslint-config-prettier": "^10.1.8",
|
|
40
|
+
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
41
|
+
"eslint-plugin-react": "^7.37.5",
|
|
42
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
43
|
+
"globals": "^17.13.0",
|
|
44
|
+
"jsdom": "^29.1.1",
|
|
45
|
+
"prettier": "^3.9.10",
|
|
46
|
+
"typescript": "^5.9.3",
|
|
47
|
+
"typescript-eslint": "^8.71.1",
|
|
48
|
+
"vite": "^8.3.4",
|
|
49
|
+
"vitest": "^5.0.4"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
|
2
|
+
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
|
3
|
+
<style>
|
|
4
|
+
path { fill: #000; }
|
|
5
|
+
@media (prefers-color-scheme: dark) {
|
|
6
|
+
path { fill: #FFF; }
|
|
7
|
+
}
|
|
8
|
+
</style>
|
|
9
|
+
</svg>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
|
|
3
|
+
"exclude": ["build", "node_modules"],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
|
6
|
+
"types": ["node", "vite/client"],
|
|
7
|
+
"target": "ES2022",
|
|
8
|
+
"module": "ES2022",
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"rootDirs": [".", "./.react-router/types"],
|
|
12
|
+
"paths": {
|
|
13
|
+
"~/*": ["./app/*"]
|
|
14
|
+
},
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"verbatimModuleSyntax": true,
|
|
17
|
+
"noEmit": true,
|
|
18
|
+
"resolveJsonModule": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"strict": true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import ace from "@sandboxsonny/ace-kit/vite";
|
|
2
|
+
import { defineConfig } from "vitest/config";
|
|
3
|
+
|
|
4
|
+
// The React Router Vite plugin is left out on purpose: tests render route modules directly. Ace Kit's plugin makes the
|
|
5
|
+
// tests and the kit share one copy of React Router, which route modules that use the kit's components need.
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [ace()],
|
|
8
|
+
resolve: {
|
|
9
|
+
tsconfigPaths: true,
|
|
10
|
+
},
|
|
11
|
+
test: {
|
|
12
|
+
environment: "jsdom",
|
|
13
|
+
include: ["app/**/*.test.{ts,tsx}"],
|
|
14
|
+
},
|
|
15
|
+
});
|
|
File without changes
|