@vertebr/react 0.2.0 → 0.4.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/components/admin/VertebrAdmin.d.ts.map +1 -1
- package/dist/components/admin/VertebrAdmin.js +26 -4
- package/dist/components/config/ConfigForm.d.ts +2 -18
- package/dist/components/config/ConfigForm.d.ts.map +1 -1
- package/dist/components/config/ConfigForm.js +1 -281
- package/dist/components/config/DescriptionInput.d.ts +18 -0
- package/dist/components/config/DescriptionInput.d.ts.map +1 -0
- package/dist/components/config/DescriptionInput.js +152 -0
- package/dist/components/config/EnumInput.d.ts +10 -0
- package/dist/components/config/EnumInput.d.ts.map +1 -0
- package/dist/components/config/EnumInput.js +19 -0
- package/dist/components/config/EnumListInput.d.ts +10 -0
- package/dist/components/config/EnumListInput.d.ts.map +1 -0
- package/dist/components/config/EnumListInput.js +46 -0
- package/dist/components/config/ImagePickerModal.d.ts +18 -0
- package/dist/components/config/ImagePickerModal.d.ts.map +1 -0
- package/dist/components/config/ImagePickerModal.js +120 -0
- package/dist/components/config/PriceInput.d.ts +12 -0
- package/dist/components/config/PriceInput.d.ts.map +1 -0
- package/dist/components/config/PriceInput.js +20 -0
- package/dist/components/config/VisibilityEye.d.ts +8 -0
- package/dist/components/config/VisibilityEye.d.ts.map +1 -0
- package/dist/components/config/VisibilityEye.js +18 -0
- package/dist/components/config/form/ConfigField.d.ts +22 -0
- package/dist/components/config/form/ConfigField.d.ts.map +1 -0
- package/dist/components/config/form/ConfigField.js +53 -0
- package/dist/components/config/form/ConfigForm.d.ts +4 -0
- package/dist/components/config/form/ConfigForm.d.ts.map +1 -0
- package/dist/components/config/form/ConfigForm.js +75 -0
- package/dist/components/config/form/ConfigFormActions.d.ts +11 -0
- package/dist/components/config/form/ConfigFormActions.d.ts.map +1 -0
- package/dist/components/config/form/ConfigFormActions.js +4 -0
- package/dist/components/config/form/RecordConfigForm.d.ts +22 -0
- package/dist/components/config/form/RecordConfigForm.d.ts.map +1 -0
- package/dist/components/config/form/RecordConfigForm.js +45 -0
- package/dist/components/config/form/SettingsConfigForm.d.ts +22 -0
- package/dist/components/config/form/SettingsConfigForm.d.ts.map +1 -0
- package/dist/components/config/form/SettingsConfigForm.js +7 -0
- package/dist/components/config/form/helpers/fields.d.ts +7 -0
- package/dist/components/config/form/helpers/fields.d.ts.map +1 -0
- package/dist/components/config/form/helpers/fields.js +31 -0
- package/dist/components/config/form/helpers/groups.d.ts +4 -0
- package/dist/components/config/form/helpers/groups.d.ts.map +1 -0
- package/dist/components/config/form/helpers/groups.js +107 -0
- package/dist/components/config/form/helpers/images.d.ts +4 -0
- package/dist/components/config/form/helpers/images.d.ts.map +1 -0
- package/dist/components/config/form/helpers/images.js +27 -0
- package/dist/components/config/form/helpers/index.d.ts +7 -0
- package/dist/components/config/form/helpers/index.d.ts.map +1 -0
- package/dist/components/config/form/helpers/index.js +6 -0
- package/dist/components/config/form/helpers/labels.d.ts +5 -0
- package/dist/components/config/form/helpers/labels.d.ts.map +1 -0
- package/dist/components/config/form/helpers/labels.js +28 -0
- package/dist/components/config/form/helpers/schema.d.ts +8 -0
- package/dist/components/config/form/helpers/schema.d.ts.map +1 -0
- package/dist/components/config/form/helpers/schema.js +30 -0
- package/dist/components/config/form/helpers/visibility.d.ts +13 -0
- package/dist/components/config/form/helpers/visibility.d.ts.map +1 -0
- package/dist/components/config/form/helpers/visibility.js +86 -0
- package/dist/components/config/form/types.d.ts +29 -0
- package/dist/components/config/form/types.d.ts.map +1 -0
- package/dist/components/config/form/types.js +1 -0
- package/dist/components/resources/ResourceCell.d.ts.map +1 -1
- package/dist/components/resources/ResourceCell.js +5 -19
- package/dist/components/resources/ResourceCreateModal.d.ts +18 -0
- package/dist/components/resources/ResourceCreateModal.d.ts.map +1 -0
- package/dist/components/resources/ResourceCreateModal.js +22 -0
- package/dist/components/resources/ResourceTable.d.ts +7 -2
- package/dist/components/resources/ResourceTable.d.ts.map +1 -1
- package/dist/components/resources/ResourceTable.js +92 -7
- package/dist/hooks/useVertebrControls.d.ts.map +1 -1
- package/dist/hooks/useVertebrControls.js +5 -4
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/lib/description.d.ts +18 -0
- package/dist/lib/description.d.ts.map +1 -0
- package/dist/lib/description.js +85 -0
- package/dist/lib/images.d.ts +14 -0
- package/dist/lib/images.d.ts.map +1 -0
- package/dist/lib/images.js +63 -0
- package/dist/lib/price.d.ts +15 -0
- package/dist/lib/price.d.ts.map +1 -0
- package/dist/lib/price.js +50 -0
- package/dist/styles/form.css +420 -0
- package/dist/styles/modal.css +133 -0
- package/dist/styles/panel.css +18 -1
- package/dist/styles/table.css +10 -1
- package/package.json +10 -3
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { flexRender, getCoreRowModel, useReactTable, } from "@tanstack/react-table";
|
|
4
4
|
import { ConfigForm } from "../config/ConfigForm";
|
|
5
5
|
import { BulkImageUploadModal } from "./BulkImageUploadModal";
|
|
6
6
|
import { ColumnVisibilityMenu } from "./ColumnVisibilityMenu";
|
|
7
7
|
import { ResourceCell } from "./ResourceCell";
|
|
8
|
+
import { ResourceCreateModal } from "./ResourceCreateModal";
|
|
8
9
|
import { ResourceViewModal } from "./ResourceViewModal";
|
|
9
10
|
function resolveRowId(row, keys) {
|
|
10
11
|
for (const key of keys) {
|
|
@@ -57,6 +58,17 @@ function writeVisibility(storageKey, value) {
|
|
|
57
58
|
/* ignore quota / private mode */
|
|
58
59
|
}
|
|
59
60
|
}
|
|
61
|
+
function addRecordLabel(resourceLabel) {
|
|
62
|
+
const name = resourceLabel?.trim();
|
|
63
|
+
if (!name)
|
|
64
|
+
return "Add record";
|
|
65
|
+
const singular = /ies$/i.test(name)
|
|
66
|
+
? name.replace(/ies$/i, "y")
|
|
67
|
+
: /s$/i.test(name)
|
|
68
|
+
? name.slice(0, -1)
|
|
69
|
+
: name;
|
|
70
|
+
return `Add ${singular.toLowerCase()}`;
|
|
71
|
+
}
|
|
60
72
|
function titleFromFilename(name) {
|
|
61
73
|
return name
|
|
62
74
|
.replace(/\.[^.]+$/, "")
|
|
@@ -64,15 +76,23 @@ function titleFromFilename(name) {
|
|
|
64
76
|
.replace(/\s+/g, " ")
|
|
65
77
|
.trim();
|
|
66
78
|
}
|
|
67
|
-
export function ResourcePanel({ schema, items, permissions, actions = {}, uploads, rowIdKeys = ["id", "slug", "uid", "email"], storageKey, onUpdate, onCreate, onDelete, onAction, onUpload, }) {
|
|
79
|
+
export function ResourcePanel({ schema, items, permissions, actions = {}, label, uploads, rowIdKeys = ["id", "slug", "uid", "email"], storageKey, onUpdate, onCreate, onDelete, onAction, onUpload, galleryUpload, onListGallery, onUploadGallery, }) {
|
|
68
80
|
const [editing, setEditing] = useState(null);
|
|
81
|
+
const [creating, setCreating] = useState(false);
|
|
69
82
|
const [viewing, setViewing] = useState(null);
|
|
70
83
|
const [bulkOpen, setBulkOpen] = useState(false);
|
|
71
84
|
const [busyAction, setBusyAction] = useState(null);
|
|
85
|
+
const tableWrapRef = useRef(null);
|
|
86
|
+
const [tableOverflow, setTableOverflow] = useState({
|
|
87
|
+
overflow: false,
|
|
88
|
+
canLeft: false,
|
|
89
|
+
canRight: false,
|
|
90
|
+
});
|
|
72
91
|
const [columnVisibility, setColumnVisibility] = useState(() => readVisibility(storageKey));
|
|
73
92
|
const canUpdate = Boolean(permissions?.update && onUpdate);
|
|
74
93
|
const canCreate = Boolean(permissions?.create && onCreate);
|
|
75
94
|
const canDelete = Boolean(permissions?.delete && onDelete);
|
|
95
|
+
const createLabel = addRecordLabel(label);
|
|
76
96
|
const canUpload = Boolean(uploads && onUpload && Object.keys(uploads).length);
|
|
77
97
|
const actionEntries = Object.entries(actions);
|
|
78
98
|
const fieldKeys = Object.keys(schema);
|
|
@@ -82,6 +102,43 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, upload
|
|
|
82
102
|
useEffect(() => {
|
|
83
103
|
writeVisibility(storageKey, columnVisibility);
|
|
84
104
|
}, [storageKey, columnVisibility]);
|
|
105
|
+
const updateTableOverflow = useCallback(() => {
|
|
106
|
+
const el = tableWrapRef.current;
|
|
107
|
+
if (!el) {
|
|
108
|
+
setTableOverflow({ overflow: false, canLeft: false, canRight: false });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const overflow = el.scrollWidth > el.clientWidth + 2;
|
|
112
|
+
setTableOverflow({
|
|
113
|
+
overflow,
|
|
114
|
+
canLeft: overflow && el.scrollLeft > 2,
|
|
115
|
+
canRight: overflow && el.scrollLeft + el.clientWidth < el.scrollWidth - 2,
|
|
116
|
+
});
|
|
117
|
+
}, []);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const el = tableWrapRef.current;
|
|
120
|
+
if (!el)
|
|
121
|
+
return;
|
|
122
|
+
updateTableOverflow();
|
|
123
|
+
const onScroll = () => updateTableOverflow();
|
|
124
|
+
el.addEventListener("scroll", onScroll, { passive: true });
|
|
125
|
+
const observer = new ResizeObserver(updateTableOverflow);
|
|
126
|
+
observer.observe(el);
|
|
127
|
+
return () => {
|
|
128
|
+
el.removeEventListener("scroll", onScroll);
|
|
129
|
+
observer.disconnect();
|
|
130
|
+
};
|
|
131
|
+
}, [updateTableOverflow, items.length, columnVisibility]);
|
|
132
|
+
const scrollTable = (direction) => {
|
|
133
|
+
const el = tableWrapRef.current;
|
|
134
|
+
if (!el)
|
|
135
|
+
return;
|
|
136
|
+
const amount = Math.max(220, Math.round(el.clientWidth * 0.55));
|
|
137
|
+
el.scrollBy({
|
|
138
|
+
left: direction === "left" ? -amount : amount,
|
|
139
|
+
behavior: "smooth",
|
|
140
|
+
});
|
|
141
|
+
};
|
|
85
142
|
const columnOptions = useMemo(() => fieldKeys.map((key) => ({
|
|
86
143
|
id: key,
|
|
87
144
|
label: schema[key]?.label ?? humanizeKey(key),
|
|
@@ -148,15 +205,32 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, upload
|
|
|
148
205
|
getCoreRowModel: getCoreRowModel(),
|
|
149
206
|
});
|
|
150
207
|
const editingId = editing ? resolveRowId(editing, rowIdKeys) : null;
|
|
208
|
+
const createSchema = Object.fromEntries(Object.entries(schema)
|
|
209
|
+
.filter(([, field]) => !field.readOnly || field.required)
|
|
210
|
+
.map(([key, field]) => [
|
|
211
|
+
key,
|
|
212
|
+
field.readOnly ? { ...field, readOnly: false } : field,
|
|
213
|
+
]));
|
|
214
|
+
const createDefaults = Object.fromEntries(Object.entries(createSchema).map(([key, field]) => [
|
|
215
|
+
key,
|
|
216
|
+
field.default ??
|
|
217
|
+
(field.type === "boolean"
|
|
218
|
+
? false
|
|
219
|
+
: field.type === "number"
|
|
220
|
+
? undefined
|
|
221
|
+
: field.type === "file[]"
|
|
222
|
+
? []
|
|
223
|
+
: ""),
|
|
224
|
+
]));
|
|
151
225
|
if (editing && editingId && onUpdate) {
|
|
152
226
|
const editableSchema = Object.fromEntries(Object.entries(schema).filter(([, field]) => !field.readOnly));
|
|
153
227
|
const title = rowTitle(editing, rowIdKeys);
|
|
154
|
-
return (_jsxs("div", { className: "vertebr-resource-edit", children: [_jsxs("div", { className: "vertebr-resource-edit-head", children: [_jsxs("div", { className: "vertebr-resource-edit-heading", children: [_jsx("p", { className: "vertebr-resource-edit-eyebrow", children: "Editing record" }), _jsx("h2", { children: title }), _jsx("p", { className: "vertebr-resource-edit-id", children: editingId })] }), _jsx("button", { type: "button", className: "vertebr-btn", onClick: () => setEditing(null), children: "Back" })] }), _jsx(ConfigForm, { variant: "record", schema: editableSchema, value: editing, uploads: uploads, onUpload: onUpload, onSave: async (next) => {
|
|
228
|
+
return (_jsxs("div", { className: "vertebr-resource-edit", children: [_jsxs("div", { className: "vertebr-resource-edit-head", children: [_jsxs("div", { className: "vertebr-resource-edit-heading", children: [_jsx("p", { className: "vertebr-resource-edit-eyebrow", children: "Editing record" }), _jsx("h2", { children: title }), _jsx("p", { className: "vertebr-resource-edit-id", children: editingId })] }), _jsx("button", { type: "button", className: "vertebr-btn", onClick: () => setEditing(null), children: "Back" })] }), _jsx(ConfigForm, { variant: "record", schema: editableSchema, value: editing, uploads: uploads, onUpload: onUpload, galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onSave: async (next) => {
|
|
155
229
|
await onUpdate(editingId, next);
|
|
156
230
|
setEditing(null);
|
|
157
231
|
} })] }));
|
|
158
232
|
}
|
|
159
|
-
const toolbar = (
|
|
233
|
+
const toolbar = (_jsxs("div", { className: "vertebr-table-toolbar", children: [_jsxs("div", { className: "vertebr-table-toolbar-start", children: [columnOptions.length > 0 ? (_jsx(ColumnVisibilityMenu, { columns: columnOptions, visibility: columnVisibility, onChange: setColumnVisibility })) : null, canUpload ? (_jsxs("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-icon", onClick: () => setBulkOpen(true), children: [_jsx(ImageUploadIcon, {}), "Bulk upload"] })) : null] }), tableOverflow.overflow ? (_jsxs("div", { className: "vertebr-table-scroll-controls", role: "group", "aria-label": "Scroll columns", children: [_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-icon vertebr-table-scroll-btn", disabled: !tableOverflow.canLeft, onClick: () => scrollTable("left"), "aria-label": "Scroll columns left", children: _jsx(ChevronLeftIcon, {}) }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-icon vertebr-table-scroll-btn", disabled: !tableOverflow.canRight, onClick: () => scrollTable("right"), "aria-label": "Scroll columns right", children: _jsx(ChevronRightIcon, {}) })] })) : null] }));
|
|
160
234
|
const bulkModal = bulkOpen && uploads && onUpload ? (_jsx(BulkImageUploadModal, { uploads: uploads, canCreate: canCreate, fieldLabels: Object.fromEntries(Object.keys(uploads).map((key) => [
|
|
161
235
|
key,
|
|
162
236
|
schema[key]?.label ?? humanizeKey(key),
|
|
@@ -172,10 +246,12 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, upload
|
|
|
172
246
|
}
|
|
173
247
|
}
|
|
174
248
|
: undefined })) : null;
|
|
249
|
+
const createModal = creating && onCreate ? (_jsx(ResourceCreateModal, { title: createLabel, schema: createSchema, value: createDefaults, uploads: uploads, onUpload: onUpload, galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onClose: () => setCreating(false), onCreate: onCreate })) : null;
|
|
250
|
+
const recordsHead = (_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: "Records" }), _jsxs("div", { className: "vertebr-panel-head-actions", children: [canCreate ? (_jsxs("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-icon", onClick: () => setCreating(true), children: [_jsx(PlusIcon, {}), createLabel] })) : null, _jsx("span", { className: "vertebr-count", children: items.length })] })] }));
|
|
175
251
|
if (items.length === 0) {
|
|
176
|
-
return (_jsxs("div", { className: "vertebr-resource-body", children: [
|
|
252
|
+
return (_jsxs("div", { className: "vertebr-resource-body", children: [recordsHead, canUpload || columnOptions.length > 0 ? toolbar : null, _jsx("div", { className: "vertebr-empty", children: "No rows yet." }), bulkModal, createModal] }));
|
|
177
253
|
}
|
|
178
|
-
return (_jsxs("div", { className: "vertebr-resource-body", children: [
|
|
254
|
+
return (_jsxs("div", { className: "vertebr-resource-body", children: [recordsHead, toolbar, _jsx("div", { className: "vertebr-table-wrap", ref: tableWrapRef, "data-overflow": tableOverflow.overflow ? "true" : undefined, "data-overflow-left": tableOverflow.canLeft ? "true" : undefined, "data-overflow-right": tableOverflow.canRight ? "true" : undefined, children: _jsxs("table", { className: "vertebr-table", children: [_jsx("thead", { children: table.getHeaderGroups().map((headerGroup) => (_jsx("tr", { children: headerGroup.headers.map((header) => (_jsx("th", { className: header.column.id === "_actions"
|
|
179
255
|
? "vertebr-col-actions"
|
|
180
256
|
: undefined, children: header.isPlaceholder
|
|
181
257
|
? null
|
|
@@ -189,12 +265,21 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, upload
|
|
|
189
265
|
: undefined, children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))) }, row.id))) })] }) }), viewing ? (_jsx(ResourceViewModal, { schema: schema, row: viewing, title: rowTitle(viewing, rowIdKeys), canEdit: canUpdate, onClose: () => setViewing(null), onEdit: () => {
|
|
190
266
|
setEditing(viewing);
|
|
191
267
|
setViewing(null);
|
|
192
|
-
} })) : null, bulkModal] }));
|
|
268
|
+
} })) : null, bulkModal, createModal] }));
|
|
193
269
|
}
|
|
194
270
|
/** @deprecated Prefer ResourcePanel */
|
|
195
271
|
export function ResourceTable(props) {
|
|
196
272
|
return _jsx(ResourcePanel, { ...props });
|
|
197
273
|
}
|
|
274
|
+
function PlusIcon() {
|
|
275
|
+
return (_jsxs("svg", { className: "vertebr-btn-glyph", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M12 5v14" }), _jsx("path", { d: "M5 12h14" })] }));
|
|
276
|
+
}
|
|
277
|
+
function ChevronLeftIcon() {
|
|
278
|
+
return (_jsx("svg", { className: "vertebr-btn-glyph", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "m15 18-6-6 6-6" }) }));
|
|
279
|
+
}
|
|
280
|
+
function ChevronRightIcon() {
|
|
281
|
+
return (_jsx("svg", { className: "vertebr-btn-glyph", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "m9 18 6-6-6-6" }) }));
|
|
282
|
+
}
|
|
198
283
|
function ImageUploadIcon() {
|
|
199
284
|
return (_jsxs("svg", { className: "vertebr-btn-glyph", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M16 5h6" }), _jsx("path", { d: "M19 2v6" }), _jsx("rect", { x: "3", y: "7", width: "14", height: "14", rx: "2" }), _jsx("circle", { cx: "8.5", cy: "12.5", r: "1.5" }), _jsx("path", { d: "m17 21-4.5-4.5L7 21" })] }));
|
|
200
285
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useVertebrControls.d.ts","sourceRoot":"","sources":["../../src/hooks/useVertebrControls.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAAuB,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE/E,KAAK,oBAAoB,GAAG;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAIF,MAAM,MAAM,4BAA4B,GAAG;IACzC,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF,YAAY,EAAE,kBAAkB,EAAE,CAAC;AAEnC;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,OAAsB,EACtB,WAAW,EACX,QAAQ,GACT,EAAE,4BAA4B,+
|
|
1
|
+
{"version":3,"file":"useVertebrControls.d.ts","sourceRoot":"","sources":["../../src/hooks/useVertebrControls.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAAuB,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE/E,KAAK,oBAAoB,GAAG;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAIF,MAAM,MAAM,4BAA4B,GAAG;IACzC,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF,YAAY,EAAE,kBAAkB,EAAE,CAAC;AAEnC;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,OAAsB,EACtB,WAAW,EACX,QAAQ,GACT,EAAE,4BAA4B,+BAyC9B;AAYD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChE,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,CAAC,GACV;IACD,IAAI,EAAE,CAAC,CAAC;IACR,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAMA;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,IAAI,oBAAoB,CAEzD"}
|
|
@@ -13,11 +13,11 @@ export function VertebrControlsProvider({ apiBase = "/admin/api", environment, c
|
|
|
13
13
|
apiBase,
|
|
14
14
|
getEnvironment: () => environment,
|
|
15
15
|
}), [apiBase, environment]);
|
|
16
|
-
const
|
|
16
|
+
const load = useCallback(async (opts) => {
|
|
17
17
|
setLoading(true);
|
|
18
18
|
setError(null);
|
|
19
19
|
try {
|
|
20
|
-
const next = await client.getPublicControls();
|
|
20
|
+
const next = await client.getPublicControls(opts);
|
|
21
21
|
setControls(next);
|
|
22
22
|
}
|
|
23
23
|
catch (err) {
|
|
@@ -27,9 +27,10 @@ export function VertebrControlsProvider({ apiBase = "/admin/api", environment, c
|
|
|
27
27
|
setLoading(false);
|
|
28
28
|
}
|
|
29
29
|
}, [client]);
|
|
30
|
+
const refresh = useCallback(() => load({ fresh: true }), [load]);
|
|
30
31
|
useEffect(() => {
|
|
31
|
-
void
|
|
32
|
-
}, [
|
|
32
|
+
void load();
|
|
33
|
+
}, [load]);
|
|
33
34
|
const value = useMemo(() => ({ environment, controls, loading, error, refresh }), [environment, controls, loading, error, refresh]);
|
|
34
35
|
return (_jsx(ControlsContext.Provider, { value: value, children: children }));
|
|
35
36
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,11 +6,20 @@ export { ResourcePanel, ResourceTable } from "./components/resources/ResourceTab
|
|
|
6
6
|
export { ThemeToggle } from "./components/theme/ThemeToggle";
|
|
7
7
|
export type { ThemeToggleProps } from "./components/theme/ThemeToggle";
|
|
8
8
|
export { ConfigForm } from "./components/config/ConfigForm";
|
|
9
|
+
export { formatPrice, formatPriceInput, parsePriceInput, isCentsField, isPriceField, } from "./lib/price";
|
|
10
|
+
export { isDescriptionField, descriptionStats, applyMarkdown, } from "./lib/description";
|
|
11
|
+
export type { MarkdownCommand, MarkdownEdit } from "./lib/description";
|
|
9
12
|
export { EnvSelect } from "./components/env/EnvSelect";
|
|
10
13
|
export { useVertebrTheme } from "./hooks/useVertebrTheme";
|
|
11
14
|
export type { VbTheme } from "./hooks/useVertebrTheme";
|
|
12
15
|
export { VertebrControlsProvider, useVertebrConfig, useVertebrControls, } from "./hooks/useVertebrControls";
|
|
13
16
|
export type { VertebrControlsProviderProps } from "./hooks/useVertebrControls";
|
|
14
|
-
export { createVertebrClient, VERTEBR_ENV_HEADER, } from "@vertebr/client";
|
|
15
|
-
export
|
|
17
|
+
export { createVertebrClient, VERTEBR_ENV_HEADER, renderMarkdown, } from "@vertebr/client";
|
|
18
|
+
export { VertebrMarkdown } from "@vertebr/client/react";
|
|
19
|
+
export type { VertebrMarkdownProps } from "@vertebr/client/react";
|
|
20
|
+
export { createVertebrCatalog, catalogHref, catalogMatch, } from "@vertebr/client";
|
|
21
|
+
export type { VertebrCatalog, VertebrCatalogOptions, PublicResourceResponse, } from "@vertebr/client";
|
|
22
|
+
export { VertebrCatalogProvider, useVertebrCatalog, useVertebrCatalogItem, } from "@vertebr/client/react";
|
|
23
|
+
export type { VertebrCatalogProviderProps, VertebrCatalogState, } from "@vertebr/client/react";
|
|
24
|
+
export type { VertebrClient, VertebrClientOptions, VertebrReadOptions, AdminEnvironment, BootstrapResponse, CatalogResponse, CatalogResource, CatalogConfig, CatalogConfigSection, EnvironmentAuthPublic, FieldSchema, FieldType, GalleryItem, GalleryMeta, GalleryResponse, MetricCard, MetricsResponse, ResourceListResponse, ResourceActionMeta, ResourceUploadMeta, ResourceUploadResponse, ResourceBulkUploadResponse, ConfigResponse, VertebrControlsMap, } from "@vertebr/client";
|
|
16
25
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAC/D,YAAY,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AACpF,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAC7D,YAAY,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAG/E,OAAO,EACL,mBAAmB,EACnB,kBAAkB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAC/D,YAAY,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AACpF,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAC7D,YAAY,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAC5D,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,YAAY,GACb,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,GACd,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAG/E,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,YAAY,GACb,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,2BAA2B,EAC3B,mBAAmB,GACpB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,aAAa,EACb,oBAAoB,EACpB,qBAAqB,EACrB,WAAW,EACX,SAAS,EACT,WAAW,EACX,WAAW,EACX,eAAe,EACf,UAAU,EACV,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,sBAAsB,EACtB,0BAA0B,EAC1B,cAAc,EACd,kBAAkB,GACnB,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,13 @@ export { GalleryBoard } from "./components/gallery/GalleryBoard";
|
|
|
4
4
|
export { ResourcePanel, ResourceTable } from "./components/resources/ResourceTable";
|
|
5
5
|
export { ThemeToggle } from "./components/theme/ThemeToggle";
|
|
6
6
|
export { ConfigForm } from "./components/config/ConfigForm";
|
|
7
|
+
export { formatPrice, formatPriceInput, parsePriceInput, isCentsField, isPriceField, } from "./lib/price";
|
|
8
|
+
export { isDescriptionField, descriptionStats, applyMarkdown, } from "./lib/description";
|
|
7
9
|
export { EnvSelect } from "./components/env/EnvSelect";
|
|
8
10
|
export { useVertebrTheme } from "./hooks/useVertebrTheme";
|
|
9
11
|
export { VertebrControlsProvider, useVertebrConfig, useVertebrControls, } from "./hooks/useVertebrControls";
|
|
10
12
|
// Re-export the framework-agnostic client for convenience.
|
|
11
|
-
export { createVertebrClient, VERTEBR_ENV_HEADER, } from "@vertebr/client";
|
|
13
|
+
export { createVertebrClient, VERTEBR_ENV_HEADER, renderMarkdown, } from "@vertebr/client";
|
|
14
|
+
export { VertebrMarkdown } from "@vertebr/client/react";
|
|
15
|
+
export { createVertebrCatalog, catalogHref, catalogMatch, } from "@vertebr/client";
|
|
16
|
+
export { VertebrCatalogProvider, useVertebrCatalog, useVertebrCatalogItem, } from "@vertebr/client/react";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FieldSchema } from "@vertebr/client";
|
|
2
|
+
export declare function isDescriptionField(key: string, field?: Pick<FieldSchema, "label" | "type">): boolean;
|
|
3
|
+
export declare function descriptionStats(value: unknown): {
|
|
4
|
+
characters: number;
|
|
5
|
+
words: number;
|
|
6
|
+
};
|
|
7
|
+
export type MarkdownCommand = "bold" | "italic" | "heading" | "ul" | "ol" | "link";
|
|
8
|
+
export type MarkdownEdit = {
|
|
9
|
+
next: string;
|
|
10
|
+
inserted: string;
|
|
11
|
+
replaceStart: number;
|
|
12
|
+
replaceEnd: number;
|
|
13
|
+
selectionStart: number;
|
|
14
|
+
selectionEnd: number;
|
|
15
|
+
};
|
|
16
|
+
/** Apply a markdown format command around the current selection. */
|
|
17
|
+
export declare function applyMarkdown(text: string, start: number, end: number, command: MarkdownCommand): MarkdownEdit;
|
|
18
|
+
//# sourceMappingURL=description.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"description.d.ts","sourceRoot":"","sources":["../../src/lib/description.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAKnD,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,GAAG,MAAM,CAAC,GAC1C,OAAO,CAIT;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf,CAOA;AAED,MAAM,MAAM,eAAe,GACvB,MAAM,GACN,QAAQ,GACR,SAAS,GACT,IAAI,GACJ,IAAI,GACJ,MAAM,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AA8CF,oEAAoE;AACpE,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,eAAe,GACvB,YAAY,CAsCd"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const DESCRIPTION_KEY = /(description|body|content|summary|bio|copy|caption|details)/i;
|
|
2
|
+
export function isDescriptionField(key, field) {
|
|
3
|
+
if (field?.type === "richtext")
|
|
4
|
+
return true;
|
|
5
|
+
const hay = `${key} ${field?.label ?? ""}`;
|
|
6
|
+
return DESCRIPTION_KEY.test(hay);
|
|
7
|
+
}
|
|
8
|
+
export function descriptionStats(value) {
|
|
9
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
10
|
+
if (!text)
|
|
11
|
+
return { characters: 0, words: 0 };
|
|
12
|
+
return {
|
|
13
|
+
characters: text.length,
|
|
14
|
+
words: text.split(/\s+/).filter(Boolean).length,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function wrap(text, start, end, before, after, placeholder) {
|
|
18
|
+
const selected = text.slice(start, end) || placeholder;
|
|
19
|
+
const inserted = before + selected + after;
|
|
20
|
+
return {
|
|
21
|
+
next: text.slice(0, start) + inserted + text.slice(end),
|
|
22
|
+
inserted,
|
|
23
|
+
replaceStart: start,
|
|
24
|
+
replaceEnd: end,
|
|
25
|
+
selectionStart: start + before.length,
|
|
26
|
+
selectionEnd: start + before.length + selected.length,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function prefixLines(text, start, end, prefixFor) {
|
|
30
|
+
const lineStart = text.lastIndexOf("\n", Math.max(0, start - 1)) + 1;
|
|
31
|
+
const lineEndIndex = text.indexOf("\n", end);
|
|
32
|
+
const lineEnd = lineEndIndex === -1 ? text.length : lineEndIndex;
|
|
33
|
+
const block = text.slice(lineStart, lineEnd) || "";
|
|
34
|
+
const lines = block.split("\n");
|
|
35
|
+
const inserted = lines
|
|
36
|
+
.map((line, index) => prefixFor(index, line))
|
|
37
|
+
.join("\n");
|
|
38
|
+
return {
|
|
39
|
+
next: text.slice(0, lineStart) + inserted + text.slice(lineEnd),
|
|
40
|
+
inserted,
|
|
41
|
+
replaceStart: lineStart,
|
|
42
|
+
replaceEnd: lineEnd,
|
|
43
|
+
selectionStart: lineStart,
|
|
44
|
+
selectionEnd: lineStart + inserted.length,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Apply a markdown format command around the current selection. */
|
|
48
|
+
export function applyMarkdown(text, start, end, command) {
|
|
49
|
+
const from = Math.max(0, Math.min(start, end, text.length));
|
|
50
|
+
const to = Math.max(0, Math.min(Math.max(start, end), text.length));
|
|
51
|
+
switch (command) {
|
|
52
|
+
case "bold":
|
|
53
|
+
return wrap(text, from, to, "**", "**", "bold text");
|
|
54
|
+
case "italic":
|
|
55
|
+
return wrap(text, from, to, "*", "*", "italic text");
|
|
56
|
+
case "link": {
|
|
57
|
+
const selected = text.slice(from, to) || "link text";
|
|
58
|
+
const inserted = `[${selected}](https://)`;
|
|
59
|
+
const urlStart = from + selected.length + 3;
|
|
60
|
+
return {
|
|
61
|
+
next: text.slice(0, from) + inserted + text.slice(to),
|
|
62
|
+
inserted,
|
|
63
|
+
replaceStart: from,
|
|
64
|
+
replaceEnd: to,
|
|
65
|
+
selectionStart: urlStart,
|
|
66
|
+
selectionEnd: urlStart + "https://".length,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
case "heading":
|
|
70
|
+
return prefixLines(text, from, to, (_index, line) => {
|
|
71
|
+
const stripped = line.replace(/^#{1,6}\s+/, "");
|
|
72
|
+
return `## ${stripped || "Heading"}`;
|
|
73
|
+
});
|
|
74
|
+
case "ul":
|
|
75
|
+
return prefixLines(text, from, to, (_index, line) => {
|
|
76
|
+
const stripped = line.replace(/^[-*]\s+/, "").replace(/^\d+\.\s+/, "");
|
|
77
|
+
return `- ${stripped || "List item"}`;
|
|
78
|
+
});
|
|
79
|
+
case "ol":
|
|
80
|
+
return prefixLines(text, from, to, (index, line) => {
|
|
81
|
+
const stripped = line.replace(/^[-*]\s+/, "").replace(/^\d+\.\s+/, "");
|
|
82
|
+
return `${index + 1}. ${stripped || "List item"}`;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FieldSchema } from "@vertebr/client";
|
|
2
|
+
export type RecordImage = {
|
|
3
|
+
id: string;
|
|
4
|
+
url: string;
|
|
5
|
+
name: string;
|
|
6
|
+
field: string;
|
|
7
|
+
};
|
|
8
|
+
declare function looksLikeUrl(value: string): boolean;
|
|
9
|
+
export declare function isImageField(key: string, field?: Pick<FieldSchema, "label" | "type">): boolean;
|
|
10
|
+
/** Flatten record image fields (single URL or URL lists) for the editor picker. */
|
|
11
|
+
export declare function collectRecordImages(schema: Record<string, FieldSchema>, values: Record<string, unknown> | undefined): RecordImage[];
|
|
12
|
+
export declare function urlsFromValue(value: unknown): string[];
|
|
13
|
+
export { looksLikeUrl };
|
|
14
|
+
//# sourceMappingURL=images.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/lib/images.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,iBAAS,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE5C;AAED,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,GAAG,MAAM,CAAC,GAC1C,OAAO,CAIT;AAqBD,mFAAmF;AACnF,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC1C,WAAW,EAAE,CAmBf;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAYtD;AAED,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
function looksLikeUrl(value) {
|
|
2
|
+
return /^https?:\/\//i.test(value) || value.startsWith("//");
|
|
3
|
+
}
|
|
4
|
+
export function isImageField(key, field) {
|
|
5
|
+
if (field?.type === "file[]")
|
|
6
|
+
return true;
|
|
7
|
+
const hay = `${key} ${field?.label ?? ""}`;
|
|
8
|
+
return /(image|img|photo|avatar|thumbnail|thumb|cover)/i.test(hay);
|
|
9
|
+
}
|
|
10
|
+
function urlFromUnknown(value) {
|
|
11
|
+
if (typeof value === "string" && looksLikeUrl(value)) {
|
|
12
|
+
return { url: value };
|
|
13
|
+
}
|
|
14
|
+
if (value && typeof value === "object" && "url" in value) {
|
|
15
|
+
const url = value.url;
|
|
16
|
+
const name = value.name;
|
|
17
|
+
if (typeof url === "string" && looksLikeUrl(url)) {
|
|
18
|
+
return {
|
|
19
|
+
url,
|
|
20
|
+
name: typeof name === "string" ? name : undefined,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
/** Flatten record image fields (single URL or URL lists) for the editor picker. */
|
|
27
|
+
export function collectRecordImages(schema, values) {
|
|
28
|
+
const images = [];
|
|
29
|
+
for (const [key, field] of Object.entries(schema)) {
|
|
30
|
+
if (!isImageField(key, field))
|
|
31
|
+
continue;
|
|
32
|
+
const label = field.label?.trim() || key;
|
|
33
|
+
const value = values?.[key];
|
|
34
|
+
const items = Array.isArray(value) ? value : [value];
|
|
35
|
+
items.forEach((item, index) => {
|
|
36
|
+
const parsed = urlFromUnknown(item);
|
|
37
|
+
if (!parsed)
|
|
38
|
+
return;
|
|
39
|
+
images.push({
|
|
40
|
+
id: `${key}:${index}`,
|
|
41
|
+
url: parsed.url,
|
|
42
|
+
name: parsed.name || (items.length > 1 ? `${label} ${index + 1}` : label),
|
|
43
|
+
field: key,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return images;
|
|
48
|
+
}
|
|
49
|
+
export function urlsFromValue(value) {
|
|
50
|
+
const items = Array.isArray(value)
|
|
51
|
+
? value
|
|
52
|
+
: value == null || value === ""
|
|
53
|
+
? []
|
|
54
|
+
: [value];
|
|
55
|
+
const urls = [];
|
|
56
|
+
for (const item of items) {
|
|
57
|
+
const parsed = urlFromUnknown(item);
|
|
58
|
+
if (parsed)
|
|
59
|
+
urls.push(parsed.url);
|
|
60
|
+
}
|
|
61
|
+
return urls;
|
|
62
|
+
}
|
|
63
|
+
export { looksLikeUrl };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FieldSchema } from "@vertebr/client";
|
|
2
|
+
export declare function isCentsField(key: string, field?: Pick<FieldSchema, "label">): boolean;
|
|
3
|
+
export declare function isPriceField(key: string, field?: Pick<FieldSchema, "label">): boolean;
|
|
4
|
+
export declare function dollarsFromStored(value: number, asCents: boolean): number;
|
|
5
|
+
export declare function storedFromDollars(dollars: number, asCents: boolean): number;
|
|
6
|
+
/** Format a stored number for display (`3800` cents → `$38.00`). */
|
|
7
|
+
export declare function formatPrice(value: number, options?: {
|
|
8
|
+
asCents?: boolean;
|
|
9
|
+
currency?: string;
|
|
10
|
+
}): string;
|
|
11
|
+
/** Value shown in an editable money input (`3800` → `38.00`). */
|
|
12
|
+
export declare function formatPriceInput(value: unknown, asCents?: boolean): string;
|
|
13
|
+
/** Parse typed money (`$38`, `38.00`, `38`) back to stored units. */
|
|
14
|
+
export declare function parsePriceInput(raw: string, asCents?: boolean): number | undefined;
|
|
15
|
+
//# sourceMappingURL=price.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"price.d.ts","sourceRoot":"","sources":["../../src/lib/price.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAKnD,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,GACjC,OAAO,CAET;AAED,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,GACjC,OAAO,CAGT;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAEzE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAE3E;AAED,oEAAoE;AACpE,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GACjD,MAAM,CAcR;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,OAAO,EACd,OAAO,UAAO,GACb,MAAM,CAGR;AAED,qEAAqE;AACrE,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,OAAO,UAAO,GACb,MAAM,GAAG,SAAS,CAQpB"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const CENTS_KEY = /(cents|priceCents|amountCents)/i;
|
|
2
|
+
const PRICE_KEY = /(price|amount|cost|total|msrp)/i;
|
|
3
|
+
export function isCentsField(key, field) {
|
|
4
|
+
return CENTS_KEY.test(key) || /cents/i.test(field?.label ?? "");
|
|
5
|
+
}
|
|
6
|
+
export function isPriceField(key, field) {
|
|
7
|
+
if (isCentsField(key, field))
|
|
8
|
+
return true;
|
|
9
|
+
return PRICE_KEY.test(key) || PRICE_KEY.test(field?.label ?? "");
|
|
10
|
+
}
|
|
11
|
+
export function dollarsFromStored(value, asCents) {
|
|
12
|
+
return asCents ? value / 100 : value;
|
|
13
|
+
}
|
|
14
|
+
export function storedFromDollars(dollars, asCents) {
|
|
15
|
+
return asCents ? Math.round(dollars * 100) : dollars;
|
|
16
|
+
}
|
|
17
|
+
/** Format a stored number for display (`3800` cents → `$38.00`). */
|
|
18
|
+
export function formatPrice(value, options) {
|
|
19
|
+
const asCents = options?.asCents ?? true;
|
|
20
|
+
const currency = options?.currency ?? "USD";
|
|
21
|
+
const amount = dollarsFromStored(value, asCents);
|
|
22
|
+
try {
|
|
23
|
+
return new Intl.NumberFormat(undefined, {
|
|
24
|
+
style: "currency",
|
|
25
|
+
currency,
|
|
26
|
+
minimumFractionDigits: 2,
|
|
27
|
+
maximumFractionDigits: 2,
|
|
28
|
+
}).format(amount);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return `$${amount.toFixed(2)}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Value shown in an editable money input (`3800` → `38.00`). */
|
|
35
|
+
export function formatPriceInput(value, asCents = true) {
|
|
36
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
37
|
+
return "";
|
|
38
|
+
return dollarsFromStored(value, asCents).toFixed(2);
|
|
39
|
+
}
|
|
40
|
+
/** Parse typed money (`$38`, `38.00`, `38`) back to stored units. */
|
|
41
|
+
export function parsePriceInput(raw, asCents = true) {
|
|
42
|
+
const cleaned = raw.replace(/[^0-9.-]/g, "");
|
|
43
|
+
if (!cleaned || cleaned === "-" || cleaned === "." || cleaned === "-.") {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const dollars = Number(cleaned);
|
|
47
|
+
if (!Number.isFinite(dollars))
|
|
48
|
+
return undefined;
|
|
49
|
+
return storedFromDollars(dollars, asCents);
|
|
50
|
+
}
|