@getrefino/react 0.1.0-rc.4 → 0.1.0-rc.6
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/dist/ArticleBody.d.ts +27 -0
- package/dist/ArticleBody.js +50 -0
- package/dist/ArticlePanel.d.ts +7 -0
- package/dist/ArticlePanel.js +93 -0
- package/dist/RefinoArticle.d.ts +37 -0
- package/dist/RefinoArticle.js +43 -0
- package/dist/RefinoProvider.d.ts +8 -0
- package/dist/RefinoProvider.js +23 -6
- package/dist/Toolbar.js +11 -3
- package/dist/UpdateNotice.d.ts +51 -0
- package/dist/UpdateNotice.js +136 -0
- package/dist/context.d.ts +3 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +3 -0
- package/dist/store.d.ts +41 -2
- package/dist/store.js +190 -42
- package/dist/styles.d.ts +1 -1
- package/dist/styles.js +191 -0
- package/dist/telemetry.d.ts +10 -1
- package/dist/updates.d.ts +49 -0
- package/dist/updates.js +52 -0
- package/dist/useCopyEditor.d.ts +8 -1
- package/dist/useCopyEditor.js +7 -2
- package/package.json +2 -2
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AllHTMLAttributes, JSX, ReactNode } from "react";
|
|
2
|
+
export type ArticleBodyElement = keyof JSX.IntrinsicElements;
|
|
3
|
+
export interface ArticleBodyProps extends Omit<AllHTMLAttributes<HTMLElement>, "id" | "as" | "children" | "contentEditable" | "dangerouslySetInnerHTML"> {
|
|
4
|
+
/** The body field's id, e.g. `blog.hello-world.body`. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Element to render. Defaults to `div`; use the element the article already had. */
|
|
7
|
+
as?: ArticleBodyElement | undefined;
|
|
8
|
+
/** DOM `id` attribute, since `id` is taken by the field id. */
|
|
9
|
+
htmlId?: string | undefined;
|
|
10
|
+
/** The article as the site renders it. Untouched in both modes. */
|
|
11
|
+
children?: ReactNode | undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Marks the rendered article body as editable.
|
|
15
|
+
*
|
|
16
|
+
* In normal mode this is exactly the element the site already had, with the
|
|
17
|
+
* site's own rendered Markdown or MDX inside it: no wrapper, no attributes,
|
|
18
|
+
* no editor code path. In edit mode the same element gains an outline and a
|
|
19
|
+
* button that opens the source editor for the body.
|
|
20
|
+
*
|
|
21
|
+
* A body is not a sentence, so it deliberately does not become
|
|
22
|
+
* contentEditable the way `EditableText` does. Editing raw Markdown or MDX
|
|
23
|
+
* in place — next to JSX, imports and fenced code the renderer has already
|
|
24
|
+
* consumed — would be a different document from the one the reader sees.
|
|
25
|
+
* The panel edits the source; the page keeps rendering the article.
|
|
26
|
+
*/
|
|
27
|
+
export declare function ArticleBody(props: ArticleBodyProps): JSX.Element;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { splitDocumentFieldId } from "@getrefino/core";
|
|
4
|
+
import { createElement, useCallback, useSyncExternalStore } from "react";
|
|
5
|
+
import { useCopyContext } from "./context.js";
|
|
6
|
+
/**
|
|
7
|
+
* Marks the rendered article body as editable.
|
|
8
|
+
*
|
|
9
|
+
* In normal mode this is exactly the element the site already had, with the
|
|
10
|
+
* site's own rendered Markdown or MDX inside it: no wrapper, no attributes,
|
|
11
|
+
* no editor code path. In edit mode the same element gains an outline and a
|
|
12
|
+
* button that opens the source editor for the body.
|
|
13
|
+
*
|
|
14
|
+
* A body is not a sentence, so it deliberately does not become
|
|
15
|
+
* contentEditable the way `EditableText` does. Editing raw Markdown or MDX
|
|
16
|
+
* in place — next to JSX, imports and fenced code the renderer has already
|
|
17
|
+
* consumed — would be a different document from the one the reader sees.
|
|
18
|
+
* The panel edits the source; the page keeps rendering the article.
|
|
19
|
+
*/
|
|
20
|
+
export function ArticleBody(props) {
|
|
21
|
+
const { id, as = "div", htmlId, children, ...rest } = props;
|
|
22
|
+
const context = useCopyContext();
|
|
23
|
+
const store = context?.store ?? null;
|
|
24
|
+
const subscribe = store ? store.subscribe : noopSubscribe;
|
|
25
|
+
const editing = useSyncExternalStore(subscribe, () => store?.getState().editing ?? false, () => false);
|
|
26
|
+
if (!store || !editing) {
|
|
27
|
+
return createElement(as, { ...rest, id: htmlId }, children);
|
|
28
|
+
}
|
|
29
|
+
return (_jsx(EditableArticleBody, { id: id, as: as, htmlId: htmlId, rest: rest, store: store, children: children }));
|
|
30
|
+
}
|
|
31
|
+
function noopSubscribe() {
|
|
32
|
+
return () => { };
|
|
33
|
+
}
|
|
34
|
+
function EditableArticleBody({ id, as, htmlId, rest, store, children }) {
|
|
35
|
+
const documentId = useSyncExternalStore(store.subscribe, () => splitDocumentFieldId(store.getState().documentIds, id)?.documentId ?? null, () => null);
|
|
36
|
+
const dirty = useSyncExternalStore(store.subscribe, () => store.isDirty(id), () => false);
|
|
37
|
+
const open = useCallback(() => {
|
|
38
|
+
if (documentId)
|
|
39
|
+
store.openPanel({ kind: "body", documentId });
|
|
40
|
+
}, [documentId, store]);
|
|
41
|
+
if (!documentId) {
|
|
42
|
+
return createElement(as, { ...rest, id: htmlId }, children);
|
|
43
|
+
}
|
|
44
|
+
return createElement(as, {
|
|
45
|
+
...rest,
|
|
46
|
+
id: htmlId,
|
|
47
|
+
"data-refino-body": id,
|
|
48
|
+
"data-refino-state": dirty ? "dirty" : "idle",
|
|
49
|
+
}, _jsx("button", { type: "button", className: "refino-body__edit", onClick: open, children: dirty ? "Edit article body — unsaved" : "Edit article body" }), children);
|
|
50
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The two editing surfaces that are not the rendered page: the article's
|
|
4
|
+
* metadata, and the article's source body. Rendered once by the provider,
|
|
5
|
+
* over the page, and only while edit mode is on.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ArticlePanel(): JSX.Element | null;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { BODY_FIELD_ID, META_FIELD_PREFIX, documentFieldId } from "@getrefino/core";
|
|
4
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
5
|
+
import { useCopyEditorState, useCopyStoreOrThrow } from "./context.js";
|
|
6
|
+
const LABELS = {
|
|
7
|
+
title: "Title",
|
|
8
|
+
subtitle: "Subtitle",
|
|
9
|
+
excerpt: "Excerpt",
|
|
10
|
+
description: "Description",
|
|
11
|
+
seoTitle: "SEO title",
|
|
12
|
+
seoDescription: "SEO description",
|
|
13
|
+
author: "Author",
|
|
14
|
+
date: "Date",
|
|
15
|
+
ctaLabel: "CTA label",
|
|
16
|
+
ctaText: "CTA text",
|
|
17
|
+
};
|
|
18
|
+
/** Fields that are usually a sentence or two rather than a line. */
|
|
19
|
+
const MULTILINE = new Set(["excerpt", "description", "seoDescription"]);
|
|
20
|
+
function fieldName(field) {
|
|
21
|
+
return field.id.startsWith(META_FIELD_PREFIX) ? field.id.slice(META_FIELD_PREFIX.length) : field.id;
|
|
22
|
+
}
|
|
23
|
+
function labelFor(field) {
|
|
24
|
+
const name = fieldName(field);
|
|
25
|
+
return LABELS[name] ?? name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (char) => char.toUpperCase());
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The two editing surfaces that are not the rendered page: the article's
|
|
29
|
+
* metadata, and the article's source body. Rendered once by the provider,
|
|
30
|
+
* over the page, and only while edit mode is on.
|
|
31
|
+
*/
|
|
32
|
+
export function ArticlePanel() {
|
|
33
|
+
const { store } = useCopyStoreOrThrow();
|
|
34
|
+
const state = useCopyEditorState(store);
|
|
35
|
+
const panel = state.panel;
|
|
36
|
+
const article = panel ? state.documents[panel.documentId] : undefined;
|
|
37
|
+
const close = useCallback(() => store.closePanel(), [store]);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!panel || typeof window === "undefined")
|
|
40
|
+
return;
|
|
41
|
+
const onKeyDown = (event) => {
|
|
42
|
+
if (event.key === "Escape") {
|
|
43
|
+
event.stopPropagation();
|
|
44
|
+
store.closePanel();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
window.addEventListener("keydown", onKeyDown, true);
|
|
48
|
+
return () => window.removeEventListener("keydown", onKeyDown, true);
|
|
49
|
+
}, [panel, store]);
|
|
50
|
+
if (!panel || !article)
|
|
51
|
+
return null;
|
|
52
|
+
const heading = panel.kind === "body" ? "Article body" : "Article metadata";
|
|
53
|
+
return (_jsxs("div", { className: "refino-panel", role: "dialog", "aria-modal": "false", "aria-label": heading, children: [_jsxs("div", { className: "refino-panel__head", children: [_jsxs("div", { children: [_jsx("strong", { className: "refino-panel__title", children: heading }), article.path && (_jsx("span", { className: "refino-panel__path", title: article.path, children: article.path }))] }), _jsx("button", { type: "button", className: "refino-panel__close", onClick: close, children: "Done" })] }), panel.kind === "body" ? _jsx(BodyEditor, { article: article }) : _jsx(MetadataEditor, { article: article })] }));
|
|
54
|
+
}
|
|
55
|
+
/** One field of the draft, subscribed to on its own so typing re-renders one input. */
|
|
56
|
+
function useField(id) {
|
|
57
|
+
const { store } = useCopyStoreOrThrow();
|
|
58
|
+
const value = useSyncExternalStore(store.subscribe, () => store.getValue(id) ?? "", () => store.getValue(id) ?? "");
|
|
59
|
+
const dirty = useSyncExternalStore(store.subscribe, () => store.isDirty(id), () => false);
|
|
60
|
+
return { value, dirty };
|
|
61
|
+
}
|
|
62
|
+
function BodyEditor({ article }) {
|
|
63
|
+
const { store } = useCopyStoreOrThrow();
|
|
64
|
+
const id = documentFieldId(article.documentId, BODY_FIELD_ID);
|
|
65
|
+
const { value, dirty } = useField(id);
|
|
66
|
+
const field = article.fields.find((candidate) => candidate.id === BODY_FIELD_ID);
|
|
67
|
+
const textarea = useRef(null);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
textarea.current?.focus();
|
|
70
|
+
}, []);
|
|
71
|
+
if (field && !field.editable) {
|
|
72
|
+
return (_jsx("div", { className: "refino-panel__body", children: _jsxs("p", { className: "refino-panel__notice", role: "alert", children: ["Refino cannot edit this article\u2019s body: ", field.reason, ". Nothing has been changed."] }) }));
|
|
73
|
+
}
|
|
74
|
+
return (_jsxs("div", { className: "refino-panel__body", children: [_jsxs("p", { className: "refino-panel__hint", children: ["This is the article\u2019s ", article.sourceType === "mdx" ? "MDX" : "Markdown", " source, exactly as it is in", " ", article.path ?? "the repository", ". Components, imports, expressions and code fences are saved back unchanged; edit the prose around them."] }), _jsx("textarea", { ref: textarea, className: "refino-panel__source", spellCheck: false, value: value, "aria-label": "Article source", onChange: (event) => store.setValue(id, event.target.value) }), _jsxs("div", { className: "refino-panel__actions", children: [_jsx("span", { className: "refino-panel__state", children: dirty ? "This body has unsaved changes" : "This body is unchanged" }), _jsx("button", { type: "button", className: "refino-toolbar__button", onClick: () => store.revertValue(id), disabled: !dirty, children: "Revert body" })] })] }));
|
|
75
|
+
}
|
|
76
|
+
function MetadataEditor({ article }) {
|
|
77
|
+
const { store } = useCopyStoreOrThrow();
|
|
78
|
+
const fields = article.fields.filter((field) => field.id !== BODY_FIELD_ID);
|
|
79
|
+
if (fields.length === 0) {
|
|
80
|
+
return (_jsx("div", { className: "refino-panel__body", children: _jsxs("p", { className: "refino-panel__notice", children: ["Refino found no metadata it recognizes in this article\u2019s frontmatter. Map the fields you want in the ", _jsx("code", { children: "content" }), " block of ", _jsx("code", { children: "refino.config.json" }), "."] }) }));
|
|
81
|
+
}
|
|
82
|
+
return (_jsxs("div", { className: "refino-panel__body", children: [_jsx("div", { className: "refino-panel__fields", children: fields.map((field) => (_jsx(MetadataField, { article: article, field: field }, field.id))) }), _jsxs("div", { className: "refino-panel__actions", children: [_jsx("span", { className: "refino-panel__state", children: "Every other frontmatter key in this file is left exactly as it is." }), _jsx("button", { type: "button", className: "refino-toolbar__button", onClick: () => {
|
|
83
|
+
for (const field of fields)
|
|
84
|
+
store.revertValue(documentFieldId(article.documentId, field.id));
|
|
85
|
+
}, children: "Revert metadata" })] })] }));
|
|
86
|
+
}
|
|
87
|
+
function MetadataField({ article, field }) {
|
|
88
|
+
const { store } = useCopyStoreOrThrow();
|
|
89
|
+
const id = documentFieldId(article.documentId, field.id);
|
|
90
|
+
const { value, dirty } = useField(id);
|
|
91
|
+
const name = fieldName(field);
|
|
92
|
+
return (_jsxs("label", { className: "refino-panel__field", "data-refino-state": dirty ? "dirty" : "idle", children: [_jsxs("span", { className: "refino-panel__label", children: [labelFor(field), field.key && _jsx("code", { className: "refino-panel__key", children: field.key })] }), MULTILINE.has(name) ? (_jsx("textarea", { className: "refino-panel__input refino-panel__input--multiline", value: value, disabled: !field.editable, onChange: (event) => store.setValue(id, event.target.value) })) : (_jsx("input", { type: "text", className: "refino-panel__input", value: value, disabled: !field.editable, onChange: (event) => store.setValue(id, event.target.value) })), !field.editable && _jsxs("span", { className: "refino-panel__reason", children: ["Read-only: ", field.reason, "."] })] }));
|
|
93
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { FieldId } from "@getrefino/core";
|
|
2
|
+
import type { JSX } from "react";
|
|
3
|
+
export interface RefinoArticleProps {
|
|
4
|
+
/**
|
|
5
|
+
* The article's id, e.g. `blog.hello-world`. Produced by
|
|
6
|
+
* `documentIdForPath` in `@getrefino/core/documents` from the source path
|
|
7
|
+
* the page already knows. A path is never sent to the server.
|
|
8
|
+
*/
|
|
9
|
+
documentId: string;
|
|
10
|
+
/**
|
|
11
|
+
* The field values this page rendered the article with, e.g.
|
|
12
|
+
* `{ "meta.title": post.title, body: post.body }`. Edit mode shows these
|
|
13
|
+
* until the canonical version has loaded, and keeps them for fields
|
|
14
|
+
* Refino can read but not write.
|
|
15
|
+
*/
|
|
16
|
+
fields: Readonly<Record<FieldId, string>>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Declares that this page renders an article, so the editor can load it,
|
|
20
|
+
* hold a draft of it and save it.
|
|
21
|
+
*
|
|
22
|
+
* It renders nothing. Put it in the article page, wherever is convenient —
|
|
23
|
+
* the provider usually lives in the root layout, which cannot know which
|
|
24
|
+
* article a route is showing, and this is how the page tells it.
|
|
25
|
+
*
|
|
26
|
+
* Registration happens on mount, which is after the server rendered the
|
|
27
|
+
* page, so give each `EditableText` for an article field the page's own
|
|
28
|
+
* value as its child:
|
|
29
|
+
*
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <EditableText id={`${documentId}.meta.title`} as="h1">{fields["meta.title"]}</EditableText>
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* The server-rendered HTML is then already correct, and the editor replaces
|
|
35
|
+
* it with the canonical value once it has loaded. Both are the same file.
|
|
36
|
+
*/
|
|
37
|
+
export declare function RefinoArticle({ documentId, fields }: RefinoArticleProps): JSX.Element | null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useMemo } from "react";
|
|
3
|
+
import { useCopyContext } from "./context.js";
|
|
4
|
+
/**
|
|
5
|
+
* Declares that this page renders an article, so the editor can load it,
|
|
6
|
+
* hold a draft of it and save it.
|
|
7
|
+
*
|
|
8
|
+
* It renders nothing. Put it in the article page, wherever is convenient —
|
|
9
|
+
* the provider usually lives in the root layout, which cannot know which
|
|
10
|
+
* article a route is showing, and this is how the page tells it.
|
|
11
|
+
*
|
|
12
|
+
* Registration happens on mount, which is after the server rendered the
|
|
13
|
+
* page, so give each `EditableText` for an article field the page's own
|
|
14
|
+
* value as its child:
|
|
15
|
+
*
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <EditableText id={`${documentId}.meta.title`} as="h1">{fields["meta.title"]}</EditableText>
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* The server-rendered HTML is then already correct, and the editor replaces
|
|
21
|
+
* it with the canonical value once it has loaded. Both are the same file.
|
|
22
|
+
*/
|
|
23
|
+
export function RefinoArticle({ documentId, fields }) {
|
|
24
|
+
const context = useCopyContext();
|
|
25
|
+
const store = context?.store ?? null;
|
|
26
|
+
// Registration is by value: a client-side navigation to another article
|
|
27
|
+
// re-registers, and a re-render with the same values does nothing.
|
|
28
|
+
const signature = useMemo(() => JSON.stringify([
|
|
29
|
+
documentId,
|
|
30
|
+
Object.keys(fields)
|
|
31
|
+
.sort()
|
|
32
|
+
.map((key) => [key, fields[key]]),
|
|
33
|
+
]), [documentId, fields]);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!store)
|
|
36
|
+
return;
|
|
37
|
+
store.addDocument({ documentId, fields });
|
|
38
|
+
return () => store.removeDocument(documentId);
|
|
39
|
+
// `signature` stands in for `fields`, which is a fresh object every render.
|
|
40
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
41
|
+
}, [store, documentId, signature]);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
package/dist/RefinoProvider.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { CopyContent } from "@getrefino/core";
|
|
|
2
2
|
import type { JSX, ReactNode } from "react";
|
|
3
3
|
import type { CopyRequestHeaders } from "./store.js";
|
|
4
4
|
import type { EditorEventSink } from "./telemetry.js";
|
|
5
|
+
import type { IntegrationUpdateSource } from "./updates.js";
|
|
5
6
|
export interface RefinoProviderProps {
|
|
6
7
|
/** The canonical copy, usually `import copy from "./content/copy.json"`. */
|
|
7
8
|
content: CopyContent;
|
|
@@ -32,6 +33,13 @@ export interface RefinoProviderProps {
|
|
|
32
33
|
* sites get a sink that posts to Refino; everyone else gets silence.
|
|
33
34
|
*/
|
|
34
35
|
onEvent?: EditorEventSink | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Lets the toolbar offer updates to the site's Refino integration. Asked
|
|
38
|
+
* once per editing session; nothing in the repository changes unless the
|
|
39
|
+
* owner presses "Update Refino". Refino-connected sites get this from
|
|
40
|
+
* their generated client; everyone else leaves it out and sees nothing.
|
|
41
|
+
*/
|
|
42
|
+
updates?: IntegrationUpdateSource | undefined;
|
|
35
43
|
/** Render the built-in toolbar in edit mode. Set false to use `useCopyEditor` with custom UI. */
|
|
36
44
|
toolbar?: boolean | undefined;
|
|
37
45
|
children?: ReactNode | undefined;
|
package/dist/RefinoProvider.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { getDirtyIds } from "@getrefino/core";
|
|
4
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
5
5
|
import { CopyContext } from "./context.js";
|
|
6
|
+
import { ArticlePanel } from "./ArticlePanel.js";
|
|
6
7
|
import { createCopyStore } from "./store.js";
|
|
7
8
|
import { EDITOR_STYLES } from "./styles.js";
|
|
8
9
|
import { CopyEditorToolbar } from "./Toolbar.js";
|
|
@@ -13,7 +14,7 @@ import { CopyEditorToolbar } from "./Toolbar.js";
|
|
|
13
14
|
* the toolbar and editor styles.
|
|
14
15
|
*/
|
|
15
16
|
export function RefinoProvider(props) {
|
|
16
|
-
const { content, editing = false, endpoint = "/api/copy", headers, signInHref = "/edit", onExit, onEvent, toolbar = true, children } = props;
|
|
17
|
+
const { content, editing = false, endpoint = "/api/copy", headers, signInHref = "/edit", onExit, onEvent, updates, toolbar = true, children } = props;
|
|
17
18
|
// Created once. The sink is read through the ref below, so a host that
|
|
18
19
|
// passes a new closure on every render neither recreates the store nor
|
|
19
20
|
// replays a lifecycle event.
|
|
@@ -26,11 +27,27 @@ export function RefinoProvider(props) {
|
|
|
26
27
|
useEffect(() => {
|
|
27
28
|
store.setBaseContent(content);
|
|
28
29
|
}, [content, store]);
|
|
30
|
+
/**
|
|
31
|
+
* Load the canonical copy once per editing session, and again when the
|
|
32
|
+
* articles on the page change (a client-side navigation to another post).
|
|
33
|
+
*
|
|
34
|
+
* Articles register from `<RefinoArticle>`, whose effect runs before this
|
|
35
|
+
* one on mount, so the first request already names them and the page is
|
|
36
|
+
* loaded in one round trip rather than two.
|
|
37
|
+
*/
|
|
38
|
+
const documentKey = useSyncExternalStore(store.subscribe, () => store.getState().documentIds.join(","), () => "");
|
|
39
|
+
const loadedKey = useRef(null);
|
|
29
40
|
useEffect(() => {
|
|
30
|
-
if (!editing)
|
|
41
|
+
if (!editing) {
|
|
42
|
+
loadedKey.current = null;
|
|
31
43
|
return;
|
|
44
|
+
}
|
|
45
|
+
const key = store.getState().documentIds.join(",");
|
|
46
|
+
if (loadedKey.current === key)
|
|
47
|
+
return;
|
|
48
|
+
loadedKey.current = key;
|
|
32
49
|
void store.load();
|
|
33
|
-
}, [editing, store]);
|
|
50
|
+
}, [editing, store, documentKey]);
|
|
34
51
|
useEffect(() => {
|
|
35
52
|
if (!editing || typeof window === "undefined")
|
|
36
53
|
return;
|
|
@@ -53,6 +70,6 @@ export function RefinoProvider(props) {
|
|
|
53
70
|
window.removeEventListener("keydown", onKeyDown);
|
|
54
71
|
};
|
|
55
72
|
}, [editing, store]);
|
|
56
|
-
const value = useMemo(() => ({ store, onExit, signInHref }), [store, onExit, signInHref]);
|
|
57
|
-
return (_jsxs(CopyContext.Provider, { value: value, children: [children, editing && (_jsxs(_Fragment, { children: [_jsx("style", { "data-refino-styles": "", children: EDITOR_STYLES }), toolbar && _jsx(CopyEditorToolbar, {})] }))] }));
|
|
73
|
+
const value = useMemo(() => ({ store, onExit, signInHref, updates }), [store, onExit, signInHref, updates]);
|
|
74
|
+
return (_jsxs(CopyContext.Provider, { value: value, children: [children, editing && (_jsxs(_Fragment, { children: [_jsx("style", { "data-refino-styles": "", children: EDITOR_STYLES }), _jsx(ArticlePanel, {}), toolbar && _jsx(CopyEditorToolbar, {})] }))] }));
|
|
58
75
|
}
|
package/dist/Toolbar.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback } from "react";
|
|
3
|
+
import { useCallback, useRef } from "react";
|
|
4
4
|
import { INSTRUCTIONS_ID } from "./dom.js";
|
|
5
|
+
import { UpdateButton, UpdatePanel, useIntegrationUpdate } from "./UpdateNotice.js";
|
|
5
6
|
import { useCopyEditor } from "./useCopyEditor.js";
|
|
6
7
|
function plural(count, word) {
|
|
7
8
|
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
@@ -14,10 +15,15 @@ function plural(count, word) {
|
|
|
14
15
|
*/
|
|
15
16
|
export function CopyEditorToolbar() {
|
|
16
17
|
const editor = useCopyEditor();
|
|
17
|
-
const { state, dirtyIds } = editor;
|
|
18
|
+
const { state, dirtyIds, articles } = editor;
|
|
19
|
+
// V1 edits the article the page is showing. A page with several is
|
|
20
|
+
// supported by the model; the toolbar simply offers the first.
|
|
21
|
+
const article = articles[0];
|
|
18
22
|
const dirtyCount = dirtyIds.length;
|
|
19
23
|
const activeDirty = state.activeId !== null && dirtyIds.includes(state.activeId);
|
|
20
24
|
const busy = state.status === "saving" || state.status === "loading";
|
|
25
|
+
const update = useIntegrationUpdate(editor.updates, state.loaded);
|
|
26
|
+
const updateButtonRef = useRef(null);
|
|
21
27
|
const onRevertSelected = useCallback(() => {
|
|
22
28
|
if (state.activeId)
|
|
23
29
|
editor.revertValue(state.activeId);
|
|
@@ -50,5 +56,7 @@ export function CopyEditorToolbar() {
|
|
|
50
56
|
statusText = dirtyCount > 0 ? `${plural(dirtyCount, "unsaved change")}` : "No changes";
|
|
51
57
|
dotClass = dirtyCount > 0 ? "dirty" : "";
|
|
52
58
|
}
|
|
53
|
-
return (_jsxs("div", { className: "refino-toolbar", role: "region", "aria-label": "Copy editor", children: [_jsx("p", { id: INSTRUCTIONS_ID, className: "refino-sr-only", children: "Editable text. Type to change it. Press Enter to finish or Escape to cancel this edit." }), state.status === "conflict" && (_jsxs("div", { className: "refino-toolbar__notice refino-toolbar__notice--conflict", role: "alert", children: [_jsx("p", { children: "The copy file changed in the repository while you were editing (an AI agent or developer probably pushed). Your edits were not saved and nothing was overwritten." }), _jsxs("p", { children: ["Reload the latest copy and your ", plural(dirtyCount, "unsaved edit"), " will be reapplied on top of it for you to review and save again."] })] })), state.status === "error" && state.error && (_jsxs("div", { className: "refino-toolbar__notice refino-toolbar__notice--error", role: "alert", children: [_jsx("p", { children: state.error.message }), _jsxs("p", { children: [_jsx("code", { children: state.error.code }), state.error.code === "UNAUTHORIZED" && (_jsxs(_Fragment, { children: [" ", _jsx("a", { className: "refino-toolbar__link", href: editor.signInHref, children: "Sign in again" })] }))] })] })), state.rebase && (_jsxs("div", { className: "refino-toolbar__notice", role: "status", children: [state.rebase.overlapping.length > 0 && (_jsxs("p", { children: ["Both you and the repository changed:", " ", state.rebase.overlapping.map((id, index) => (_jsxs("span", { children: [index > 0 ? ", " : "", _jsx("code", { children: id })] }, id))), ". Your wording is shown; review before saving."] })), state.rebase.dropped.length > 0 && (_jsxs("p", { children: ["Dropped edits for fields that no longer exist:", " ", state.rebase.dropped.map((id, index) => (_jsxs("span", { children: [index > 0 ? ", " : "", _jsx("code", { children: id })] }, id))), "."] }))] })), _jsxs("div", { className: "refino-toolbar__bar", role: "toolbar", "aria-label": "Copy editor controls", children: [_jsxs("span", { className: "refino-toolbar__status", "aria-live": "polite", children: [_jsx("span", { className: `refino-toolbar__dot${dotClass ? ` refino-toolbar__dot--${dotClass}` : ""}` }), statusText, state.status === "saved" && state.lastSave?.commitUrl && (_jsxs(_Fragment, { children: [" ", _jsx("a", { className: "refino-toolbar__link", href: state.lastSave.commitUrl, target: "_blank", rel: "noreferrer", children: "view commit" })] }))] }),
|
|
59
|
+
return (_jsxs("div", { className: "refino-toolbar", role: "region", "aria-label": "Copy editor", children: [_jsx("p", { id: INSTRUCTIONS_ID, className: "refino-sr-only", children: "Editable text. Type to change it. Press Enter to finish or Escape to cancel this edit." }), _jsx(UpdatePanel, { control: update, saving: state.status === "saving", buttonRef: updateButtonRef }), state.status === "conflict" && (_jsxs("div", { className: "refino-toolbar__notice refino-toolbar__notice--conflict", role: "alert", children: [_jsx("p", { children: "The copy file changed in the repository while you were editing (an AI agent or developer probably pushed). Your edits were not saved and nothing was overwritten." }), _jsxs("p", { children: ["Reload the latest copy and your ", plural(dirtyCount, "unsaved edit"), " will be reapplied on top of it for you to review and save again."] })] })), state.status === "error" && state.error && (_jsxs("div", { className: "refino-toolbar__notice refino-toolbar__notice--error", role: "alert", children: [_jsx("p", { children: state.error.message }), _jsxs("p", { children: [_jsx("code", { children: state.error.code }), state.error.code === "UNAUTHORIZED" && (_jsxs(_Fragment, { children: [" ", _jsx("a", { className: "refino-toolbar__link", href: editor.signInHref, children: "Sign in again" })] }))] })] })), state.rebase && (_jsxs("div", { className: "refino-toolbar__notice", role: "status", children: [state.rebase.overlapping.length > 0 && (_jsxs("p", { children: ["Both you and the repository changed:", " ", state.rebase.overlapping.map((id, index) => (_jsxs("span", { children: [index > 0 ? ", " : "", _jsx("code", { children: id })] }, id))), ". Your wording is shown; review before saving."] })), state.rebase.dropped.length > 0 && (_jsxs("p", { children: ["Dropped edits for fields that no longer exist:", " ", state.rebase.dropped.map((id, index) => (_jsxs("span", { children: [index > 0 ? ", " : "", _jsx("code", { children: id })] }, id))), "."] }))] })), _jsxs("div", { className: "refino-toolbar__bar", role: "toolbar", "aria-label": "Copy editor controls", children: [_jsxs("span", { className: "refino-toolbar__status", "aria-live": "polite", children: [_jsx("span", { className: `refino-toolbar__dot${dotClass ? ` refino-toolbar__dot--${dotClass}` : ""}` }), statusText, state.status === "saved" && state.lastSave?.commitUrl && (_jsxs(_Fragment, { children: [" ", _jsx("a", { className: "refino-toolbar__link", href: state.lastSave.commitUrl, target: "_blank", rel: "noreferrer", children: "view commit" })] }))] }), article && state.status !== "conflict" && (_jsx("button", { type: "button", className: "refino-toolbar__button", onClick: () => state.panel?.kind === "metadata"
|
|
60
|
+
? editor.closePanel()
|
|
61
|
+
: editor.openPanel({ kind: "metadata", documentId: article.documentId }), "aria-pressed": state.panel?.kind === "metadata", title: "Edit this article's title, excerpt, SEO fields and dates", children: "Article metadata" })), state.activeId && (_jsx("span", { className: "refino-toolbar__key", title: state.activeId, children: state.activeId })), state.status === "conflict" ? (_jsx("button", { type: "button", className: "refino-toolbar__button refino-toolbar__button--primary", onClick: () => void editor.reloadLatest(), children: "Reload latest" })) : (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "refino-toolbar__button", onMouseDown: (event) => event.preventDefault(), onClick: onRevertSelected, disabled: !activeDirty || busy, title: "Revert the selected text to its saved value", children: "Revert" }), _jsx("button", { type: "button", className: "refino-toolbar__button", onClick: editor.revertAll, disabled: dirtyCount === 0 || busy, title: "Discard all unsaved changes", children: "Revert all" }), _jsx("button", { type: "button", className: "refino-toolbar__button refino-toolbar__button--primary", onClick: () => void editor.save(), disabled: dirtyCount === 0 || busy || !state.loaded, title: "Save all changes (Ctrl/Cmd+S)", children: state.status === "saving" ? "Saving…" : "Save" })] })), _jsx(UpdateButton, { control: update, buttonRef: updateButtonRef }), _jsx("button", { type: "button", className: "refino-toolbar__button", onClick: () => void editor.exit(), disabled: busy, children: "Exit" })] })] }));
|
|
54
62
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { JSX, RefObject } from "react";
|
|
2
|
+
import type { IntegrationUpdateResult, IntegrationUpdateSource, IntegrationUpdateStatus } from "./updates.js";
|
|
3
|
+
type Available = Extract<IntegrationUpdateStatus, {
|
|
4
|
+
state: "available";
|
|
5
|
+
}>;
|
|
6
|
+
export type UpdatePhase = {
|
|
7
|
+
readonly kind: "hidden";
|
|
8
|
+
} | {
|
|
9
|
+
readonly kind: "available";
|
|
10
|
+
readonly update: Available;
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: "updating";
|
|
13
|
+
readonly update: Available;
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: "updated";
|
|
16
|
+
readonly version: string;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: "failed";
|
|
19
|
+
readonly update: Available;
|
|
20
|
+
readonly result: Extract<IntegrationUpdateResult, {
|
|
21
|
+
ok: false;
|
|
22
|
+
}>;
|
|
23
|
+
};
|
|
24
|
+
export interface IntegrationUpdateControl {
|
|
25
|
+
readonly phase: UpdatePhase;
|
|
26
|
+
readonly open: boolean;
|
|
27
|
+
setOpen(open: boolean): void;
|
|
28
|
+
/** Install the offered version. The only path to a repository change, and only ever from a click. */
|
|
29
|
+
apply(): Promise<void>;
|
|
30
|
+
/** Close the panel after a success or failure; a success also retires the toolbar control. */
|
|
31
|
+
close(): void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Asks once per editing session, after the copy has loaded, whether the
|
|
35
|
+
* site's integration has an update, and runs the update when (and only
|
|
36
|
+
* when) the owner asks for it. No polling, no retries, no background work.
|
|
37
|
+
*/
|
|
38
|
+
export declare function useIntegrationUpdate(source: IntegrationUpdateSource | undefined, ready: boolean): IntegrationUpdateControl;
|
|
39
|
+
/** The toolbar control: absent unless there is something to offer. */
|
|
40
|
+
export declare function UpdateButton({ control, buttonRef }: {
|
|
41
|
+
control: IntegrationUpdateControl;
|
|
42
|
+
buttonRef: RefObject<HTMLButtonElement | null>;
|
|
43
|
+
}): JSX.Element | null;
|
|
44
|
+
/** The compact panel above the toolbar. */
|
|
45
|
+
export declare function UpdatePanel({ control, saving, buttonRef, }: {
|
|
46
|
+
control: IntegrationUpdateControl;
|
|
47
|
+
/** A save is in flight; an update now would race it for the branch. */
|
|
48
|
+
saving: boolean;
|
|
49
|
+
buttonRef: RefObject<HTMLButtonElement | null>;
|
|
50
|
+
}): JSX.Element | null;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
|
4
|
+
import { readUpdateResult, readUpdateStatus } from "./updates.js";
|
|
5
|
+
/**
|
|
6
|
+
* Asks once per editing session, after the copy has loaded, whether the
|
|
7
|
+
* site's integration has an update, and runs the update when (and only
|
|
8
|
+
* when) the owner asks for it. No polling, no retries, no background work.
|
|
9
|
+
*/
|
|
10
|
+
export function useIntegrationUpdate(source, ready) {
|
|
11
|
+
const [phase, setPhase] = useState({ kind: "hidden" });
|
|
12
|
+
const [open, setOpen] = useState(false);
|
|
13
|
+
const asked = useRef(false);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!source || !ready || asked.current)
|
|
16
|
+
return;
|
|
17
|
+
asked.current = true;
|
|
18
|
+
// Whatever goes wrong while asking, the answer is "nothing to show".
|
|
19
|
+
void Promise.resolve()
|
|
20
|
+
.then(() => source.check())
|
|
21
|
+
.then(readUpdateStatus, () => ({ state: "current" }))
|
|
22
|
+
.then((status) => {
|
|
23
|
+
if (status.state === "available")
|
|
24
|
+
setPhase({ kind: "available", update: status });
|
|
25
|
+
});
|
|
26
|
+
}, [source, ready]);
|
|
27
|
+
const apply = useCallback(async () => {
|
|
28
|
+
if (!source || (phase.kind !== "available" && phase.kind !== "failed"))
|
|
29
|
+
return;
|
|
30
|
+
const update = phase.update;
|
|
31
|
+
if (!update.automatic)
|
|
32
|
+
return;
|
|
33
|
+
setPhase({ kind: "updating", update });
|
|
34
|
+
let result;
|
|
35
|
+
try {
|
|
36
|
+
result = readUpdateResult(await source.apply(update.version), update.version);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
result = readUpdateResult(null, update.version);
|
|
40
|
+
}
|
|
41
|
+
setPhase(result.ok ? { kind: "updated", version: result.version } : { kind: "failed", update, result });
|
|
42
|
+
}, [phase, source]);
|
|
43
|
+
const close = useCallback(() => {
|
|
44
|
+
setOpen(false);
|
|
45
|
+
if (phase.kind === "updated")
|
|
46
|
+
setPhase({ kind: "hidden" });
|
|
47
|
+
else if (phase.kind === "failed")
|
|
48
|
+
setPhase({ kind: "available", update: phase.update });
|
|
49
|
+
}, [phase]);
|
|
50
|
+
return { phase, open, setOpen, apply, close };
|
|
51
|
+
}
|
|
52
|
+
function title(update) {
|
|
53
|
+
switch (update.urgency) {
|
|
54
|
+
case "required":
|
|
55
|
+
return "Refino update required";
|
|
56
|
+
case "recommended":
|
|
57
|
+
return "Refino update recommended";
|
|
58
|
+
default:
|
|
59
|
+
return "Refino update available";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function triggerLabel(phase) {
|
|
63
|
+
switch (phase.kind) {
|
|
64
|
+
case "available":
|
|
65
|
+
case "failed":
|
|
66
|
+
return phase.update.urgency === "required" ? "Update required" : phase.update.urgency === "recommended" ? "Update recommended" : "Update available";
|
|
67
|
+
case "updating":
|
|
68
|
+
return "Updating…";
|
|
69
|
+
case "updated":
|
|
70
|
+
return "Updated";
|
|
71
|
+
default:
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** The toolbar control: absent unless there is something to offer. */
|
|
76
|
+
export function UpdateButton({ control, buttonRef }) {
|
|
77
|
+
const label = triggerLabel(control.phase);
|
|
78
|
+
if (!label)
|
|
79
|
+
return null;
|
|
80
|
+
return (_jsxs("button", { ref: buttonRef, type: "button", className: "refino-toolbar__button refino-toolbar__button--update", "aria-haspopup": "dialog", "aria-expanded": control.open, onClick: () => control.setOpen(!control.open), children: [_jsx("span", { className: "refino-toolbar__dot refino-toolbar__dot--update", "aria-hidden": "true" }), label] }));
|
|
81
|
+
}
|
|
82
|
+
/** The compact panel above the toolbar. */
|
|
83
|
+
export function UpdatePanel({ control, saving, buttonRef, }) {
|
|
84
|
+
const { phase, open } = control;
|
|
85
|
+
const titleId = useId();
|
|
86
|
+
const focusRef = useRef(null);
|
|
87
|
+
const visible = open && phase.kind !== "hidden";
|
|
88
|
+
const dismiss = useCallback(() => {
|
|
89
|
+
if (phase.kind === "updating")
|
|
90
|
+
return;
|
|
91
|
+
control.close();
|
|
92
|
+
buttonRef.current?.focus();
|
|
93
|
+
}, [buttonRef, control, phase.kind]);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (visible)
|
|
96
|
+
focusRef.current?.focus();
|
|
97
|
+
}, [visible, phase.kind]);
|
|
98
|
+
if (!visible)
|
|
99
|
+
return null;
|
|
100
|
+
const onKeyDown = (event) => {
|
|
101
|
+
if (event.key === "Escape") {
|
|
102
|
+
event.stopPropagation();
|
|
103
|
+
dismiss();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
let heading;
|
|
107
|
+
let body;
|
|
108
|
+
let actions;
|
|
109
|
+
switch (phase.kind) {
|
|
110
|
+
case "available": {
|
|
111
|
+
const { update } = phase;
|
|
112
|
+
heading = title(update);
|
|
113
|
+
body = (_jsxs(_Fragment, { children: [_jsxs("p", { children: ["Version ", update.version, " is available."] }), update.summary.length > 0 && (_jsx("ul", { className: "refino-update__summary", children: update.summary.map((line) => (_jsx("li", { children: line }, line))) })), update.automatic ? (_jsx("p", { className: "refino-update__note", children: "Your site redeploys with the update as usual. Your text and unsaved edits are not affected." })) : (_jsx("p", { className: "refino-update__note", children: update.message ?? "This update needs to be installed by whoever maintains your site's code." }))] }));
|
|
114
|
+
actions = update.automatic ? (_jsxs(_Fragment, { children: [_jsx("button", { ref: focusRef, type: "button", className: "refino-toolbar__button", onClick: dismiss, children: "Not now" }), _jsx("button", { type: "button", className: "refino-toolbar__button refino-toolbar__button--primary", onClick: () => void control.apply(), disabled: saving, title: saving ? "Wait for the save to finish" : undefined, children: "Update Refino" })] })) : (_jsx("button", { ref: focusRef, type: "button", className: "refino-toolbar__button", onClick: dismiss, children: "Close" }));
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "updating":
|
|
118
|
+
heading = "Updating Refino…";
|
|
119
|
+
body = _jsx("p", { children: "This usually takes a few seconds. Keep this page open." });
|
|
120
|
+
actions = _jsx(_Fragment, {});
|
|
121
|
+
break;
|
|
122
|
+
case "updated":
|
|
123
|
+
heading = `Refino updated to ${phase.version}`;
|
|
124
|
+
body = _jsx("p", { children: "Your site is redeploying with the update, which can take a few minutes. You can keep editing in the meantime." });
|
|
125
|
+
actions = (_jsx("button", { ref: focusRef, type: "button", className: "refino-toolbar__button refino-toolbar__button--primary", onClick: dismiss, children: "Done" }));
|
|
126
|
+
break;
|
|
127
|
+
case "failed": {
|
|
128
|
+
const { result } = phase;
|
|
129
|
+
heading = result.unchanged ? "Refino was not updated" : "Update status unknown";
|
|
130
|
+
body = (_jsxs(_Fragment, { children: [_jsx("p", { children: result.message }), _jsx("p", { className: "refino-update__note", children: result.unchanged ? "Nothing on your site was changed." : "Reload this page in a minute to see whether the update went through." })] }));
|
|
131
|
+
actions = (_jsxs(_Fragment, { children: [_jsx("button", { ref: focusRef, type: "button", className: "refino-toolbar__button", onClick: dismiss, children: "Close" }), result.unchanged && (_jsx("button", { type: "button", className: "refino-toolbar__button refino-toolbar__button--primary", onClick: () => void control.apply(), disabled: saving, children: "Try again" }))] }));
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return (_jsxs("div", { className: "refino-toolbar__notice refino-update", role: "dialog", "aria-labelledby": titleId, onKeyDown: onKeyDown, children: [_jsx("p", { id: titleId, className: "refino-update__title", "aria-live": "polite", children: heading }), body, phase.kind !== "updating" && _jsx("div", { className: "refino-update__actions", children: actions })] }));
|
|
136
|
+
}
|
package/dist/context.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { CopyEditorState, CopyStore } from "./store.js";
|
|
2
|
+
import type { IntegrationUpdateSource } from "./updates.js";
|
|
2
3
|
export interface CopyContextValue {
|
|
3
4
|
readonly store: CopyStore;
|
|
4
5
|
readonly onExit: (() => void | Promise<void>) | undefined;
|
|
5
6
|
/** Where to send the owner when the session has ended. */
|
|
6
7
|
readonly signInHref: string;
|
|
8
|
+
/** Where the toolbar asks about, and installs, updates to the site's Refino integration. */
|
|
9
|
+
readonly updates: IntegrationUpdateSource | undefined;
|
|
7
10
|
}
|
|
8
11
|
export declare const CopyContext: import("react").Context<CopyContextValue | null>;
|
|
9
12
|
export declare function useCopyContext(): CopyContextValue | null;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,13 @@ export type { EditableTextElement, EditableTextProps } from "./EditableText.js";
|
|
|
5
5
|
export { CopyEditorToolbar } from "./Toolbar.js";
|
|
6
6
|
export { useCopyEditor } from "./useCopyEditor.js";
|
|
7
7
|
export type { CopyEditor } from "./useCopyEditor.js";
|
|
8
|
+
export { ArticleBody } from "./ArticleBody.js";
|
|
9
|
+
export type { ArticleBodyElement, ArticleBodyProps } from "./ArticleBody.js";
|
|
10
|
+
export { RefinoArticle } from "./RefinoArticle.js";
|
|
11
|
+
export type { RefinoArticleProps } from "./RefinoArticle.js";
|
|
8
12
|
export { createCopyStore } from "./store.js";
|
|
9
|
-
export type { CopyEditorState, CopyRequestHeaders, CopyStore, CopyStoreOptions, EditorError, EditorStatus, LastSave, RebaseInfo, } from "./store.js";
|
|
10
|
-
export type { EditorEvent, EditorEventName, EditorEventProperties, EditorEventSink } from "./telemetry.js";
|
|
13
|
+
export type { ArticleDocument, ArticleState, CopyEditorState, EditorPanel, CopyRequestHeaders, CopyStore, CopyStoreOptions, EditorError, EditorStatus, LastSave, RebaseInfo, } from "./store.js";
|
|
14
|
+
export type { EditorEvent, EditorEventName, EditorEventProperties, EditorEventSink, EditorSurface, } from "./telemetry.js";
|
|
15
|
+
export { readUpdateResult, readUpdateStatus } from "./updates.js";
|
|
16
|
+
export type { IntegrationUpdateResult, IntegrationUpdateSource, IntegrationUpdateStatus, UpdateUrgency } from "./updates.js";
|
|
11
17
|
export { EDITOR_STYLES } from "./styles.js";
|
package/dist/index.js
CHANGED
|
@@ -2,5 +2,8 @@ export { RefinoProvider } from "./RefinoProvider.js";
|
|
|
2
2
|
export { EditableText } from "./EditableText.js";
|
|
3
3
|
export { CopyEditorToolbar } from "./Toolbar.js";
|
|
4
4
|
export { useCopyEditor } from "./useCopyEditor.js";
|
|
5
|
+
export { ArticleBody } from "./ArticleBody.js";
|
|
6
|
+
export { RefinoArticle } from "./RefinoArticle.js";
|
|
5
7
|
export { createCopyStore } from "./store.js";
|
|
8
|
+
export { readUpdateResult, readUpdateStatus } from "./updates.js";
|
|
6
9
|
export { EDITOR_STYLES } from "./styles.js";
|