@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.
Files changed (89) hide show
  1. package/dist/components/admin/VertebrAdmin.d.ts.map +1 -1
  2. package/dist/components/admin/VertebrAdmin.js +26 -4
  3. package/dist/components/config/ConfigForm.d.ts +2 -18
  4. package/dist/components/config/ConfigForm.d.ts.map +1 -1
  5. package/dist/components/config/ConfigForm.js +1 -281
  6. package/dist/components/config/DescriptionInput.d.ts +18 -0
  7. package/dist/components/config/DescriptionInput.d.ts.map +1 -0
  8. package/dist/components/config/DescriptionInput.js +152 -0
  9. package/dist/components/config/EnumInput.d.ts +10 -0
  10. package/dist/components/config/EnumInput.d.ts.map +1 -0
  11. package/dist/components/config/EnumInput.js +19 -0
  12. package/dist/components/config/EnumListInput.d.ts +10 -0
  13. package/dist/components/config/EnumListInput.d.ts.map +1 -0
  14. package/dist/components/config/EnumListInput.js +46 -0
  15. package/dist/components/config/ImagePickerModal.d.ts +18 -0
  16. package/dist/components/config/ImagePickerModal.d.ts.map +1 -0
  17. package/dist/components/config/ImagePickerModal.js +120 -0
  18. package/dist/components/config/PriceInput.d.ts +12 -0
  19. package/dist/components/config/PriceInput.d.ts.map +1 -0
  20. package/dist/components/config/PriceInput.js +20 -0
  21. package/dist/components/config/VisibilityEye.d.ts +8 -0
  22. package/dist/components/config/VisibilityEye.d.ts.map +1 -0
  23. package/dist/components/config/VisibilityEye.js +18 -0
  24. package/dist/components/config/form/ConfigField.d.ts +22 -0
  25. package/dist/components/config/form/ConfigField.d.ts.map +1 -0
  26. package/dist/components/config/form/ConfigField.js +53 -0
  27. package/dist/components/config/form/ConfigForm.d.ts +4 -0
  28. package/dist/components/config/form/ConfigForm.d.ts.map +1 -0
  29. package/dist/components/config/form/ConfigForm.js +75 -0
  30. package/dist/components/config/form/ConfigFormActions.d.ts +11 -0
  31. package/dist/components/config/form/ConfigFormActions.d.ts.map +1 -0
  32. package/dist/components/config/form/ConfigFormActions.js +4 -0
  33. package/dist/components/config/form/RecordConfigForm.d.ts +22 -0
  34. package/dist/components/config/form/RecordConfigForm.d.ts.map +1 -0
  35. package/dist/components/config/form/RecordConfigForm.js +45 -0
  36. package/dist/components/config/form/SettingsConfigForm.d.ts +22 -0
  37. package/dist/components/config/form/SettingsConfigForm.d.ts.map +1 -0
  38. package/dist/components/config/form/SettingsConfigForm.js +7 -0
  39. package/dist/components/config/form/helpers/fields.d.ts +7 -0
  40. package/dist/components/config/form/helpers/fields.d.ts.map +1 -0
  41. package/dist/components/config/form/helpers/fields.js +31 -0
  42. package/dist/components/config/form/helpers/groups.d.ts +4 -0
  43. package/dist/components/config/form/helpers/groups.d.ts.map +1 -0
  44. package/dist/components/config/form/helpers/groups.js +107 -0
  45. package/dist/components/config/form/helpers/images.d.ts +4 -0
  46. package/dist/components/config/form/helpers/images.d.ts.map +1 -0
  47. package/dist/components/config/form/helpers/images.js +27 -0
  48. package/dist/components/config/form/helpers/index.d.ts +7 -0
  49. package/dist/components/config/form/helpers/index.d.ts.map +1 -0
  50. package/dist/components/config/form/helpers/index.js +6 -0
  51. package/dist/components/config/form/helpers/labels.d.ts +5 -0
  52. package/dist/components/config/form/helpers/labels.d.ts.map +1 -0
  53. package/dist/components/config/form/helpers/labels.js +28 -0
  54. package/dist/components/config/form/helpers/schema.d.ts +8 -0
  55. package/dist/components/config/form/helpers/schema.d.ts.map +1 -0
  56. package/dist/components/config/form/helpers/schema.js +30 -0
  57. package/dist/components/config/form/helpers/visibility.d.ts +13 -0
  58. package/dist/components/config/form/helpers/visibility.d.ts.map +1 -0
  59. package/dist/components/config/form/helpers/visibility.js +86 -0
  60. package/dist/components/config/form/types.d.ts +29 -0
  61. package/dist/components/config/form/types.d.ts.map +1 -0
  62. package/dist/components/config/form/types.js +1 -0
  63. package/dist/components/resources/ResourceCell.d.ts.map +1 -1
  64. package/dist/components/resources/ResourceCell.js +5 -19
  65. package/dist/components/resources/ResourceCreateModal.d.ts +18 -0
  66. package/dist/components/resources/ResourceCreateModal.d.ts.map +1 -0
  67. package/dist/components/resources/ResourceCreateModal.js +22 -0
  68. package/dist/components/resources/ResourceTable.d.ts +7 -2
  69. package/dist/components/resources/ResourceTable.d.ts.map +1 -1
  70. package/dist/components/resources/ResourceTable.js +92 -7
  71. package/dist/hooks/useVertebrControls.d.ts.map +1 -1
  72. package/dist/hooks/useVertebrControls.js +5 -4
  73. package/dist/index.d.ts +11 -2
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +6 -1
  76. package/dist/lib/description.d.ts +18 -0
  77. package/dist/lib/description.d.ts.map +1 -0
  78. package/dist/lib/description.js +85 -0
  79. package/dist/lib/images.d.ts +14 -0
  80. package/dist/lib/images.d.ts.map +1 -0
  81. package/dist/lib/images.js +63 -0
  82. package/dist/lib/price.d.ts +15 -0
  83. package/dist/lib/price.d.ts.map +1 -0
  84. package/dist/lib/price.js +50 -0
  85. package/dist/styles/form.css +420 -0
  86. package/dist/styles/modal.css +133 -0
  87. package/dist/styles/panel.css +18 -1
  88. package/dist/styles/table.css +10 -1
  89. 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 = (_jsx("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] }) }));
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: [_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: "Records" }), _jsx("span", { className: "vertebr-count", children: "0" })] }), canUpload || columnOptions.length > 0 ? toolbar : null, _jsx("div", { className: "vertebr-empty", children: "No rows yet." }), bulkModal] }));
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: [_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: "Records" }), _jsx("span", { className: "vertebr-count", children: items.length })] }), canUpload || columnOptions.length > 0 ? toolbar : null, _jsx("div", { className: "vertebr-table-wrap", 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"
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,+BAuC9B;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"}
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 refresh = useCallback(async () => {
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 refresh();
32
- }, [refresh]);
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 type { VertebrClient, VertebrClientOptions, AdminEnvironment, BootstrapResponse, CatalogResponse, CatalogResource, CatalogConfig, CatalogConfigSection, EnvironmentAuthPublic, FieldSchema, FieldType, GalleryItem, GalleryMeta, GalleryResponse, MetricCard, MetricsResponse, ResourceListResponse, ResourceActionMeta, ResourceUploadMeta, ResourceUploadResponse, ResourceBulkUploadResponse, ConfigResponse, VertebrControlsMap, } from "@vertebr/client";
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
@@ -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,GACnB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,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"}
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
+ }