@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,118 @@
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 the published date and the button", () => {
51
+ show({
52
+ kind: "entry",
53
+ status: 200,
54
+ entry: {
55
+ ...entry,
56
+ values: { ...entry.values, published: "2026-10-13", button: { href: "/pages/contact", label: "Get in touch" } },
57
+ },
58
+ headers: {},
59
+ });
60
+ expect(screen.getByText("13 October 2026")).toBeTruthy();
61
+ expect(screen.getByRole("link", { name: "Get in touch" }).getAttribute("href")).toBe("/pages/contact");
62
+ });
63
+
64
+ it("draws each section with the component for its kind, in order, and leaves out a kind it has none for", () => {
65
+ const richText = (text: string) => ({
66
+ type: "doc",
67
+ content: [{ type: "paragraph", content: [{ type: "text", text }] }],
68
+ });
69
+ show({
70
+ kind: "entry",
71
+ status: 200,
72
+ entry: {
73
+ ...entry,
74
+ values: {
75
+ ...entry.values,
76
+ content: [
77
+ { id: "a", type: "hero", values: { heading: "Big news", subheading: "Read on" }, blocks: [] },
78
+ {
79
+ id: "b",
80
+ type: "faq",
81
+ values: { heading: "Questions" },
82
+ blocks: [
83
+ { id: "b1", type: "question", values: { question: "Why?", answer: richText("Because.") } },
84
+ { id: "b2", type: "question", values: { question: "How?" } },
85
+ ],
86
+ },
87
+ { id: "c", type: "gallery", values: { heading: "Photos" }, blocks: [] },
88
+ { id: "d", type: "mystery", values: {}, blocks: [] },
89
+ ],
90
+ },
91
+ },
92
+ headers: {},
93
+ });
94
+ expect(screen.getByRole("heading", { name: "Big news" })).toBeTruthy();
95
+ expect(screen.getByText("Read on")).toBeTruthy();
96
+ expect(screen.getByText("Why?")).toBeTruthy();
97
+ expect(screen.getByText("Because.")).toBeTruthy();
98
+ expect(screen.getByText("How?")).toBeTruthy();
99
+ expect(screen.getByText("No photos yet.")).toBeTruthy();
100
+ const kinds = [...document.querySelectorAll("[data-section]")].map((node) => node.getAttribute("data-section"));
101
+ expect(kinds).toEqual(["hero", "faq", "gallery"]);
102
+ });
103
+
104
+ it("shows plain text bodies, from before the field was rich text, as paragraphs", () => {
105
+ show({ kind: "entry", status: 200, entry: { ...entry, values: { body: "One\n\nTwo" } }, headers: {} });
106
+ expect(screen.getByText("One")).toBeTruthy();
107
+ expect(screen.getByText("Two")).toBeTruthy();
108
+ });
109
+
110
+ it.each([
111
+ ["not-found", "Page not found"],
112
+ ["unavailable", "Temporarily unavailable"],
113
+ ["preview-expired", "Preview expired"],
114
+ ])("explains a %s", (kind, heading) => {
115
+ show({ kind, status: 404, headers: {} });
116
+ expect(screen.getByRole("heading", { name: heading })).toBeTruthy();
117
+ });
118
+ });
@@ -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,8 @@
1
+ import { type RouteConfig, index, route } from "@react-router/dev/routes";
2
+
3
+ // One route draws every page: "/" is the home entry and "/{content type}/{entry}" any other. The home page needs its own
4
+ // `index` line: a "*" route doesn't match "/", so with only that line the home page would come out empty.
5
+ export default [
6
+ index("routes/page.tsx", { id: "home" }),
7
+ route("*", "routes/page.tsx", { id: "page" }),
8
+ ] satisfies RouteConfig;
@@ -0,0 +1,4 @@
1
+ import type { AceContentTypes } from "@sandboxsonny/ace-kit";
2
+
3
+ /** The fields of the "Pages" content type, as generated from the CMS (see `npm run generate`). */
4
+ export type PageValues = AceContentTypes["pages"];
@@ -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,6 @@
1
+ import type { Config } from "@react-router/dev/config";
2
+
3
+ export default {
4
+ // Pages are rendered on the server for every request, so they always show the CMS's live content.
5
+ ssr: true,
6
+ } satisfies Config;
@@ -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,10 @@
1
+ import ace from "@sandboxsonny/ace-kit/vite";
2
+ import { reactRouter } from "@react-router/dev/vite";
3
+ import { defineConfig } from "vite";
4
+
5
+ export default defineConfig({
6
+ plugins: [ace(), reactRouter()],
7
+ resolve: {
8
+ tsconfigPaths: true,
9
+ },
10
+ });
@@ -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
+ });
@@ -0,0 +1,109 @@
1
+ {
2
+ "sections": [
3
+ {
4
+ "handle": "faq",
5
+ "name": "FAQ",
6
+ "fields": [
7
+ {
8
+ "handle": "heading",
9
+ "label": "Heading",
10
+ "component_key": "text",
11
+ "config": {}
12
+ }
13
+ ],
14
+ "blocks": [
15
+ {
16
+ "handle": "question",
17
+ "name": "Question",
18
+ "fields": [
19
+ {
20
+ "handle": "question",
21
+ "label": "Question",
22
+ "component_key": "text",
23
+ "config": {
24
+ "required": true
25
+ }
26
+ },
27
+ {
28
+ "handle": "answer",
29
+ "label": "Answer",
30
+ "component_key": "richtext",
31
+ "config": {}
32
+ }
33
+ ]
34
+ }
35
+ ]
36
+ },
37
+ {
38
+ "handle": "gallery",
39
+ "name": "Gallery",
40
+ "fields": [
41
+ {
42
+ "handle": "heading",
43
+ "label": "Heading",
44
+ "component_key": "text",
45
+ "config": {}
46
+ },
47
+ {
48
+ "handle": "published",
49
+ "label": "Published",
50
+ "component_key": "date",
51
+ "config": {}
52
+ }
53
+ ],
54
+ "blocks": [
55
+ {
56
+ "handle": "photo",
57
+ "name": "Photo",
58
+ "fields": [
59
+ {
60
+ "handle": "image",
61
+ "label": "Image",
62
+ "component_key": "image",
63
+ "config": {}
64
+ },
65
+ {
66
+ "handle": "caption",
67
+ "label": "Caption",
68
+ "component_key": "text",
69
+ "config": {}
70
+ }
71
+ ]
72
+ }
73
+ ]
74
+ },
75
+ {
76
+ "handle": "hero",
77
+ "name": "Hero",
78
+ "fields": [
79
+ {
80
+ "handle": "heading",
81
+ "label": "Heading",
82
+ "component_key": "text",
83
+ "config": {
84
+ "required": true
85
+ }
86
+ },
87
+ {
88
+ "handle": "subheading",
89
+ "label": "Subheading",
90
+ "component_key": "textarea",
91
+ "config": {}
92
+ },
93
+ {
94
+ "handle": "picture",
95
+ "label": "Picture",
96
+ "component_key": "image",
97
+ "config": {}
98
+ },
99
+ {
100
+ "handle": "button",
101
+ "label": "Button",
102
+ "component_key": "link",
103
+ "config": {}
104
+ }
105
+ ],
106
+ "blocks": []
107
+ }
108
+ ]
109
+ }