@realiizlabs/admin 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth-ui/index.cjs +17 -1
- package/dist/auth-ui/index.cjs.map +1 -1
- package/dist/auth-ui/index.js +1 -1
- package/dist/{chunk-CE2DRQBD.js → chunk-FTD4WE3V.js} +19 -3
- package/dist/chunk-FTD4WE3V.js.map +1 -0
- package/dist/forms-ui/index.cjs +243 -36
- package/dist/forms-ui/index.cjs.map +1 -1
- package/dist/forms-ui/index.d.cts +69 -12
- package/dist/forms-ui/index.d.ts +69 -12
- package/dist/forms-ui/index.js +226 -36
- package/dist/forms-ui/index.js.map +1 -1
- package/dist/media/index.cjs +95 -0
- package/dist/media/index.cjs.map +1 -0
- package/dist/media/index.d.cts +65 -0
- package/dist/media/index.d.ts +65 -0
- package/dist/media/index.js +85 -0
- package/dist/media/index.js.map +1 -0
- package/dist/shell/index.cjs +31 -24
- package/dist/shell/index.cjs.map +1 -1
- package/dist/shell/index.d.cts +6 -3
- package/dist/shell/index.d.ts +6 -3
- package/dist/shell/index.js +31 -24
- package/dist/shell/index.js.map +1 -1
- package/package.json +6 -1
- package/dist/chunk-CE2DRQBD.js.map +0 -1
|
@@ -4,16 +4,35 @@ import { C as ContentTypeEntry } from '../types-DyryuIX1.cjs';
|
|
|
4
4
|
import 'zod';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Picked images beside the form state. An image FIELD's value stays a path
|
|
8
|
+
* string (what the frontmatter wants); the shrunk bytes for a newly chosen
|
|
9
|
+
* picture live here until submit, keyed by field name, with an object URL for
|
|
10
|
+
* the preview and the previous path so Remove can restore it.
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* a Date from a previously-parsed file — and `toValue` assembles what goes to
|
|
12
|
-
* safeParse. Anything the schema then coerces (dates) is the schema's business.
|
|
12
|
+
* The host decides the path and does the shrinking (`onPickImage`) because it
|
|
13
|
+
* knows the slug and the size rules; this hook only keeps the result.
|
|
13
14
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
interface PickedImage {
|
|
16
|
+
path: string;
|
|
17
|
+
blob: Blob;
|
|
18
|
+
previewUrl: string;
|
|
19
|
+
width?: number;
|
|
20
|
+
height?: number;
|
|
21
|
+
/** The field's value before this pick, restored by Remove. */
|
|
22
|
+
previous: string;
|
|
23
|
+
}
|
|
24
|
+
type PickResult = {
|
|
25
|
+
path: string;
|
|
26
|
+
blob: Blob;
|
|
27
|
+
previewUrl?: string;
|
|
28
|
+
width?: number;
|
|
29
|
+
height?: number;
|
|
30
|
+
};
|
|
31
|
+
type PickImage = (field: string, file: File) => Promise<PickResult>;
|
|
32
|
+
type SubmittedImages = Record<string, {
|
|
33
|
+
path: string;
|
|
34
|
+
blob: Blob;
|
|
35
|
+
}>;
|
|
17
36
|
|
|
18
37
|
type LabelRenderer = (label: string, help: string) => ReactNode;
|
|
19
38
|
/**
|
|
@@ -70,13 +89,25 @@ interface MarkdownEditorProps {
|
|
|
70
89
|
* selection as `<Term definition="">word</Term>`. Off unless the site has such a component.
|
|
71
90
|
*/
|
|
72
91
|
termComponent?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Upload a picture for the body: shrink, name and hand back its final path and a
|
|
94
|
+
* preview URL. When set, the toolbar Image button opens a file picker (and drop or
|
|
95
|
+
* paste of an image onto the text does the same) and inserts .
|
|
96
|
+
*/
|
|
97
|
+
onUploadImage?: (file: File) => Promise<{
|
|
98
|
+
path: string;
|
|
99
|
+
previewUrl: string;
|
|
100
|
+
}>;
|
|
101
|
+
/** Paths of not-yet-committed pictures → object URLs, applied to <img src> in the preview. */
|
|
102
|
+
previewSrc?: Record<string, string>;
|
|
73
103
|
}
|
|
74
|
-
declare function MarkdownEditor({ id, name, value, onChange, placeholder, renderPreview, previewClassName, minHeight, termComponent }: MarkdownEditorProps): react.JSX.Element;
|
|
104
|
+
declare function MarkdownEditor({ id, name, value, onChange, placeholder, renderPreview, previewClassName, minHeight, termComponent, onUploadImage, previewSrc }: MarkdownEditorProps): react.JSX.Element;
|
|
75
105
|
|
|
76
106
|
interface ContentFormSubmission {
|
|
77
107
|
frontmatter: Record<string, unknown>;
|
|
78
108
|
body: string;
|
|
79
|
-
|
|
109
|
+
/** Newly picked pictures by field name — the frontmatter already holds each one's final path. */
|
|
110
|
+
images: SubmittedImages;
|
|
80
111
|
}
|
|
81
112
|
interface ContentFormProps {
|
|
82
113
|
entry: ContentTypeEntry;
|
|
@@ -99,6 +130,17 @@ interface ContentFormProps {
|
|
|
99
130
|
previewClassName?: string;
|
|
100
131
|
/** Site tooltip MDX component name (e.g. "Term") to expose as a toolbar button. */
|
|
101
132
|
termComponent?: string;
|
|
133
|
+
/**
|
|
134
|
+
* Image fields: shrink and name a chosen file (the host knows the slug and the
|
|
135
|
+
* size rules) and hand back { path, blob, previewUrl?, width?, height? }. The
|
|
136
|
+
* field's value becomes `path`; the blob rides along in the submission. Without
|
|
137
|
+
* this, image fields are plain text paths.
|
|
138
|
+
*/
|
|
139
|
+
onPickImage?: PickImage;
|
|
140
|
+
/** Editor toolbar Image button: upload a picture for the body and return its path + preview URL. */
|
|
141
|
+
onUploadImage?: MarkdownEditorProps["onUploadImage"];
|
|
142
|
+
/** Paths of not-yet-committed pictures → object URLs, so the Preview tab shows them. */
|
|
143
|
+
previewSrc?: MarkdownEditorProps["previewSrc"];
|
|
102
144
|
/**
|
|
103
145
|
* Put Cancel/Save in a bar that the host can pin to the bottom of the page
|
|
104
146
|
* (`.realiiz-form__bar`; the shell makes it sticky). The bar also reports
|
|
@@ -130,7 +172,22 @@ interface ContentFormProps {
|
|
|
130
172
|
}
|
|
131
173
|
/** "Hello, World! 2026" → "hello-world-2026" — the same rule the blog's slug regex enforces. */
|
|
132
174
|
declare function slugify(s: string): string;
|
|
133
|
-
declare function ContentForm({ entry, initialValues, initialBody, onSubmit, onCancel, submitLabel, className, showBody, bodyLabel, renderPreview, previewClassName, termComponent, stickyBar, status, barExtra, footer, renderLabel, derive, guardUnsaved }: ContentFormProps): react.JSX.Element;
|
|
175
|
+
declare function ContentForm({ entry, initialValues, initialBody, onSubmit, onCancel, submitLabel, className, showBody, bodyLabel, renderPreview, previewClassName, termComponent, onPickImage, onUploadImage, previewSrc, stickyBar, status, barExtra, footer, renderLabel, derive, guardUnsaved }: ContentFormProps): react.JSX.Element;
|
|
176
|
+
|
|
177
|
+
declare const IMAGE_HINT = "JPG, PNG or WebP, up to 10MB. We shrink it for the web.";
|
|
178
|
+
interface ImageInputProps {
|
|
179
|
+
id: string;
|
|
180
|
+
name: string;
|
|
181
|
+
/** The field's current value — a path such as /blog/my-post.webp, or "". */
|
|
182
|
+
value: string;
|
|
183
|
+
picked?: PickedImage;
|
|
184
|
+
invalid?: boolean;
|
|
185
|
+
disabled?: boolean;
|
|
186
|
+
onPick: (file: File) => Promise<void>;
|
|
187
|
+
onRemove?: () => void;
|
|
188
|
+
onBlur?: () => void;
|
|
189
|
+
}
|
|
190
|
+
declare function ImageInput(p: ImageInputProps): react.JSX.Element;
|
|
134
191
|
|
|
135
192
|
/**
|
|
136
193
|
* ChipsInput — tags as chips. Enter or comma adds, × or Backspace-on-empty
|
|
@@ -149,4 +206,4 @@ declare function ChipsInput({ id, name, value, onChange, onBlur, placeholder, in
|
|
|
149
206
|
max?: number;
|
|
150
207
|
}): react.JSX.Element;
|
|
151
208
|
|
|
152
|
-
export { ChipsInput, ContentForm, type ContentFormProps, type ContentFormSubmission, type LabelRenderer, LengthGauge, MarkdownEditor, type MarkdownEditorProps,
|
|
209
|
+
export { ChipsInput, ContentForm, type ContentFormProps, type ContentFormSubmission, IMAGE_HINT, ImageInput, type ImageInputProps, type LabelRenderer, LengthGauge, MarkdownEditor, type MarkdownEditorProps, type PickImage, type PickResult, type PickedImage, type SubmittedImages, TOOLS, slugify };
|
package/dist/forms-ui/index.d.ts
CHANGED
|
@@ -4,16 +4,35 @@ import { C as ContentTypeEntry } from '../types-DyryuIX1.js';
|
|
|
4
4
|
import 'zod';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Picked images beside the form state. An image FIELD's value stays a path
|
|
8
|
+
* string (what the frontmatter wants); the shrunk bytes for a newly chosen
|
|
9
|
+
* picture live here until submit, keyed by field name, with an object URL for
|
|
10
|
+
* the preview and the previous path so Remove can restore it.
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* a Date from a previously-parsed file — and `toValue` assembles what goes to
|
|
12
|
-
* safeParse. Anything the schema then coerces (dates) is the schema's business.
|
|
12
|
+
* The host decides the path and does the shrinking (`onPickImage`) because it
|
|
13
|
+
* knows the slug and the size rules; this hook only keeps the result.
|
|
13
14
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
interface PickedImage {
|
|
16
|
+
path: string;
|
|
17
|
+
blob: Blob;
|
|
18
|
+
previewUrl: string;
|
|
19
|
+
width?: number;
|
|
20
|
+
height?: number;
|
|
21
|
+
/** The field's value before this pick, restored by Remove. */
|
|
22
|
+
previous: string;
|
|
23
|
+
}
|
|
24
|
+
type PickResult = {
|
|
25
|
+
path: string;
|
|
26
|
+
blob: Blob;
|
|
27
|
+
previewUrl?: string;
|
|
28
|
+
width?: number;
|
|
29
|
+
height?: number;
|
|
30
|
+
};
|
|
31
|
+
type PickImage = (field: string, file: File) => Promise<PickResult>;
|
|
32
|
+
type SubmittedImages = Record<string, {
|
|
33
|
+
path: string;
|
|
34
|
+
blob: Blob;
|
|
35
|
+
}>;
|
|
17
36
|
|
|
18
37
|
type LabelRenderer = (label: string, help: string) => ReactNode;
|
|
19
38
|
/**
|
|
@@ -70,13 +89,25 @@ interface MarkdownEditorProps {
|
|
|
70
89
|
* selection as `<Term definition="">word</Term>`. Off unless the site has such a component.
|
|
71
90
|
*/
|
|
72
91
|
termComponent?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Upload a picture for the body: shrink, name and hand back its final path and a
|
|
94
|
+
* preview URL. When set, the toolbar Image button opens a file picker (and drop or
|
|
95
|
+
* paste of an image onto the text does the same) and inserts .
|
|
96
|
+
*/
|
|
97
|
+
onUploadImage?: (file: File) => Promise<{
|
|
98
|
+
path: string;
|
|
99
|
+
previewUrl: string;
|
|
100
|
+
}>;
|
|
101
|
+
/** Paths of not-yet-committed pictures → object URLs, applied to <img src> in the preview. */
|
|
102
|
+
previewSrc?: Record<string, string>;
|
|
73
103
|
}
|
|
74
|
-
declare function MarkdownEditor({ id, name, value, onChange, placeholder, renderPreview, previewClassName, minHeight, termComponent }: MarkdownEditorProps): react.JSX.Element;
|
|
104
|
+
declare function MarkdownEditor({ id, name, value, onChange, placeholder, renderPreview, previewClassName, minHeight, termComponent, onUploadImage, previewSrc }: MarkdownEditorProps): react.JSX.Element;
|
|
75
105
|
|
|
76
106
|
interface ContentFormSubmission {
|
|
77
107
|
frontmatter: Record<string, unknown>;
|
|
78
108
|
body: string;
|
|
79
|
-
|
|
109
|
+
/** Newly picked pictures by field name — the frontmatter already holds each one's final path. */
|
|
110
|
+
images: SubmittedImages;
|
|
80
111
|
}
|
|
81
112
|
interface ContentFormProps {
|
|
82
113
|
entry: ContentTypeEntry;
|
|
@@ -99,6 +130,17 @@ interface ContentFormProps {
|
|
|
99
130
|
previewClassName?: string;
|
|
100
131
|
/** Site tooltip MDX component name (e.g. "Term") to expose as a toolbar button. */
|
|
101
132
|
termComponent?: string;
|
|
133
|
+
/**
|
|
134
|
+
* Image fields: shrink and name a chosen file (the host knows the slug and the
|
|
135
|
+
* size rules) and hand back { path, blob, previewUrl?, width?, height? }. The
|
|
136
|
+
* field's value becomes `path`; the blob rides along in the submission. Without
|
|
137
|
+
* this, image fields are plain text paths.
|
|
138
|
+
*/
|
|
139
|
+
onPickImage?: PickImage;
|
|
140
|
+
/** Editor toolbar Image button: upload a picture for the body and return its path + preview URL. */
|
|
141
|
+
onUploadImage?: MarkdownEditorProps["onUploadImage"];
|
|
142
|
+
/** Paths of not-yet-committed pictures → object URLs, so the Preview tab shows them. */
|
|
143
|
+
previewSrc?: MarkdownEditorProps["previewSrc"];
|
|
102
144
|
/**
|
|
103
145
|
* Put Cancel/Save in a bar that the host can pin to the bottom of the page
|
|
104
146
|
* (`.realiiz-form__bar`; the shell makes it sticky). The bar also reports
|
|
@@ -130,7 +172,22 @@ interface ContentFormProps {
|
|
|
130
172
|
}
|
|
131
173
|
/** "Hello, World! 2026" → "hello-world-2026" — the same rule the blog's slug regex enforces. */
|
|
132
174
|
declare function slugify(s: string): string;
|
|
133
|
-
declare function ContentForm({ entry, initialValues, initialBody, onSubmit, onCancel, submitLabel, className, showBody, bodyLabel, renderPreview, previewClassName, termComponent, stickyBar, status, barExtra, footer, renderLabel, derive, guardUnsaved }: ContentFormProps): react.JSX.Element;
|
|
175
|
+
declare function ContentForm({ entry, initialValues, initialBody, onSubmit, onCancel, submitLabel, className, showBody, bodyLabel, renderPreview, previewClassName, termComponent, onPickImage, onUploadImage, previewSrc, stickyBar, status, barExtra, footer, renderLabel, derive, guardUnsaved }: ContentFormProps): react.JSX.Element;
|
|
176
|
+
|
|
177
|
+
declare const IMAGE_HINT = "JPG, PNG or WebP, up to 10MB. We shrink it for the web.";
|
|
178
|
+
interface ImageInputProps {
|
|
179
|
+
id: string;
|
|
180
|
+
name: string;
|
|
181
|
+
/** The field's current value — a path such as /blog/my-post.webp, or "". */
|
|
182
|
+
value: string;
|
|
183
|
+
picked?: PickedImage;
|
|
184
|
+
invalid?: boolean;
|
|
185
|
+
disabled?: boolean;
|
|
186
|
+
onPick: (file: File) => Promise<void>;
|
|
187
|
+
onRemove?: () => void;
|
|
188
|
+
onBlur?: () => void;
|
|
189
|
+
}
|
|
190
|
+
declare function ImageInput(p: ImageInputProps): react.JSX.Element;
|
|
134
191
|
|
|
135
192
|
/**
|
|
136
193
|
* ChipsInput — tags as chips. Enter or comma adds, × or Backspace-on-empty
|
|
@@ -149,4 +206,4 @@ declare function ChipsInput({ id, name, value, onChange, onBlur, placeholder, in
|
|
|
149
206
|
max?: number;
|
|
150
207
|
}): react.JSX.Element;
|
|
151
208
|
|
|
152
|
-
export { ChipsInput, ContentForm, type ContentFormProps, type ContentFormSubmission, type LabelRenderer, LengthGauge, MarkdownEditor, type MarkdownEditorProps,
|
|
209
|
+
export { ChipsInput, ContentForm, type ContentFormProps, type ContentFormSubmission, IMAGE_HINT, ImageInput, type ImageInputProps, type LabelRenderer, LengthGauge, MarkdownEditor, type MarkdownEditorProps, type PickImage, type PickResult, type PickedImage, type SubmittedImages, TOOLS, slugify };
|
package/dist/forms-ui/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { walkSchema } from '../chunk-J6FI73PF.js';
|
|
3
|
-
import { FORM_CSS } from '../chunk-
|
|
3
|
+
import { FORM_CSS } from '../chunk-FTD4WE3V.js';
|
|
4
4
|
import { useState, useRef, useCallback, useEffect, useId, useMemo } from 'react';
|
|
5
5
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
6
|
|
|
@@ -82,16 +82,7 @@ function Control(p) {
|
|
|
82
82
|
case "boolean":
|
|
83
83
|
return /* @__PURE__ */ jsx("input", { ...common, type: "checkbox", checked: p.value === true, onChange: (e) => p.onChange(e.target.checked) });
|
|
84
84
|
case "image":
|
|
85
|
-
return /* @__PURE__ */ jsx(
|
|
86
|
-
"input",
|
|
87
|
-
{
|
|
88
|
-
...common,
|
|
89
|
-
type: "file",
|
|
90
|
-
accept: "image/*",
|
|
91
|
-
className: cls,
|
|
92
|
-
onChange: (e) => p.onChange(e.target.files?.[0] ?? null)
|
|
93
|
-
}
|
|
94
|
-
);
|
|
85
|
+
return /* @__PURE__ */ jsx("input", { ...common, type: "text", className: cls, value: str, placeholder: p.spec.placeholder ?? "/images/picture.webp", onChange: (e) => p.onChange(e.target.value) });
|
|
95
86
|
case "date":
|
|
96
87
|
return /* @__PURE__ */ jsx("input", { ...common, type: "date", className: cls, value: str, onChange: (e) => p.onChange(e.target.value) });
|
|
97
88
|
case "number":
|
|
@@ -137,6 +128,151 @@ function Control(p) {
|
|
|
137
128
|
);
|
|
138
129
|
}
|
|
139
130
|
}
|
|
131
|
+
function useImages() {
|
|
132
|
+
const [images, setImages] = useState({});
|
|
133
|
+
const latest = useRef(images);
|
|
134
|
+
latest.current = images;
|
|
135
|
+
const urls = useRef(/* @__PURE__ */ new Set());
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
const set = urls.current;
|
|
138
|
+
return () => {
|
|
139
|
+
for (const u of set) URL.revokeObjectURL(u);
|
|
140
|
+
};
|
|
141
|
+
}, []);
|
|
142
|
+
const keep = useCallback((field, r, previous) => {
|
|
143
|
+
const previewUrl = r.previewUrl ?? URL.createObjectURL(r.blob);
|
|
144
|
+
if (!r.previewUrl) urls.current.add(previewUrl);
|
|
145
|
+
const picked = { path: r.path, blob: r.blob, previewUrl, width: r.width, height: r.height, previous };
|
|
146
|
+
setImages((m) => ({ ...m, [field]: picked }));
|
|
147
|
+
return picked;
|
|
148
|
+
}, []);
|
|
149
|
+
const remove = useCallback((field) => {
|
|
150
|
+
const previous = latest.current[field]?.previous;
|
|
151
|
+
setImages((m) => {
|
|
152
|
+
const rest = { ...m };
|
|
153
|
+
delete rest[field];
|
|
154
|
+
return rest;
|
|
155
|
+
});
|
|
156
|
+
return previous;
|
|
157
|
+
}, []);
|
|
158
|
+
const toSubmission = useCallback(() => {
|
|
159
|
+
return Object.fromEntries(Object.entries(images).map(([k, v]) => [k, { path: v.path, blob: v.blob }]));
|
|
160
|
+
}, [images]);
|
|
161
|
+
return { images, keep, remove, toSubmission };
|
|
162
|
+
}
|
|
163
|
+
function formatBytes(n) {
|
|
164
|
+
if (n < 1024 * 1024) return `${Math.max(1, Math.round(n / 1024))} KB`;
|
|
165
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/forms-ui/md/upload.ts
|
|
169
|
+
var IMAGE_ACCEPT = "image/jpeg,image/png,image/webp,image/gif";
|
|
170
|
+
function pickFile(accept = IMAGE_ACCEPT) {
|
|
171
|
+
return new Promise((resolve) => {
|
|
172
|
+
const input = document.createElement("input");
|
|
173
|
+
input.type = "file";
|
|
174
|
+
input.accept = accept;
|
|
175
|
+
input.style.display = "none";
|
|
176
|
+
document.body.appendChild(input);
|
|
177
|
+
const done = () => {
|
|
178
|
+
resolve(input.files?.[0]);
|
|
179
|
+
input.remove();
|
|
180
|
+
};
|
|
181
|
+
input.addEventListener("change", done, { once: true });
|
|
182
|
+
window.addEventListener("focus", () => setTimeout(() => {
|
|
183
|
+
if (document.body.contains(input)) done();
|
|
184
|
+
}, 400), { once: true });
|
|
185
|
+
input.click();
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function imageFromDrop(e) {
|
|
189
|
+
return Array.from(e.dataTransfer?.files ?? []).find((f) => f.type.startsWith("image/"));
|
|
190
|
+
}
|
|
191
|
+
function imageFromPaste(e) {
|
|
192
|
+
const item = Array.from(e.clipboardData?.items ?? []).find((i) => i.kind === "file" && i.type.startsWith("image/"));
|
|
193
|
+
return item?.getAsFile() ?? void 0;
|
|
194
|
+
}
|
|
195
|
+
function messageOf(err) {
|
|
196
|
+
if (err && typeof err === "object" && err.name === "MediaError") return err.message;
|
|
197
|
+
return "That picture couldn\u2019t be prepared. Try a different file.";
|
|
198
|
+
}
|
|
199
|
+
var IMAGE_HINT = "JPG, PNG or WebP, up to 10MB. We shrink it for the web.";
|
|
200
|
+
function ImageInput(p) {
|
|
201
|
+
const [busy, setBusy] = useState(false);
|
|
202
|
+
const [error, setError] = useState(null);
|
|
203
|
+
const [over, setOver] = useState(false);
|
|
204
|
+
const zone = useRef(null);
|
|
205
|
+
const take = async (file) => {
|
|
206
|
+
if (!file || busy || p.disabled) return;
|
|
207
|
+
setBusy(true);
|
|
208
|
+
setError(null);
|
|
209
|
+
try {
|
|
210
|
+
await p.onPick(file);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
setError(messageOf(err));
|
|
213
|
+
} finally {
|
|
214
|
+
setBusy(false);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const choose = async () => take(await pickFile());
|
|
218
|
+
const onDrop = (e) => {
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
setOver(false);
|
|
221
|
+
void take(imageFromDrop(e));
|
|
222
|
+
};
|
|
223
|
+
const onPaste = (e) => {
|
|
224
|
+
const f = imageFromPaste(e);
|
|
225
|
+
if (f) {
|
|
226
|
+
e.preventDefault();
|
|
227
|
+
void take(f);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const src = p.picked?.previewUrl ?? (p.value || null);
|
|
231
|
+
const detail = p.picked ? [p.picked.width && p.picked.height ? `${p.picked.width} \xD7 ${p.picked.height}` : null, formatBytes(p.picked.blob.size)].filter(Boolean).join(" \xB7 ") : p.value;
|
|
232
|
+
return /* @__PURE__ */ jsxs(
|
|
233
|
+
"div",
|
|
234
|
+
{
|
|
235
|
+
ref: zone,
|
|
236
|
+
className: ["realiiz-image", over && "realiiz-image--over", p.invalid && "realiiz-image--invalid", busy && "realiiz-image--busy"].filter(Boolean).join(" "),
|
|
237
|
+
"data-field": p.name,
|
|
238
|
+
onDragOver: (e) => {
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
if (!over) setOver(true);
|
|
241
|
+
},
|
|
242
|
+
onDragLeave: () => setOver(false),
|
|
243
|
+
onDrop,
|
|
244
|
+
onPaste,
|
|
245
|
+
onBlur: p.onBlur,
|
|
246
|
+
tabIndex: -1,
|
|
247
|
+
children: [
|
|
248
|
+
/* @__PURE__ */ jsx("input", { id: p.id, name: p.name, type: "text", value: p.value, readOnly: true, className: "realiiz-sr-only", "aria-busy": busy || void 0 }),
|
|
249
|
+
src ? /* @__PURE__ */ jsxs("div", { className: "realiiz-image__has", children: [
|
|
250
|
+
/* @__PURE__ */ jsx("img", { src, alt: "", className: "realiiz-image__thumb", "data-testid": "realiiz-image-thumb" }),
|
|
251
|
+
/* @__PURE__ */ jsxs("div", { className: "realiiz-image__meta", children: [
|
|
252
|
+
/* @__PURE__ */ jsx("div", { className: "realiiz-image__detail", children: busy ? "Shrinking\u2026" : detail }),
|
|
253
|
+
/* @__PURE__ */ jsxs("div", { className: "realiiz-image__actions", children: [
|
|
254
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "realiiz-form__button", onClick: choose, disabled: busy || p.disabled, children: "Replace" }),
|
|
255
|
+
p.picked && p.onRemove && /* @__PURE__ */ jsx("button", { type: "button", className: "realiiz-image__remove", onClick: p.onRemove, disabled: busy, children: "Remove" })
|
|
256
|
+
] })
|
|
257
|
+
] })
|
|
258
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "realiiz-image__empty", children: [
|
|
259
|
+
/* @__PURE__ */ jsx(PictureIcon, {}),
|
|
260
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "realiiz-form__button", onClick: choose, disabled: busy || p.disabled, children: busy ? "Shrinking\u2026" : "Choose a picture" }),
|
|
261
|
+
/* @__PURE__ */ jsx("span", { className: "realiiz-image__or", children: "or drag one here" })
|
|
262
|
+
] }),
|
|
263
|
+
/* @__PURE__ */ jsx("div", { className: "realiiz-image__hint", children: IMAGE_HINT }),
|
|
264
|
+
error && /* @__PURE__ */ jsx("div", { className: "realiiz-form__error", role: "alert", children: error })
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
function PictureIcon() {
|
|
270
|
+
return /* @__PURE__ */ jsxs("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: "realiiz-image__icon", children: [
|
|
271
|
+
/* @__PURE__ */ jsx("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
|
|
272
|
+
/* @__PURE__ */ jsx("circle", { cx: "9", cy: "10", r: "1.6" }),
|
|
273
|
+
/* @__PURE__ */ jsx("path", { d: "M21 16l-5-5-8 8" })
|
|
274
|
+
] });
|
|
275
|
+
}
|
|
140
276
|
var defaultLabel = (label, help) => /* @__PURE__ */ jsx("span", { className: "realiiz-form__help", title: help, children: label });
|
|
141
277
|
function LengthGauge({ length: raw, range, suffix }) {
|
|
142
278
|
const [min, max] = range;
|
|
@@ -169,7 +305,19 @@ function Field(p) {
|
|
|
169
305
|
const isCheck = p.spec.input === "boolean";
|
|
170
306
|
const isTags = p.spec.input === "tags";
|
|
171
307
|
const labelText = p.spec.help ? (p.renderLabel ?? defaultLabel)(p.spec.label, p.spec.help) : p.spec.label;
|
|
172
|
-
const control =
|
|
308
|
+
const control = p.spec.input === "image" && p.onPickImage ? /* @__PURE__ */ jsx(
|
|
309
|
+
ImageInput,
|
|
310
|
+
{
|
|
311
|
+
id,
|
|
312
|
+
name: p.spec.name,
|
|
313
|
+
value: typeof p.value === "string" ? p.value : "",
|
|
314
|
+
picked: p.picked,
|
|
315
|
+
invalid,
|
|
316
|
+
onPick: p.onPickImage,
|
|
317
|
+
onRemove: p.onRemoveImage,
|
|
318
|
+
onBlur: p.onBlur
|
|
319
|
+
}
|
|
320
|
+
) : isTags ? /* @__PURE__ */ jsx(
|
|
173
321
|
ChipsInput,
|
|
174
322
|
{
|
|
175
323
|
id,
|
|
@@ -196,10 +344,6 @@ function Field(p) {
|
|
|
196
344
|
] }),
|
|
197
345
|
control
|
|
198
346
|
] }),
|
|
199
|
-
p.spec.input === "image" && p.current && !(p.value instanceof File) && /* @__PURE__ */ jsxs("div", { className: "realiiz-form__current", children: [
|
|
200
|
-
"Current: ",
|
|
201
|
-
p.current
|
|
202
|
-
] }),
|
|
203
347
|
isText && (p.spec.recommended || typeof max === "number") && /* @__PURE__ */ jsxs("div", { className: "realiiz-form__helper", children: [
|
|
204
348
|
p.spec.recommended ? /* @__PURE__ */ jsx(LengthGauge, { length: len, range: p.spec.recommended, suffix: p.spec.suffix }) : /* @__PURE__ */ jsx("span", {}),
|
|
205
349
|
typeof max === "number" && /* @__PURE__ */ jsxs("span", { className: "realiiz-form__count", children: [
|
|
@@ -534,7 +678,7 @@ var TABLE_OPS = [
|
|
|
534
678
|
{ op: "colAfter", label: "+ Col", tip: "Insert a column to the right" },
|
|
535
679
|
{ op: "colDel", label: "\u2212 Col", tip: "Delete this column" }
|
|
536
680
|
];
|
|
537
|
-
function MarkdownEditor({ id, name = "body", value, onChange, placeholder, renderPreview, previewClassName, minHeight = 320, termComponent }) {
|
|
681
|
+
function MarkdownEditor({ id, name = "body", value, onChange, placeholder, renderPreview, previewClassName, minHeight = 320, termComponent, onUploadImage, previewSrc }) {
|
|
538
682
|
const [mode, setMode] = useState(() => renderPreview && value.trim() ? "preview" : "edit");
|
|
539
683
|
const [preview, setPreview] = useState(null);
|
|
540
684
|
const [loading, setLoading] = useState(false);
|
|
@@ -542,6 +686,7 @@ function MarkdownEditor({ id, name = "body", value, onChange, placeholder, rende
|
|
|
542
686
|
const [on, setOn] = useState(() => /* @__PURE__ */ new Set());
|
|
543
687
|
const [inTable, setInTable] = useState(false);
|
|
544
688
|
const [note, setNote] = useState(null);
|
|
689
|
+
const [uploading, setUploading] = useState(false);
|
|
545
690
|
const ta = useRef(null);
|
|
546
691
|
const sync = useCallback(() => {
|
|
547
692
|
const el = ta.current;
|
|
@@ -588,6 +733,37 @@ function MarkdownEditor({ id, name = "body", value, onChange, placeholder, rende
|
|
|
588
733
|
setNote(null);
|
|
589
734
|
commit(r);
|
|
590
735
|
};
|
|
736
|
+
const upload = async (file) => {
|
|
737
|
+
const el = ta.current;
|
|
738
|
+
if (!file || !onUploadImage || !el || uploading) return;
|
|
739
|
+
setUploading(true);
|
|
740
|
+
setNote(null);
|
|
741
|
+
const a = el.selectionStart, b = el.selectionEnd;
|
|
742
|
+
try {
|
|
743
|
+
const { path } = await onUploadImage(file);
|
|
744
|
+
const alt = value.slice(a, b) || "description";
|
|
745
|
+
const text = value.slice(0, a) + `` + value.slice(b);
|
|
746
|
+
commit({ text, a: a + 2, b: a + 2 + alt.length });
|
|
747
|
+
} catch (err) {
|
|
748
|
+
setNote(messageOf(err));
|
|
749
|
+
} finally {
|
|
750
|
+
setUploading(false);
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
const onDrop = (e) => {
|
|
754
|
+
const f = imageFromDrop(e);
|
|
755
|
+
if (f && onUploadImage) {
|
|
756
|
+
e.preventDefault();
|
|
757
|
+
void upload(f);
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
const onPaste = (e) => {
|
|
761
|
+
const f = imageFromPaste(e);
|
|
762
|
+
if (f && onUploadImage) {
|
|
763
|
+
e.preventDefault();
|
|
764
|
+
void upload(f);
|
|
765
|
+
}
|
|
766
|
+
};
|
|
591
767
|
const onKey = (e) => {
|
|
592
768
|
if (e.key === "Escape" && expanded) {
|
|
593
769
|
e.preventDefault();
|
|
@@ -626,7 +802,7 @@ function MarkdownEditor({ id, name = "body", value, onChange, placeholder, rende
|
|
|
626
802
|
let live = true;
|
|
627
803
|
setLoading(true);
|
|
628
804
|
renderPreview(value).then((r) => {
|
|
629
|
-
if (live) setPreview(r);
|
|
805
|
+
if (live) setPreview("html" in r ? { html: swapPreviewSrc(r.html, previewSrc) } : r);
|
|
630
806
|
}).catch((err) => {
|
|
631
807
|
if (live) setPreview({ error: err instanceof Error ? err.message : String(err) });
|
|
632
808
|
}).finally(() => {
|
|
@@ -635,11 +811,13 @@ function MarkdownEditor({ id, name = "body", value, onChange, placeholder, rende
|
|
|
635
811
|
return () => {
|
|
636
812
|
live = false;
|
|
637
813
|
};
|
|
638
|
-
}, [mode, value, renderPreview]);
|
|
814
|
+
}, [mode, value, renderPreview, previewSrc]);
|
|
639
815
|
const termTool = termComponent ? { id: "term", title: `Tooltip \u2014 wraps the selection in <${termComponent}>`, apply: toggleTerm(termComponent) } : null;
|
|
640
816
|
const btn = (toolId) => {
|
|
641
817
|
const t = toolId === "term" && termTool ? termTool : toolById(toolId);
|
|
642
|
-
|
|
818
|
+
const uploads = t.id === "image" && Boolean(onUploadImage);
|
|
819
|
+
const title = uploads ? uploading ? "Shrinking\u2026" : "Picture \u2014 choose a file, or drop/paste one into the text" : t.title;
|
|
820
|
+
return /* @__PURE__ */ jsx("button", { type: "button", "data-tool": t.id, "data-tip": title, "aria-label": title, "aria-pressed": on.has(t.id) || void 0, "aria-busy": uploads && uploading ? true : void 0, className: on.has(t.id) ? "on" : void 0, disabled: uploads && uploading, onMouseDown: (e) => e.preventDefault(), onClick: () => uploads ? pickFile().then(upload) : run(t.apply), children: MD_ICONS[t.id] }, t.id);
|
|
643
821
|
};
|
|
644
822
|
return /* @__PURE__ */ jsxs("div", { className: ["realiiz-md", expanded && "realiiz-md--fs"].filter(Boolean).join(" "), children: [
|
|
645
823
|
renderPreview && /* @__PURE__ */ jsxs("span", { className: "realiiz-md__mode", role: "tablist", "aria-label": "Editor mode", children: [
|
|
@@ -696,6 +874,8 @@ function MarkdownEditor({ id, name = "body", value, onChange, placeholder, rende
|
|
|
696
874
|
onKeyUp: sync,
|
|
697
875
|
onClick: sync,
|
|
698
876
|
onKeyDown: onKey,
|
|
877
|
+
onDrop,
|
|
878
|
+
onPaste,
|
|
699
879
|
spellCheck: true
|
|
700
880
|
}
|
|
701
881
|
) : /* @__PURE__ */ jsxs("div", { className: "realiiz-md__preview", style: expanded ? void 0 : { minHeight }, "aria-live": "polite", "aria-busy": loading, children: [
|
|
@@ -707,9 +887,13 @@ function MarkdownEditor({ id, name = "body", value, onChange, placeholder, rende
|
|
|
707
887
|
!loading && preview && "html" in preview && /* @__PURE__ */ jsx("div", { className: ["realiiz-prose", previewClassName].filter(Boolean).join(" "), dangerouslySetInnerHTML: { __html: preview.html } })
|
|
708
888
|
] })
|
|
709
889
|
] }),
|
|
710
|
-
note && /* @__PURE__ */ jsx("div", { className: "realiiz-form__helper realiiz-md__note", role: "status", children: note })
|
|
890
|
+
(note || uploading) && /* @__PURE__ */ jsx("div", { className: "realiiz-form__helper realiiz-md__note", role: "status", children: uploading ? "Shrinking\u2026" : note })
|
|
711
891
|
] });
|
|
712
892
|
}
|
|
893
|
+
function swapPreviewSrc(html, map) {
|
|
894
|
+
if (!map || Object.keys(map).length === 0) return html;
|
|
895
|
+
return html.replace(/(<img\b[^>]*\bsrc=")([^"]+)(")/g, (m, pre, src, post) => map[src] ? `${pre}${map[src]}${post}` : m);
|
|
896
|
+
}
|
|
713
897
|
function commonPrefix(a, b) {
|
|
714
898
|
const n = Math.min(a.length, b.length);
|
|
715
899
|
let i = 0;
|
|
@@ -766,7 +950,6 @@ function groupIssues(list) {
|
|
|
766
950
|
}
|
|
767
951
|
|
|
768
952
|
// src/forms-ui/values.ts
|
|
769
|
-
var PENDING_UPLOAD_PREFIX = "pending-upload/";
|
|
770
953
|
function toControl(spec, raw) {
|
|
771
954
|
if (raw === void 0 || raw === null) return spec.input === "boolean" ? false : "";
|
|
772
955
|
switch (spec.input) {
|
|
@@ -785,7 +968,6 @@ function toControl(spec, raw) {
|
|
|
785
968
|
}
|
|
786
969
|
function toFrontmatter(specs, state, hiddenValues) {
|
|
787
970
|
const frontmatter = { ...hiddenValues };
|
|
788
|
-
const files = {};
|
|
789
971
|
for (const spec of specs) {
|
|
790
972
|
if (spec.role === "hidden" || spec.role === "body") continue;
|
|
791
973
|
const v = state[spec.name];
|
|
@@ -794,11 +976,6 @@ function toFrontmatter(specs, state, hiddenValues) {
|
|
|
794
976
|
else if (spec.required) frontmatter[spec.name] = false;
|
|
795
977
|
continue;
|
|
796
978
|
}
|
|
797
|
-
if (v instanceof File) {
|
|
798
|
-
files[spec.name] = v;
|
|
799
|
-
frontmatter[spec.name] = PENDING_UPLOAD_PREFIX + v.name;
|
|
800
|
-
continue;
|
|
801
|
-
}
|
|
802
979
|
if (Array.isArray(v)) {
|
|
803
980
|
if (v.length || spec.required) frontmatter[spec.name] = v;
|
|
804
981
|
continue;
|
|
@@ -821,13 +998,13 @@ function toFrontmatter(specs, state, hiddenValues) {
|
|
|
821
998
|
frontmatter[spec.name] = s;
|
|
822
999
|
}
|
|
823
1000
|
}
|
|
824
|
-
return
|
|
1001
|
+
return frontmatter;
|
|
825
1002
|
}
|
|
826
1003
|
var LEAVE_MESSAGE = "You have unsaved changes. Leave this page and lose them?";
|
|
827
1004
|
function slugify(s) {
|
|
828
1005
|
return s.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
829
1006
|
}
|
|
830
|
-
function ContentForm({ entry, initialValues = {}, initialBody = "", onSubmit, onCancel, submitLabel = "Save", className, showBody = true, bodyLabel = "Content", renderPreview, previewClassName, termComponent, stickyBar = false, status, barExtra, footer, renderLabel, derive, guardUnsaved = true }) {
|
|
1007
|
+
function ContentForm({ entry, initialValues = {}, initialBody = "", onSubmit, onCancel, submitLabel = "Save", className, showBody = true, bodyLabel = "Content", renderPreview, previewClassName, termComponent, onPickImage, onUploadImage, previewSrc, stickyBar = false, status, barExtra, footer, renderLabel, derive, guardUnsaved = true }) {
|
|
831
1008
|
const formId = useId();
|
|
832
1009
|
const specs = useMemo(() => walkSchema(entry.schema, entry), [entry]);
|
|
833
1010
|
const visible = useMemo(() => specs.filter((s) => s.role !== "hidden" && s.role !== "body"), [specs]);
|
|
@@ -855,6 +1032,16 @@ function ContentForm({ entry, initialValues = {}, initialBody = "", onSubmit, on
|
|
|
855
1032
|
update(name, v);
|
|
856
1033
|
};
|
|
857
1034
|
const [body, setBody] = useState(initialBody);
|
|
1035
|
+
const pictures = useImages();
|
|
1036
|
+
const pickImage = onPickImage ? (field) => async (file) => {
|
|
1037
|
+
const r = await onPickImage(field, file);
|
|
1038
|
+
pictures.keep(field, r, typeof state[field] === "string" ? state[field] : "");
|
|
1039
|
+
onFieldChange(field, r.path);
|
|
1040
|
+
} : void 0;
|
|
1041
|
+
const removeImage = (field) => () => {
|
|
1042
|
+
const previous = pictures.remove(field);
|
|
1043
|
+
onFieldChange(field, previous ?? "");
|
|
1044
|
+
};
|
|
858
1045
|
const dirty = body !== initialBody || specs.some((s) => !sameValue(state[s.name], initialState[s.name]));
|
|
859
1046
|
const [issues, setIssues] = useState([]);
|
|
860
1047
|
const [attempted, setAttempted] = useState(false);
|
|
@@ -904,7 +1091,7 @@ function ContentForm({ entry, initialValues = {}, initialBody = "", onSubmit, on
|
|
|
904
1091
|
};
|
|
905
1092
|
}, [guardUnsaved, dirty, submitting]);
|
|
906
1093
|
const validate = useCallback(() => {
|
|
907
|
-
const
|
|
1094
|
+
const frontmatter = toFrontmatter(specs, state, hiddenValues);
|
|
908
1095
|
const result = entry.schema.safeParse(frontmatter);
|
|
909
1096
|
setIssues(result.success ? [] : mapIssues(result.error.issues, specs));
|
|
910
1097
|
return result;
|
|
@@ -915,10 +1102,9 @@ function ContentForm({ entry, initialValues = {}, initialBody = "", onSubmit, on
|
|
|
915
1102
|
setAttempted(true);
|
|
916
1103
|
const result = validate();
|
|
917
1104
|
if (!result.success) return;
|
|
918
|
-
const { files } = toFrontmatter(specs, state, hiddenValues);
|
|
919
1105
|
setSubmitting(true);
|
|
920
1106
|
try {
|
|
921
|
-
await onSubmit({ frontmatter: result.data, body,
|
|
1107
|
+
await onSubmit({ frontmatter: result.data, body, images: pictures.toSubmission() });
|
|
922
1108
|
} finally {
|
|
923
1109
|
setSubmitting(false);
|
|
924
1110
|
}
|
|
@@ -940,9 +1126,11 @@ function ContentForm({ entry, initialValues = {}, initialBody = "", onSubmit, on
|
|
|
940
1126
|
spec: s,
|
|
941
1127
|
value: state[s.name],
|
|
942
1128
|
issues: byField.get(s.name) ?? [],
|
|
943
|
-
current: typeof initialValues[s.name] === "string" ? initialValues[s.name] : void 0,
|
|
944
1129
|
options: optionsFor(s),
|
|
945
1130
|
renderLabel,
|
|
1131
|
+
picked: pictures.images[s.name],
|
|
1132
|
+
onPickImage: pickImage?.(s.name),
|
|
1133
|
+
onRemoveImage: removeImage(s.name),
|
|
946
1134
|
onChange: (v) => onFieldChange(s.name, v),
|
|
947
1135
|
onBlur: () => {
|
|
948
1136
|
if (attempted) validate();
|
|
@@ -963,7 +1151,9 @@ function ContentForm({ entry, initialValues = {}, initialBody = "", onSubmit, on
|
|
|
963
1151
|
placeholder: bodySpec?.placeholder ?? "Write in Markdown. Headings with ##, links as [text](url).",
|
|
964
1152
|
renderPreview,
|
|
965
1153
|
previewClassName,
|
|
966
|
-
termComponent
|
|
1154
|
+
termComponent,
|
|
1155
|
+
onUploadImage,
|
|
1156
|
+
previewSrc
|
|
967
1157
|
}
|
|
968
1158
|
)
|
|
969
1159
|
] }),
|
|
@@ -981,6 +1171,6 @@ function sameValue(a, b) {
|
|
|
981
1171
|
return a === b;
|
|
982
1172
|
}
|
|
983
1173
|
|
|
984
|
-
export { ChipsInput, ContentForm, LengthGauge, MarkdownEditor,
|
|
1174
|
+
export { ChipsInput, ContentForm, IMAGE_HINT, ImageInput, LengthGauge, MarkdownEditor, TOOLS, slugify };
|
|
985
1175
|
//# sourceMappingURL=index.js.map
|
|
986
1176
|
//# sourceMappingURL=index.js.map
|