@sandboxsonny/ace-kit 0.0.0-stage → 0.1.1
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 +25 -0
- package/LICENSE +21 -0
- package/README.md +79 -2
- package/astro/AceImage.astro +29 -0
- package/astro/AceMenu.astro +24 -0
- package/astro/AceMenuList.astro +25 -0
- package/astro/AceNotice.astro +19 -0
- package/astro/AcePreviewRefresh.astro +15 -0
- package/astro/AceRichText.astro +12 -0
- package/astro/AceTextArea.astro +14 -0
- package/dist/astro-integration.d.ts +20 -0
- package/dist/astro-integration.js +32 -0
- package/dist/astro-middleware.js +10 -0
- package/dist/astro.d.ts +139 -0
- package/dist/astro.js +24 -0
- package/dist/chunk-KEV3YAWN.js +66 -0
- package/dist/chunk-KLRCWAE4.js +147 -0
- package/dist/chunk-M5WR5TCR.js +16 -0
- package/dist/chunk-URLKW2YS.js +107 -0
- package/dist/chunk-XPZY5R5A.js +16 -0
- package/dist/chunk-YLO6IAXF.js +45 -0
- package/dist/chunk-YN7LJWGI.js +36 -0
- package/dist/cli.js +211 -0
- package/dist/index.d.ts +310 -0
- package/dist/index.js +6 -0
- package/dist/preview-script.js +6 -0
- package/dist/react-router-server.d.ts +225 -0
- package/dist/react-router-server.js +89 -0
- package/dist/react-router.d.ts +155 -0
- package/dist/react-router.js +55 -0
- package/dist/react.d.ts +88 -0
- package/dist/react.js +5 -0
- package/dist/vite.d.ts +17 -0
- package/dist/vite.js +27 -0
- package/docs/api-reference.md +105 -0
- package/docs/astro.md +93 -0
- package/docs/getting-started.md +62 -0
- package/docs/live-preview.md +15 -0
- package/docs/react-router.md +116 -0
- package/docs/react.md +81 -0
- package/docs/releasing.md +45 -0
- package/docs/rich-text.md +54 -0
- package/docs/typed-content.md +50 -0
- package/package.json +158 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @sandboxsonny/ace-kit
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7cee0fb: Maintenance only, no change to the package: releases are now published with npm trusted publishing (GitHub proves its identity to npm) instead of a stored token.
|
|
8
|
+
|
|
9
|
+
## 0.1.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- First release. Everything a website needs to use CMS Ace:
|
|
14
|
+
|
|
15
|
+
- A typed client for the public API (`createAceClient`) with short-lived caching, and errors that tell "not found" apart from "the CMS is unavailable" and "the preview has expired".
|
|
16
|
+
- `loadPage`, which works out what to do for a request (show an entry, redirect `/pages/home` to `/`, not found, unavailable, expired preview) the same way for every framework.
|
|
17
|
+
- `/{content type}/{entry}` addressing, menu links ready to draw, and responsive image attributes (`src`, `srcset`, `sizes`, dimensions, alt text).
|
|
18
|
+
- Rich text: a safe renderer for the editor's documents as HTML, plus Astro and React components.
|
|
19
|
+
- `AceTextArea` (Astro, React, React Router) and `renderTextArea` for plain Textarea fields: blank lines make paragraphs, a new line a line break, or `multiline={false}` for one line of text.
|
|
20
|
+
- Live preview support: framing policy, no-cache headers, and the script that refreshes the page when the editor saves a draft.
|
|
21
|
+
- An Astro integration (`@sandboxsonny/ace-kit/astro/integration`): add `ace()` to `astro.config.mjs` and the settings (`ACE_API_URL`, `ACE_SITE_DOMAIN`, `ACE_PREVIEW_ADMIN_ORIGIN`), the live-preview framing middleware and a ready-made client are in place. Pages then call `getPage(Astro)`, `getEntry`, `getEntries`, `getMenu` and `getMenuLinks` directly, with no client to create.
|
|
22
|
+
- Astro components: `AceImage`, `AceRichText`, `AceMenu` (a whole menu as a `<nav>`), `AceNotice` (the message for a missing, unavailable or expired page) and `AcePreviewRefresh`.
|
|
23
|
+
- React components and hooks (`AceImage`, `AceRichText`, `usePreviewRefresh`).
|
|
24
|
+
- A React Router 7 adapter (`@sandboxsonny/ace-kit/react-router` and `/react-router/server`) and a small Vite plugin (`/vite`): `createPageLoader` (the loader for the one route that draws every page, with real 404, 503, 403 and redirect responses), `createRootLoader` (the header menu and preview detection), `handleRequest` (a server entry that also lets the CMS frame the site for preview), `AceRichText` with router links, `AceNotice`, `createMeta`, and the `useAceMenu` and `useAcePreview` hooks. A site writes only its design: five small files.
|
|
25
|
+
- `ace-kit generate`, which reads a `.env` file in the folder it runs in and writes TypeScript types for your site's content types from the CMS, so `getEntry` and `getEntries` return the right shape. Needs a CMS Ace API that has `GET /public/content-types`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adams Development
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,80 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Ace Kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The website-side toolkit for **CMS Ace**: everything a site needs to read its content, draw it safely and support live preview, for Astro and React Router 7, with plain React and framework-free pieces underneath.
|
|
4
|
+
|
|
5
|
+
- **A typed client** for the CMS Ace public API.
|
|
6
|
+
- **Addressing, menus and images** ready to draw: `/{content type}/{entry}` pages, nested menus, responsive `<img>` attributes.
|
|
7
|
+
- **Rich text** rendered safely (escaped, links checked) as Astro, React or an HTML string.
|
|
8
|
+
- **Live preview** support: the framing header, the no-cache headers and the script that refreshes a page when the editor saves.
|
|
9
|
+
- **Typed content**: `ace-kit generate` writes TypeScript types for your content types, so a change in the CMS shows up as a type error.
|
|
10
|
+
- **No runtime dependencies**, ES modules with types, Node 22.12 or newer.
|
|
11
|
+
|
|
12
|
+
> **You need a CMS Ace API to use it.** CMS Ace is a separate product and is not part of this repository; Ace Kit is the open source client side of it.
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @sandboxsonny/ace-kit
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { createAceClient, loadPage } from "@sandboxsonny/ace-kit";
|
|
20
|
+
|
|
21
|
+
const cms = createAceClient({ apiUrl: "https://api.example.com", siteDomain: "www.example.com" });
|
|
22
|
+
|
|
23
|
+
const page = await loadPage(cms, { slug: "pages/about", url: new URL(request.url) });
|
|
24
|
+
if (page.kind === "entry") console.log(page.entry.title, page.entry.values);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
On Astro there is nothing to wire up by hand: add the integration to `astro.config.mjs` (`integrations: [ace()]`), set `ACE_API_URL` and `ACE_SITE_DOMAIN`, and read the CMS straight from a page:
|
|
28
|
+
|
|
29
|
+
```astro
|
|
30
|
+
---
|
|
31
|
+
import { getEntry } from "@sandboxsonny/ace-kit/astro";
|
|
32
|
+
import AceImage from "@sandboxsonny/ace-kit/astro/AceImage.astro";
|
|
33
|
+
import AceRichText from "@sandboxsonny/ace-kit/astro/AceRichText.astro";
|
|
34
|
+
import AceMenu from "@sandboxsonny/ace-kit/astro/AceMenu.astro";
|
|
35
|
+
|
|
36
|
+
const about = await getEntry("pages", "about");
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
<AceMenu handle="main-menu" />
|
|
40
|
+
<AceImage image={about.values.hero} sizes="100vw" lazy={false} />
|
|
41
|
+
<AceRichText document={about.values.body} />
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Documentation
|
|
45
|
+
|
|
46
|
+
| | |
|
|
47
|
+
| ------------------------------------------ | -------------------------------------------- |
|
|
48
|
+
| [Getting started](docs/getting-started.md) | Install, the client, `loadPage` |
|
|
49
|
+
| [Astro](docs/astro.md) | Settings, middleware, route, components |
|
|
50
|
+
| [React Router](docs/react-router.md) | The React Router 7 adapter: five small files |
|
|
51
|
+
| [React](docs/react.md) | Components for any React framework |
|
|
52
|
+
| [Live preview](docs/live-preview.md) | What a site must do for the editor's preview |
|
|
53
|
+
| [Typed content](docs/typed-content.md) | `ace-kit generate` |
|
|
54
|
+
| [Rich text](docs/rich-text.md) | The document format and how it is drawn |
|
|
55
|
+
| [API reference](docs/api-reference.md) | Every export |
|
|
56
|
+
| [Releasing](docs/releasing.md) | Versions, the changelog and publishing |
|
|
57
|
+
|
|
58
|
+
The same pages, with live examples of the React components, are in Storybook: `npm run storybook`.
|
|
59
|
+
|
|
60
|
+
## Status
|
|
61
|
+
|
|
62
|
+
Ace Kit is **0.x**: it works and is tested, but until 1.0 a minor version may change an API. Every change is in the [changelog](CHANGELOG.md).
|
|
63
|
+
|
|
64
|
+
## Compatibility
|
|
65
|
+
|
|
66
|
+
Node 22.12 or newer. Astro 5 or newer, React 18 or newer, React Router 7 and Vite 5 or newer are optional: install the ones you use. A newer Ace Kit that needs a newer CMS Ace API says so in the changelog.
|
|
67
|
+
|
|
68
|
+
## Development
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npm install
|
|
72
|
+
npm run check # format, lint, types, tests, build, and a check of what would be published
|
|
73
|
+
npm run storybook # the documentation, with live components
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). To report a security problem, see [SECURITY.md](SECURITY.md).
|
|
77
|
+
|
|
78
|
+
## Licence
|
|
79
|
+
|
|
80
|
+
[MIT](LICENSE). The licence covers the code in this repository. It does not give a right to the names or logos "CMS Ace" or "Adams Development", and it does not cover the CMS Ace server.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { imageAttributes } from "../dist/index.js";
|
|
3
|
+
import type { AceImageData } from "../dist/index.js";
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
image: AceImageData;
|
|
7
|
+
/** How wide the image is shown, e.g. "(min-width: 64rem) 64rem, 100vw". The browser uses it to pick a copy from srcset. */
|
|
8
|
+
sizes: string;
|
|
9
|
+
/** Leave on for images below the first screen; turn off for the one at the top of the page. */
|
|
10
|
+
lazy?: boolean;
|
|
11
|
+
class?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const { image, sizes, lazy = true, class: className } = Astro.props;
|
|
15
|
+
const attributes = imageAttributes(image, { sizes, lazy });
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<img
|
|
19
|
+
src={attributes.src}
|
|
20
|
+
srcset={attributes.srcSet}
|
|
21
|
+
sizes={attributes.sizes}
|
|
22
|
+
width={attributes.width}
|
|
23
|
+
height={attributes.height}
|
|
24
|
+
alt={attributes.alt}
|
|
25
|
+
loading={attributes.loading}
|
|
26
|
+
decoding={attributes.decoding}
|
|
27
|
+
fetchpriority={attributes.fetchPriority}
|
|
28
|
+
class={className}
|
|
29
|
+
/>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { AceNavigationLink } from "../dist/index.js";
|
|
3
|
+
import { getMenuLinks } from "../dist/astro.js";
|
|
4
|
+
import AceMenuList from "./AceMenuList.astro";
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
/** The handle of the menu in the CMS, e.g. "main-menu". */
|
|
8
|
+
handle: string;
|
|
9
|
+
/** Shown if the site has no such menu yet, or the CMS can't be reached. */
|
|
10
|
+
fallback?: AceNavigationLink[];
|
|
11
|
+
/** What the navigation is called for people using screen readers. */
|
|
12
|
+
label?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const { handle, fallback = [], label = "Main", class: className } = Astro.props;
|
|
17
|
+
const links = await getMenuLinks(handle, { fallback });
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
{links.length > 0 && (
|
|
21
|
+
<nav aria-label={label} class:list={["ace-menu", className]}>
|
|
22
|
+
<AceMenuList links={links} pathname={Astro.url.pathname} />
|
|
23
|
+
</nav>
|
|
24
|
+
)}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { isCurrentPage } from "../dist/index.js";
|
|
3
|
+
import type { AceNavigationLink } from "../dist/index.js";
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
links: AceNavigationLink[];
|
|
7
|
+
pathname: string;
|
|
8
|
+
depth?: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const { links, pathname, depth = 0 } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<ul class={depth === 0 ? "ace-menu__list" : "ace-menu__sublist"}>
|
|
15
|
+
{links.map((link) => (
|
|
16
|
+
<li>
|
|
17
|
+
<a href={link.to} aria-current={isCurrentPage(link, pathname) ? "page" : undefined}>
|
|
18
|
+
{link.label}
|
|
19
|
+
</a>
|
|
20
|
+
{link.children && link.children.length > 0 && (
|
|
21
|
+
<Astro.self links={link.children} pathname={pathname} depth={depth + 1} />
|
|
22
|
+
)}
|
|
23
|
+
</li>
|
|
24
|
+
))}
|
|
25
|
+
</ul>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { pageNotices } from "../dist/index.js";
|
|
3
|
+
import type { AcePage } from "../dist/index.js";
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
/** The answer from `getPage` when it isn't an entry. */
|
|
7
|
+
page: Exclude<AcePage, { kind: "entry" }>;
|
|
8
|
+
class?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const { page, class: className } = Astro.props;
|
|
12
|
+
const { heading, message } = pageNotices[page.kind];
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<div class={className}>
|
|
16
|
+
<h1>{heading}</h1>
|
|
17
|
+
<p>{message}</p>
|
|
18
|
+
{page.kind !== "preview-expired" && <a href="/">Back to the home page</a>}
|
|
19
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { ACE_PREVIEW_ADMIN_ORIGIN } from "astro:env/server";
|
|
3
|
+
|
|
4
|
+
import { previewTokenFrom } from "../dist/index.js";
|
|
5
|
+
|
|
6
|
+
// Only a page opened by the CMS's editor (it carries a preview token) listens for the editor's messages.
|
|
7
|
+
const isPreview = previewTokenFrom(Astro.url) !== null;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
{isPreview && <div data-ace-preview-origin={ACE_PREVIEW_ADMIN_ORIGIN} hidden />}
|
|
11
|
+
|
|
12
|
+
<!-- In the CMS preview frame, this refetches the page when the editor saves a draft. It does nothing elsewhere. -->
|
|
13
|
+
<script>
|
|
14
|
+
import "../dist/preview-script.js";
|
|
15
|
+
</script>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { renderRichText } from "../dist/index.js";
|
|
3
|
+
import type { AceRichTextDocument } from "../dist/index.js";
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
document: AceRichTextDocument;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const { document } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<Fragment set:html={renderRichText(document)} />
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { renderTextArea } from "../dist/index.js";
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
/** The text area field's value. Missing or blank draws nothing. */
|
|
6
|
+
value: string | undefined;
|
|
7
|
+
/** True (the default): blank lines start new paragraphs and a single new line is a line break. False: one line of text. */
|
|
8
|
+
multiline?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const { value, multiline = true } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<Fragment set:html={renderTextArea(value, { multiline })} />
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AstroIntegration } from 'astro';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The Astro integration. Add it once to `astro.config.mjs` and a site is wired up:
|
|
5
|
+
*
|
|
6
|
+
* import ace from "@sandboxsonny/ace-kit/astro/integration";
|
|
7
|
+
* export default defineConfig({ output: "server", integrations: [ace()] });
|
|
8
|
+
*
|
|
9
|
+
* It declares the three settings below, and lets the CMS's admin app (and only it) frame the site for live preview.
|
|
10
|
+
* The settings are read when the server starts, not baked into the build, so one build runs anywhere.
|
|
11
|
+
*
|
|
12
|
+
* | Setting | Default | Meaning |
|
|
13
|
+
* | -------------------------- | ----------------------- | -------------------------------------------------------- |
|
|
14
|
+
* | `ACE_API_URL` | `http://localhost:8080` | where the CMS Ace API lives |
|
|
15
|
+
* | `ACE_SITE_DOMAIN` | `localhost` | the domain this site has in the CMS |
|
|
16
|
+
* | `ACE_PREVIEW_ADMIN_ORIGIN` | `http://localhost:5173` | where the CMS admin app runs (may frame the site) |
|
|
17
|
+
*/
|
|
18
|
+
declare function ace(): AstroIntegration;
|
|
19
|
+
|
|
20
|
+
export { ace as default };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { envField } from 'astro/config';
|
|
2
|
+
|
|
3
|
+
// src/astro/integration.ts
|
|
4
|
+
function ace() {
|
|
5
|
+
return {
|
|
6
|
+
name: "ace-kit",
|
|
7
|
+
hooks: {
|
|
8
|
+
"astro:config:setup": ({ updateConfig, addMiddleware }) => {
|
|
9
|
+
updateConfig({
|
|
10
|
+
env: {
|
|
11
|
+
schema: {
|
|
12
|
+
// "secret" only because Astro reads secrets when the server starts, while "public" values are baked into the build.
|
|
13
|
+
ACE_API_URL: envField.string({ context: "server", access: "secret", default: "http://localhost:8080" }),
|
|
14
|
+
ACE_SITE_DOMAIN: envField.string({ context: "server", access: "secret", default: "localhost" }),
|
|
15
|
+
ACE_PREVIEW_ADMIN_ORIGIN: envField.string({
|
|
16
|
+
context: "server",
|
|
17
|
+
access: "secret",
|
|
18
|
+
default: "http://localhost:5173"
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
// The kit reads those settings through `astro:env`, which only Vite understands, so Vite must process the
|
|
23
|
+
// kit itself rather than leave it to Node.
|
|
24
|
+
vite: { ssr: { noExternal: ["@sandboxsonny/ace-kit"] } }
|
|
25
|
+
});
|
|
26
|
+
addMiddleware({ entrypoint: "@sandboxsonny/ace-kit/astro/middleware", order: "pre" });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { ace as default };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { addFramingPolicy } from './chunk-YLO6IAXF.js';
|
|
2
|
+
import { ACE_PREVIEW_ADMIN_ORIGIN } from 'astro:env/server';
|
|
3
|
+
|
|
4
|
+
async function onRequest(_context, next) {
|
|
5
|
+
const response = await next();
|
|
6
|
+
addFramingPolicy(response.headers, ACE_PREVIEW_ADMIN_ORIGIN);
|
|
7
|
+
return response;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { onRequest };
|
package/dist/astro.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/** A field of a content type as `GET /public/content-types` describes it. */
|
|
2
|
+
type AceFieldSchema = {
|
|
3
|
+
handle: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** `text`, `textarea`, `select`, `number`, `checkbox`, `image` or `richtext`. */
|
|
6
|
+
component_key: string;
|
|
7
|
+
config: { required?: boolean; options?: string[] } | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** A content type as `GET /public/content-types` describes it. */
|
|
11
|
+
type AceContentTypeSchema = {
|
|
12
|
+
handle: string;
|
|
13
|
+
name: string;
|
|
14
|
+
fields: AceFieldSchema[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** One link of a menu as the public API returns it, with its address already worked out. */
|
|
18
|
+
type AceMenuItem = {
|
|
19
|
+
label: string;
|
|
20
|
+
/** A path on this site for a link to an entry, or the address as typed. */
|
|
21
|
+
url: string;
|
|
22
|
+
kind: "entry" | "url";
|
|
23
|
+
/** The entry an entry link goes to; null for a typed address. */
|
|
24
|
+
entry: { id: number; title: string; handle: string; content_type_handle: string } | null;
|
|
25
|
+
children: AceMenuItem[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type AceMenu = {
|
|
29
|
+
name: string;
|
|
30
|
+
handle: string;
|
|
31
|
+
items: AceMenuItem[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** One link in a navigation bar, ready to draw. */
|
|
35
|
+
type AceNavigationLink = {
|
|
36
|
+
label: string;
|
|
37
|
+
to: string;
|
|
38
|
+
/** Links shown under this one. */
|
|
39
|
+
children?: AceNavigationLink[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** A live entry from the public API. `Values` is the shape of its content type's fields. */
|
|
43
|
+
type AceEntry<Values = Record<string, unknown>> = {
|
|
44
|
+
id: number;
|
|
45
|
+
content_type_id: number;
|
|
46
|
+
/** The content type's handle, which is the first part of the entry's address. */
|
|
47
|
+
content_type_handle: string;
|
|
48
|
+
title: string;
|
|
49
|
+
handle: string;
|
|
50
|
+
description: string;
|
|
51
|
+
values: Values;
|
|
52
|
+
created_at: string;
|
|
53
|
+
updated_at: string;
|
|
54
|
+
/** Only on a preview: true when the entry has a draft and this is it. */
|
|
55
|
+
is_draft?: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The shape of each content type's values, keyed by the content type's handle. It starts empty; running
|
|
60
|
+
* `ace-kit generate` fills it in for your site (by declaration merging), and the client then returns the right
|
|
61
|
+
* type for each content type.
|
|
62
|
+
*/
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
64
|
+
interface AceContentTypes {}
|
|
65
|
+
|
|
66
|
+
/** The handles of your content types once types are generated; any text before that. */
|
|
67
|
+
type AceContentTypeHandle = keyof AceContentTypes extends never
|
|
68
|
+
? string
|
|
69
|
+
: Extract<keyof AceContentTypes, string>;
|
|
70
|
+
|
|
71
|
+
/** The values of a content type's entries: what was generated for it, or loose before that. */
|
|
72
|
+
type AceValuesOf<Handle extends string> = Handle extends keyof AceContentTypes
|
|
73
|
+
? AceContentTypes[Handle]
|
|
74
|
+
: Record<string, unknown>;
|
|
75
|
+
|
|
76
|
+
/** What to do with a request for a page: show an entry, redirect, or explain. */
|
|
77
|
+
type AcePage<Values = Record<string, unknown>> =
|
|
78
|
+
| { kind: "entry"; status: 200; entry: AceEntry<Values>; headers: Record<string, string> }
|
|
79
|
+
| { kind: "redirect"; status: 301 | 302; location: string; headers: Record<string, string> }
|
|
80
|
+
| { kind: "not-found"; status: 404; headers: Record<string, string> }
|
|
81
|
+
| { kind: "unavailable"; status: 503; headers: Record<string, string> }
|
|
82
|
+
| { kind: "preview-expired"; status: 403; headers: Record<string, string> };
|
|
83
|
+
|
|
84
|
+
/** The client for this site, made from the integration's settings. Most pages only need the functions below. */
|
|
85
|
+
declare const ace: {
|
|
86
|
+
getEntry<Handle extends AceContentTypeHandle>(contentTypeHandle: Handle, handle: string, options?: {
|
|
87
|
+
previewToken?: string | null;
|
|
88
|
+
}): Promise<AceEntry<AceValuesOf<Handle>> | null>;
|
|
89
|
+
getEntries<Handle extends AceContentTypeHandle>(contentTypeHandle: Handle, options?: {
|
|
90
|
+
limit?: number;
|
|
91
|
+
}): Promise<AceEntry<AceValuesOf<Handle>>[]>;
|
|
92
|
+
getMenu(handle: string): Promise<AceMenu | null>;
|
|
93
|
+
getContentTypes(): Promise<AceContentTypeSchema[]>;
|
|
94
|
+
};
|
|
95
|
+
/** One live entry of a content type by its handle, or `null`. See `createAceClient`. */
|
|
96
|
+
declare const getEntry: <Handle extends AceContentTypeHandle>(contentTypeHandle: Handle, handle: string, options?: {
|
|
97
|
+
previewToken?: string | null;
|
|
98
|
+
}) => Promise<AceEntry<AceValuesOf<Handle>> | null>;
|
|
99
|
+
/** A content type's live entries, newest first. */
|
|
100
|
+
declare const getEntries: <Handle extends AceContentTypeHandle>(contentTypeHandle: Handle, options?: {
|
|
101
|
+
limit?: number;
|
|
102
|
+
}) => Promise<AceEntry<AceValuesOf<Handle>>[]>;
|
|
103
|
+
/** A navigation menu with every link worked out, or `null`. */
|
|
104
|
+
declare const getMenu: (handle: string) => Promise<AceMenu | null>;
|
|
105
|
+
/**
|
|
106
|
+
* A menu's links, ready to draw: `[{ label, to, children? }]`. If the site has no such menu, or the CMS can't be
|
|
107
|
+
* reached, you get `fallback` instead, so a missing menu never takes a page down.
|
|
108
|
+
*
|
|
109
|
+
* const links = await getMenuLinks("main-menu", { fallback: [{ label: "Home", to: "/" }] });
|
|
110
|
+
*/
|
|
111
|
+
declare function getMenuLinks(handle: string, options?: {
|
|
112
|
+
fallback?: AceNavigationLink[];
|
|
113
|
+
}): Promise<AceNavigationLink[]>;
|
|
114
|
+
/** Puts a page's status and headers (no caching or indexing for previews, and so on) on Astro's response. */
|
|
115
|
+
declare function applyPage(response: {
|
|
116
|
+
status?: number;
|
|
117
|
+
headers: Headers;
|
|
118
|
+
}, page: {
|
|
119
|
+
status: number;
|
|
120
|
+
headers: Record<string, string>;
|
|
121
|
+
}): void;
|
|
122
|
+
/**
|
|
123
|
+
* What to show for the page being requested, from the address alone (any catch-all route will do). It also sets
|
|
124
|
+
* the response's status and headers. Only a redirect is left to you, because Astro makes only a page do that:
|
|
125
|
+
*
|
|
126
|
+
* const page = await getPage(Astro);
|
|
127
|
+
* if (page.kind === "redirect") return Astro.redirect(page.location, page.status);
|
|
128
|
+
*
|
|
129
|
+
* `page.kind` is `entry` (see `page.entry`), `not-found`, `unavailable`, `preview-expired` or `redirect`.
|
|
130
|
+
*/
|
|
131
|
+
declare function getPage<Values = Record<string, unknown>>(astro: {
|
|
132
|
+
url: URL;
|
|
133
|
+
response: {
|
|
134
|
+
status?: number;
|
|
135
|
+
headers: Headers;
|
|
136
|
+
};
|
|
137
|
+
}): Promise<AcePage<Values>>;
|
|
138
|
+
|
|
139
|
+
export { ace, applyPage, getEntries, getEntry, getMenu, getMenuLinks, getPage };
|
package/dist/astro.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createAceClient, loadMenuLinks, loadPage } from './chunk-KLRCWAE4.js';
|
|
2
|
+
import './chunk-M5WR5TCR.js';
|
|
3
|
+
import './chunk-YLO6IAXF.js';
|
|
4
|
+
import { ACE_SITE_DOMAIN, ACE_API_URL } from 'astro:env/server';
|
|
5
|
+
|
|
6
|
+
var ace = createAceClient({ apiUrl: ACE_API_URL, siteDomain: ACE_SITE_DOMAIN });
|
|
7
|
+
var getEntry = ace.getEntry;
|
|
8
|
+
var getEntries = ace.getEntries;
|
|
9
|
+
var getMenu = ace.getMenu;
|
|
10
|
+
function getMenuLinks(handle, options = {}) {
|
|
11
|
+
return loadMenuLinks(ace, handle, options.fallback ?? []);
|
|
12
|
+
}
|
|
13
|
+
function applyPage(response, page) {
|
|
14
|
+
response.status = page.status;
|
|
15
|
+
for (const [name, value] of Object.entries(page.headers)) response.headers.set(name, value);
|
|
16
|
+
}
|
|
17
|
+
async function getPage(astro) {
|
|
18
|
+
const slug = astro.url.pathname.replace(/^\/+|\/+$/g, "");
|
|
19
|
+
const page = await loadPage(ace, { slug, url: astro.url });
|
|
20
|
+
applyPage(astro.response, page);
|
|
21
|
+
return page;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { ace, applyPage, getEntries, getEntry, getMenu, getMenuLinks, getPage };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { imageAttributes, textAreaSingleLine, textAreaParagraphs, textAreaLines, richTextImageSizes, listStart, headingLevel, linkAddress } from './chunk-URLKW2YS.js';
|
|
2
|
+
import { listenForPreviewRefresh } from './chunk-YN7LJWGI.js';
|
|
3
|
+
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
4
|
+
import { Fragment as Fragment$1, useEffect } from 'react';
|
|
5
|
+
|
|
6
|
+
function AceImage({ image, sizes, lazy, ...rest }) {
|
|
7
|
+
const attributes = imageAttributes(image, { sizes, lazy });
|
|
8
|
+
return /* @__PURE__ */ jsx("img", { ...rest, ...attributes });
|
|
9
|
+
}
|
|
10
|
+
var PlainLink = ({ href, children }) => /* @__PURE__ */ jsx("a", { href, children });
|
|
11
|
+
function renderText(node, Link) {
|
|
12
|
+
return (node.marks ?? []).reduce((inner, mark) => {
|
|
13
|
+
if (mark.type === "bold") return /* @__PURE__ */ jsx("strong", { children: inner });
|
|
14
|
+
if (mark.type === "italic") return /* @__PURE__ */ jsx("em", { children: inner });
|
|
15
|
+
const address = linkAddress(mark.attrs.href);
|
|
16
|
+
return address ? /* @__PURE__ */ jsx(Link, { href: address, children: inner }) : inner;
|
|
17
|
+
}, node.text ?? "");
|
|
18
|
+
}
|
|
19
|
+
function renderNodes(nodes, Link) {
|
|
20
|
+
return (nodes ?? []).map((node, index) => /* @__PURE__ */ jsx(Node, { node, Link }, index));
|
|
21
|
+
}
|
|
22
|
+
function Node({ node, Link }) {
|
|
23
|
+
const children = renderNodes(node.content, Link);
|
|
24
|
+
switch (node.type) {
|
|
25
|
+
case "text":
|
|
26
|
+
return renderText(node, Link);
|
|
27
|
+
case "hardBreak":
|
|
28
|
+
return /* @__PURE__ */ jsx("br", {});
|
|
29
|
+
case "paragraph":
|
|
30
|
+
return /* @__PURE__ */ jsx("p", { children });
|
|
31
|
+
case "heading": {
|
|
32
|
+
const Heading = `h${headingLevel(node)}`;
|
|
33
|
+
return /* @__PURE__ */ jsx(Heading, { children });
|
|
34
|
+
}
|
|
35
|
+
case "bulletList":
|
|
36
|
+
return /* @__PURE__ */ jsx("ul", { children });
|
|
37
|
+
case "orderedList":
|
|
38
|
+
return /* @__PURE__ */ jsx("ol", { start: listStart(node), children });
|
|
39
|
+
case "listItem":
|
|
40
|
+
return /* @__PURE__ */ jsx("li", { children });
|
|
41
|
+
case "blockquote":
|
|
42
|
+
return /* @__PURE__ */ jsx("blockquote", { children });
|
|
43
|
+
case "image":
|
|
44
|
+
return node.attrs?.image ? /* @__PURE__ */ jsx("figure", { children: /* @__PURE__ */ jsx("img", { ...imageAttributes(node.attrs.image, { sizes: richTextImageSizes }) }) }) : null;
|
|
45
|
+
default:
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function AceRichText({ document, link = PlainLink }) {
|
|
50
|
+
return /* @__PURE__ */ jsx(Fragment, { children: renderNodes(document.content, link) });
|
|
51
|
+
}
|
|
52
|
+
function AceTextArea({ value, multiline = true }) {
|
|
53
|
+
if (!multiline) return /* @__PURE__ */ jsx(Fragment, { children: textAreaSingleLine(value) });
|
|
54
|
+
return /* @__PURE__ */ jsx(Fragment, { children: textAreaParagraphs(value).map((paragraph, paragraphIndex) => /* @__PURE__ */ jsx("p", { children: textAreaLines(paragraph).map((line, lineIndex) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
55
|
+
lineIndex > 0 ? /* @__PURE__ */ jsx("br", {}) : null,
|
|
56
|
+
line
|
|
57
|
+
] }, lineIndex)) }, paragraphIndex)) });
|
|
58
|
+
}
|
|
59
|
+
function usePreviewRefresh(adminOrigin, refresh) {
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!adminOrigin) return;
|
|
62
|
+
return listenForPreviewRefresh(adminOrigin, refresh);
|
|
63
|
+
}, [adminOrigin, refresh]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { AceImage, AceRichText, AceTextArea, usePreviewRefresh };
|