@sandboxsonny/ace-kit 0.5.0 → 0.7.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/astro-integration.js +1 -1
  3. package/dist/chunk-7M26C36M.js +35 -0
  4. package/dist/{chunk-WM5NOQE3.js → chunk-T7DKI2U3.js} +4 -3
  5. package/dist/cli.js +14 -1179
  6. package/dist/environment-J4VYOHDK.js +223 -0
  7. package/dist/main-AGLPUGNN.js +1733 -0
  8. package/dist/react-router-server.d.ts +1 -1
  9. package/dist/vite.js +1 -1
  10. package/docs/command-line.md +55 -1
  11. package/docs/getting-started.md +8 -0
  12. package/docs/react-router.md +32 -3
  13. package/docs/react.md +5 -2
  14. package/package.json +5 -2
  15. package/templates/astro/.dockerignore +7 -0
  16. package/templates/astro/.github/dependabot.yml +19 -0
  17. package/templates/astro/.github/workflows/ci.yml +23 -0
  18. package/templates/astro/.prettierignore +6 -0
  19. package/templates/astro/.prettierrc.json +20 -0
  20. package/templates/astro/.vscode/extensions.json +4 -0
  21. package/templates/astro/.vscode/launch.json +11 -0
  22. package/templates/astro/AGENTS.md +22 -0
  23. package/templates/astro/Dockerfile +17 -0
  24. package/templates/astro/_gitignore +26 -0
  25. package/templates/astro/astro.config.mjs +18 -0
  26. package/templates/astro/eslint.config.js +56 -0
  27. package/templates/astro/package.json +41 -0
  28. package/templates/astro/public/favicon.ico +0 -0
  29. package/templates/astro/public/favicon.svg +9 -0
  30. package/templates/astro/src/ace-kit.generated.d.ts +24 -0
  31. package/templates/astro/src/components/PageContent.astro +113 -0
  32. package/templates/astro/src/components/SiteFooter.astro +19 -0
  33. package/templates/astro/src/components/SiteHeader.astro +121 -0
  34. package/templates/astro/src/components/sections/Faq.astro +54 -0
  35. package/templates/astro/src/components/sections/Faq.schema.ts +17 -0
  36. package/templates/astro/src/components/sections/Gallery.astro +74 -0
  37. package/templates/astro/src/components/sections/Gallery.schema.ts +20 -0
  38. package/templates/astro/src/components/sections/Hero.astro +58 -0
  39. package/templates/astro/src/components/sections/Hero.schema.ts +14 -0
  40. package/templates/astro/src/layouts/BaseLayout.astro +36 -0
  41. package/templates/astro/src/pages/[...slug].astro +31 -0
  42. package/templates/astro/src/styles/global.css +56 -0
  43. package/templates/astro/src/types/page.d.ts +4 -0
  44. package/templates/astro/tsconfig.json +11 -0
  45. package/templates/astro/vitest.config.ts +4 -0
  46. package/templates/astro.section-types.json +109 -0
  47. package/templates/react-router/.prettierignore +5 -0
  48. package/templates/react-router/.prettierrc.json +11 -0
  49. package/templates/react-router/_gitignore +7 -0
  50. package/templates/react-router/app/ace-kit.generated.d.ts +24 -0
  51. package/templates/react-router/app/app.css +257 -0
  52. package/templates/react-router/app/components/page-content.tsx +55 -0
  53. package/templates/react-router/app/components/sections/faq.schema.ts +17 -0
  54. package/templates/react-router/app/components/sections/faq.tsx +26 -0
  55. package/templates/react-router/app/components/sections/gallery.schema.ts +20 -0
  56. package/templates/react-router/app/components/sections/gallery.tsx +39 -0
  57. package/templates/react-router/app/components/sections/hero.schema.ts +14 -0
  58. package/templates/react-router/app/components/sections/hero.tsx +25 -0
  59. package/templates/react-router/app/components/site-footer.tsx +9 -0
  60. package/templates/react-router/app/components/site-header.tsx +49 -0
  61. package/templates/react-router/app/entry.server.tsx +1 -0
  62. package/templates/react-router/app/root.tsx +75 -0
  63. package/templates/react-router/app/routes/page.test.tsx +118 -0
  64. package/templates/react-router/app/routes/page.tsx +22 -0
  65. package/templates/react-router/app/routes.ts +8 -0
  66. package/templates/react-router/app/types/page.d.ts +4 -0
  67. package/templates/react-router/eslint.config.js +76 -0
  68. package/templates/react-router/package.json +51 -0
  69. package/templates/react-router/public/favicon.svg +9 -0
  70. package/templates/react-router/react-router.config.ts +6 -0
  71. package/templates/react-router/tsconfig.json +22 -0
  72. package/templates/react-router/vite.config.ts +10 -0
  73. package/templates/react-router/vitest.config.ts +15 -0
  74. package/templates/react-router.section-types.json +109 -0
@@ -0,0 +1,257 @@
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
+ }
173
+ .published {
174
+ color: var(--colour-muted);
175
+ font-size: 0.9rem;
176
+ }
177
+ .actions {
178
+ margin-top: 2rem;
179
+ }
180
+
181
+ /* Sections: a hero, an FAQ and a gallery (see components/sections) */
182
+ .hero {
183
+ padding-block: 3rem;
184
+ }
185
+ .hero__inner {
186
+ display: grid;
187
+ gap: 2rem;
188
+ align-items: center;
189
+ }
190
+ @media (min-width: 48rem) {
191
+ .hero__inner--with-picture {
192
+ grid-template-columns: 1fr 1fr;
193
+ }
194
+ }
195
+ .hero h2 {
196
+ margin: 0 0 0.75rem;
197
+ font-size: 2.25rem;
198
+ line-height: 1.15;
199
+ }
200
+ .hero__subheading {
201
+ margin: 0 0 1.5rem;
202
+ color: var(--colour-muted);
203
+ font-size: 1.15rem;
204
+ }
205
+ .hero__picture img {
206
+ border-radius: 0.5rem;
207
+ }
208
+
209
+ .faq {
210
+ max-width: 42rem;
211
+ }
212
+ .faq h2 {
213
+ margin: 0 0 1rem;
214
+ font-size: 1.75rem;
215
+ }
216
+ .faq details {
217
+ border-bottom: 1px solid var(--colour-surface);
218
+ padding-block: 0.75rem;
219
+ }
220
+ .faq summary {
221
+ cursor: pointer;
222
+ font-weight: 600;
223
+ }
224
+ .faq__answer {
225
+ margin-top: 0.5rem;
226
+ color: var(--colour-muted);
227
+ }
228
+ .faq__answer > * + * {
229
+ margin-top: 0.75rem;
230
+ }
231
+
232
+ .gallery__note {
233
+ margin: 0 0 1rem;
234
+ color: var(--colour-muted);
235
+ font-size: 0.9rem;
236
+ }
237
+ .gallery__photos {
238
+ display: grid;
239
+ grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
240
+ gap: 1rem;
241
+ margin: 0;
242
+ padding: 0;
243
+ list-style: none;
244
+ }
245
+ .gallery__photos figure {
246
+ margin: 0;
247
+ }
248
+ .gallery__photos img {
249
+ border-radius: 0.5rem;
250
+ width: 100%;
251
+ object-fit: cover;
252
+ }
253
+ .gallery__photos figcaption {
254
+ margin-top: 0.4rem;
255
+ color: var(--colour-muted);
256
+ font-size: 0.9rem;
257
+ }
@@ -0,0 +1,55 @@
1
+ import { formatAceDate } from "@sandboxsonny/ace-kit";
2
+ import type { AceEntry } from "@sandboxsonny/ace-kit";
3
+ import { AceImage, AceLink, AceRichText, AceSections, AceTextArea } from "@sandboxsonny/ace-kit/react-router";
4
+
5
+ import { Faq } from "~/components/sections/faq";
6
+ import { Gallery } from "~/components/sections/gallery";
7
+ import { Hero } from "~/components/sections/hero";
8
+ import type { PageValues } from "~/types/page";
9
+
10
+ /** An entry of the "Pages" content type: its title (or headline), description, hero image, body, button and sections. */
11
+ export function PageContent({ entry }: { entry: AceEntry<PageValues> }) {
12
+ const { headline, body, hero, published, button, content } = entry.values;
13
+ return (
14
+ <>
15
+ <section className="section">
16
+ <div className="container prose">
17
+ <h1>{headline || entry.title}</h1>
18
+ {entry.description ? <p className="intro">{entry.description}</p> : null}
19
+ {
20
+ // A date field is a day with no time of day, so it is the same day wherever the server runs.
21
+ published ? (
22
+ <p className="published">
23
+ Published <time dateTime={published}>{formatAceDate(published, { locale: "en-GB" }) ?? published}</time>
24
+ </p>
25
+ ) : null
26
+ }
27
+ {hero ? (
28
+ <figure>
29
+ <AceImage image={hero} sizes="(min-width: 64rem) 64rem, 100vw" lazy={false} />
30
+ </figure>
31
+ ) : null}
32
+ {
33
+ // A site made before its Body field was rich text still has plain text, with blank lines between paragraphs.
34
+ typeof body === "string" ? (
35
+ <AceTextArea value={body} />
36
+ ) : body ? (
37
+ <div className="rich-text">
38
+ <AceRichText document={body} />
39
+ </div>
40
+ ) : null
41
+ }
42
+ {
43
+ // A link field: the editor's words and where it goes, already worked out by the CMS.
44
+ button ? (
45
+ <p className="actions">
46
+ <AceLink link={button} className="button" />
47
+ </p>
48
+ ) : null
49
+ }
50
+ </div>
51
+ </section>
52
+ <AceSections sections={content} components={{ hero: Hero, faq: Faq, gallery: Gallery }} />
53
+ </>
54
+ );
55
+ }
@@ -0,0 +1,17 @@
1
+ import { defineSection } from "@sandboxsonny/ace-kit";
2
+
3
+ // An FAQ is a heading and a list of questions. The questions are blocks: the editor adds as many as they like.
4
+ export default defineSection({
5
+ handle: "faq",
6
+ name: "FAQ",
7
+ fields: [{ handle: "heading", label: "Heading", type: "text" }],
8
+ blocks: {
9
+ question: {
10
+ name: "Question",
11
+ fields: [
12
+ { handle: "question", label: "Question", type: "text", required: true },
13
+ { handle: "answer", label: "Answer", type: "richtext" },
14
+ ],
15
+ },
16
+ },
17
+ });
@@ -0,0 +1,26 @@
1
+ import type { SectionOf } from "@sandboxsonny/ace-kit";
2
+ import { AceRichText } from "@sandboxsonny/ace-kit/react-router";
3
+
4
+ import type faq from "./faq.schema";
5
+
6
+ /** An FAQ: a heading and a list of questions, each a block. Its fields and blocks are in faq.schema.ts. */
7
+ export function Faq({ section }: { section: SectionOf<typeof faq> }) {
8
+ const { heading } = section.values;
9
+ return (
10
+ <section className="section" data-section={section.type}>
11
+ <div className="container faq">
12
+ {heading ? <h2>{heading}</h2> : null}
13
+ {section.blocks.map((block) => (
14
+ <details key={block.id}>
15
+ <summary>{block.values.question}</summary>
16
+ {block.values.answer ? (
17
+ <div className="faq__answer">
18
+ <AceRichText document={block.values.answer} />
19
+ </div>
20
+ ) : null}
21
+ </details>
22
+ ))}
23
+ </div>
24
+ </section>
25
+ );
26
+ }
@@ -0,0 +1,20 @@
1
+ import { defineSection } from "@sandboxsonny/ace-kit";
2
+
3
+ // A gallery is a heading, a date and a list of photos. The photos are blocks.
4
+ export default defineSection({
5
+ handle: "gallery",
6
+ name: "Gallery",
7
+ fields: [
8
+ { handle: "heading", label: "Heading", type: "text" },
9
+ { handle: "published", label: "Published", type: "date" },
10
+ ],
11
+ blocks: {
12
+ photo: {
13
+ name: "Photo",
14
+ fields: [
15
+ { handle: "image", label: "Image", type: "image" },
16
+ { handle: "caption", label: "Caption", type: "text" },
17
+ ],
18
+ },
19
+ },
20
+ });
@@ -0,0 +1,39 @@
1
+ import { formatAceDate } from "@sandboxsonny/ace-kit";
2
+ import type { SectionOf } from "@sandboxsonny/ace-kit";
3
+ import { AceImage } from "@sandboxsonny/ace-kit/react-router";
4
+
5
+ import type gallery from "./gallery.schema";
6
+
7
+ /** A gallery: a heading, a date and a list of photos, each a block. Its fields and blocks are in gallery.schema.ts. */
8
+ export function Gallery({ section }: { section: SectionOf<typeof gallery> }) {
9
+ const { heading, published } = section.values;
10
+ const photos = section.blocks.filter((block) => block.values.image || block.values.caption);
11
+ return (
12
+ <section className="section" data-section={section.type}>
13
+ <div className="container">
14
+ {heading ? <h2>{heading}</h2> : null}
15
+ {published ? (
16
+ <p className="gallery__note">
17
+ <time dateTime={published}>{formatAceDate(published, { locale: "en-GB" }) ?? published}</time>
18
+ </p>
19
+ ) : null}
20
+ {photos.length > 0 ? (
21
+ <ul className="gallery__photos">
22
+ {photos.map((photo) => (
23
+ <li key={photo.id}>
24
+ <figure>
25
+ {photo.values.image ? (
26
+ <AceImage image={photo.values.image} sizes="(min-width: 48rem) 20rem, 100vw" />
27
+ ) : null}
28
+ {photo.values.caption ? <figcaption>{photo.values.caption}</figcaption> : null}
29
+ </figure>
30
+ </li>
31
+ ))}
32
+ </ul>
33
+ ) : (
34
+ <p className="gallery__note">No photos yet.</p>
35
+ )}
36
+ </div>
37
+ </section>
38
+ );
39
+ }
@@ -0,0 +1,14 @@
1
+ import { defineSection } from "@sandboxsonny/ace-kit";
2
+
3
+ // What a hero can hold. The CMS learns it from this file (the dev server syncs it when it changes), and Hero.astro's
4
+ // props are typed from it.
5
+ export default defineSection({
6
+ handle: "hero",
7
+ name: "Hero",
8
+ fields: [
9
+ { handle: "heading", label: "Heading", type: "text", required: true },
10
+ { handle: "subheading", label: "Subheading", type: "textarea" },
11
+ { handle: "picture", label: "Picture", type: "image" },
12
+ { handle: "button", label: "Button", type: "link" },
13
+ ],
14
+ });
@@ -0,0 +1,25 @@
1
+ import type { SectionOf } from "@sandboxsonny/ace-kit";
2
+ import { AceImage, AceLink } from "@sandboxsonny/ace-kit/react-router";
3
+
4
+ import type hero from "./hero.schema";
5
+
6
+ /** A hero: a heading, an optional subheading and button, and an optional picture. Its fields are in hero.schema.ts. */
7
+ export function Hero({ section }: { section: SectionOf<typeof hero> }) {
8
+ const { heading, subheading, picture, button } = section.values;
9
+ return (
10
+ <section className="hero section--surface" data-section={section.type}>
11
+ <div className={picture ? "container hero__inner hero__inner--with-picture" : "container hero__inner"}>
12
+ <div>
13
+ <h2>{heading}</h2>
14
+ {subheading ? <p className="hero__subheading">{subheading}</p> : null}
15
+ {button ? <AceLink link={button} className="button" /> : null}
16
+ </div>
17
+ {picture ? (
18
+ <div className="hero__picture">
19
+ <AceImage image={picture} sizes="(min-width: 64rem) 32rem, 100vw" lazy={false} />
20
+ </div>
21
+ ) : null}
22
+ </div>
23
+ </section>
24
+ );
25
+ }
@@ -0,0 +1,9 @@
1
+ export function SiteFooter() {
2
+ return (
3
+ <footer className="site-footer">
4
+ <div className="container">
5
+ <p>© {new Date().getFullYear()} Example Site. A test site for CMS Ace.</p>
6
+ </div>
7
+ </footer>
8
+ );
9
+ }
@@ -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
+ }