@underverse-ui/underverse 1.0.22 → 1.0.24

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/index.cjs CHANGED
@@ -166,6 +166,7 @@ __export(index_exports, {
166
166
  Tooltip: () => Tooltip,
167
167
  TranslationProvider: () => TranslationProvider,
168
168
  UEditor: () => UEditor_default,
169
+ UEditorPrepareContentForSaveError: () => UEditorPrepareContentForSaveError,
169
170
  UnderverseProvider: () => UnderverseProvider,
170
171
  VARIANT_STYLES_ALERT: () => VARIANT_STYLES_ALERT,
171
172
  VARIANT_STYLES_BTN: () => VARIANT_STYLES_BTN,
@@ -176,6 +177,7 @@ __export(index_exports, {
176
177
  getAnimationStyles: () => getAnimationStyles,
177
178
  getUnderverseMessages: () => getUnderverseMessages,
178
179
  injectAnimationStyles: () => injectAnimationStyles,
180
+ prepareUEditorContentForSave: () => prepareUEditorContentForSave,
179
181
  shadcnAnimationStyles: () => shadcnAnimationStyles2,
180
182
  underverseMessages: () => underverseMessages,
181
183
  useFormField: () => useFormField,
@@ -19430,6 +19432,14 @@ function useStickyColumns(columns, visibleKeys) {
19430
19432
  }
19431
19433
  return positions;
19432
19434
  }, [leafColumns]);
19435
+ const { leftBoundaryKey, rightBoundaryKey } = import_react36.default.useMemo(() => {
19436
+ const leftFixed = leafColumns.filter((c) => c.fixed === "left");
19437
+ const rightFixed = leafColumns.filter((c) => c.fixed === "right");
19438
+ return {
19439
+ leftBoundaryKey: leftFixed.length > 0 ? leftFixed[leftFixed.length - 1].key : null,
19440
+ rightBoundaryKey: rightFixed.length > 0 ? rightFixed[0].key : null
19441
+ };
19442
+ }, [leafColumns]);
19433
19443
  const getStickyColumnStyle = import_react36.default.useCallback(
19434
19444
  (col) => {
19435
19445
  if (!col.fixed) return {};
@@ -19441,24 +19451,38 @@ function useStickyColumns(columns, visibleKeys) {
19441
19451
  },
19442
19452
  [stickyPositions]
19443
19453
  );
19444
- const getStickyHeaderClass = import_react36.default.useCallback((col) => {
19445
- if (!col.fixed) return "";
19446
- return cn(
19447
- "sticky",
19448
- col.fixed === "left" && "left-0 shadow-[2px_0_6px_-3px_rgba(0,0,0,0.08)]",
19449
- col.fixed === "right" && "right-0 shadow-[-2px_0_6px_-3px_rgba(0,0,0,0.08)]",
19450
- "z-50 bg-muted!"
19451
- );
19452
- }, []);
19453
- const getStickyCellClass = import_react36.default.useCallback((col, isStripedRow) => {
19454
- if (!col.fixed) return "";
19455
- return cn(
19456
- "sticky z-10",
19457
- col.fixed === "left" && "left-0 shadow-[2px_0_6px_-3px_rgba(0,0,0,0.08)]",
19458
- col.fixed === "right" && "right-0 shadow-[-2px_0_6px_-3px_rgba(0,0,0,0.08)]",
19459
- isStripedRow ? "bg-(--surface-1)!" : "bg-(--surface-0)!"
19460
- );
19461
- }, []);
19454
+ const getBoundaryShadowClass = import_react36.default.useCallback(
19455
+ (col) => {
19456
+ if (col.fixed === "left" && col.key === leftBoundaryKey) {
19457
+ return "border-r border-border/80 shadow-[10px_0_16px_-10px_rgba(0,0,0,0.55)]";
19458
+ }
19459
+ if (col.fixed === "right" && col.key === rightBoundaryKey) {
19460
+ return "border-l border-border/80 shadow-[-10px_0_16px_-10px_rgba(0,0,0,0.55)]";
19461
+ }
19462
+ return "";
19463
+ },
19464
+ [leftBoundaryKey, rightBoundaryKey]
19465
+ );
19466
+ const getStickyHeaderClass = import_react36.default.useCallback(
19467
+ (col) => {
19468
+ if (!col.fixed) return "";
19469
+ return cn("sticky", col.fixed === "left" && "left-0", col.fixed === "right" && "right-0", getBoundaryShadowClass(col), "z-50 !bg-muted");
19470
+ },
19471
+ [getBoundaryShadowClass]
19472
+ );
19473
+ const getStickyCellClass = import_react36.default.useCallback(
19474
+ (col, isStripedRow) => {
19475
+ if (!col.fixed) return "";
19476
+ return cn(
19477
+ "sticky z-10",
19478
+ col.fixed === "left" && "left-0",
19479
+ col.fixed === "right" && "right-0",
19480
+ getBoundaryShadowClass(col),
19481
+ isStripedRow ? "!bg-surface-1" : "!bg-surface-0"
19482
+ );
19483
+ },
19484
+ [getBoundaryShadowClass]
19485
+ );
19462
19486
  const getStickyHeaderCellStyle = import_react36.default.useCallback(
19463
19487
  (headerCell) => {
19464
19488
  const col = headerCell.column;
@@ -21439,7 +21463,7 @@ function useSmartLocale() {
21439
21463
  }
21440
21464
 
21441
21465
  // ../../components/ui/UEditor/UEditor.tsx
21442
- var import_react51 = require("react");
21466
+ var import_react51 = __toESM(require("react"), 1);
21443
21467
  var import_next_intl6 = require("next-intl");
21444
21468
  var import_react52 = require("@tiptap/react");
21445
21469
 
@@ -24333,14 +24357,207 @@ var CharacterCountDisplay = ({ editor, maxCharacters }) => {
24333
24357
  ] });
24334
24358
  };
24335
24359
 
24360
+ // ../../components/ui/UEditor/prepare-content-for-save.ts
24361
+ var MIME_EXTENSION_MAP = {
24362
+ "image/png": "png",
24363
+ "image/jpeg": "jpg",
24364
+ "image/webp": "webp",
24365
+ "image/gif": "gif",
24366
+ "image/svg+xml": "svg",
24367
+ "image/bmp": "bmp",
24368
+ "image/x-icon": "ico",
24369
+ "image/avif": "avif"
24370
+ };
24371
+ function isDataImageUrl(value) {
24372
+ return /^data:image\//i.test(value.trim());
24373
+ }
24374
+ function parseDataImageUrl(dataUrl) {
24375
+ const value = dataUrl.trim();
24376
+ if (!isDataImageUrl(value)) return null;
24377
+ const commaIndex = value.indexOf(",");
24378
+ if (commaIndex < 0) return null;
24379
+ const header = value.slice(5, commaIndex);
24380
+ const base64Data = value.slice(commaIndex + 1).trim();
24381
+ if (!/;base64/i.test(header)) return null;
24382
+ const mime = header.split(";")[0]?.trim().toLowerCase();
24383
+ if (!mime || !base64Data) return null;
24384
+ return { mime, base64Data };
24385
+ }
24386
+ function decodeBase64ToBytes(base64Data) {
24387
+ const normalized = base64Data.replace(/\s+/g, "");
24388
+ const binary = atob(normalized);
24389
+ const bytes = new Uint8Array(binary.length);
24390
+ for (let i = 0; i < binary.length; i += 1) {
24391
+ bytes[i] = binary.charCodeAt(i);
24392
+ }
24393
+ return bytes;
24394
+ }
24395
+ function inferFileExtension(mime) {
24396
+ return MIME_EXTENSION_MAP[mime] ?? "bin";
24397
+ }
24398
+ function createFileFromDataImageUrl(dataUrl, index) {
24399
+ const parsed = parseDataImageUrl(dataUrl);
24400
+ if (!parsed) {
24401
+ throw new Error("Invalid data image URL format.");
24402
+ }
24403
+ const bytes = decodeBase64ToBytes(parsed.base64Data);
24404
+ const extension = inferFileExtension(parsed.mime);
24405
+ const name = `ueditor-image-${index + 1}.${extension}`;
24406
+ return new File([bytes], name, { type: parsed.mime });
24407
+ }
24408
+ function normalizeUploadResult(result) {
24409
+ if (typeof result === "string") {
24410
+ const url2 = result.trim();
24411
+ if (!url2) throw new Error("Upload handler returned an empty URL.");
24412
+ return { url: url2 };
24413
+ }
24414
+ if (!result || typeof result !== "object") {
24415
+ throw new Error("Upload handler returned invalid result.");
24416
+ }
24417
+ const url = typeof result.url === "string" ? result.url.trim() : "";
24418
+ if (!url) throw new Error("Upload handler object result is missing `url`.");
24419
+ const { url: _ignoredUrl, ...rest } = result;
24420
+ const meta = Object.keys(rest).length > 0 ? rest : void 0;
24421
+ return { url, meta };
24422
+ }
24423
+ function getErrorReason(error) {
24424
+ if (error instanceof Error && error.message) return error.message;
24425
+ if (typeof error === "string" && error.trim()) return error;
24426
+ return "Unknown upload error.";
24427
+ }
24428
+ function replaceSrcInTag(match, nextSrc) {
24429
+ if (!match.srcAttr) return match.tag;
24430
+ const { start, end, quote } = match.srcAttr;
24431
+ const escaped = quote === '"' ? nextSrc.replace(/"/g, "&quot;") : quote === "'" ? nextSrc.replace(/'/g, "&#39;") : nextSrc;
24432
+ const srcAttr = quote ? `src=${quote}${escaped}${quote}` : `src=${escaped}`;
24433
+ return `${match.tag.slice(0, start)}${srcAttr}${match.tag.slice(end)}`;
24434
+ }
24435
+ function collectImgTagMatches(html) {
24436
+ const matches = [];
24437
+ const imgRegex = /<img\b[^>]*>/gi;
24438
+ const srcAttrRegex = /\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/i;
24439
+ let tagMatch = imgRegex.exec(html);
24440
+ while (tagMatch) {
24441
+ const tag = tagMatch[0];
24442
+ const start = tagMatch.index;
24443
+ const end = start + tag.length;
24444
+ const srcMatch = srcAttrRegex.exec(tag);
24445
+ let srcAttr = null;
24446
+ if (srcMatch) {
24447
+ const value = srcMatch[1] ?? srcMatch[2] ?? srcMatch[3] ?? "";
24448
+ const quote = srcMatch[1] !== void 0 ? '"' : srcMatch[2] !== void 0 ? "'" : "";
24449
+ srcAttr = {
24450
+ start: srcMatch.index,
24451
+ end: srcMatch.index + srcMatch[0].length,
24452
+ value,
24453
+ quote
24454
+ };
24455
+ }
24456
+ matches.push({ start, end, tag, srcAttr });
24457
+ tagMatch = imgRegex.exec(html);
24458
+ }
24459
+ return matches;
24460
+ }
24461
+ var UEditorPrepareContentForSaveError = class extends Error {
24462
+ constructor(result) {
24463
+ super(
24464
+ `Failed to upload ${result.errors.length} image(s): ${result.errors.map((item) => `#${item.index} ${item.reason}`).join("; ")}`
24465
+ );
24466
+ this.name = "UEditorPrepareContentForSaveError";
24467
+ this.result = result;
24468
+ }
24469
+ };
24470
+ async function prepareUEditorContentForSave({
24471
+ html,
24472
+ uploadImageForSave
24473
+ }) {
24474
+ if (!html || !html.includes("<img")) {
24475
+ return { html, uploaded: [], errors: [] };
24476
+ }
24477
+ const imgMatches = collectImgTagMatches(html);
24478
+ if (imgMatches.length === 0) {
24479
+ return { html, uploaded: [], errors: [] };
24480
+ }
24481
+ const base64Candidates = [];
24482
+ for (const match of imgMatches) {
24483
+ if (!match.srcAttr) continue;
24484
+ const src = match.srcAttr.value.trim();
24485
+ if (!isDataImageUrl(src)) continue;
24486
+ base64Candidates.push({
24487
+ id: `${match.start}:${match.end}`,
24488
+ match,
24489
+ index: base64Candidates.length,
24490
+ src
24491
+ });
24492
+ }
24493
+ if (base64Candidates.length === 0) {
24494
+ return { html, uploaded: [], errors: [] };
24495
+ }
24496
+ if (!uploadImageForSave) {
24497
+ return {
24498
+ html,
24499
+ uploaded: [],
24500
+ errors: base64Candidates.map((item) => ({
24501
+ index: item.index,
24502
+ reason: "`uploadImageForSave` is required to transform base64 images before save."
24503
+ }))
24504
+ };
24505
+ }
24506
+ const uploaded = [];
24507
+ const errors = [];
24508
+ const replacements = /* @__PURE__ */ new Map();
24509
+ const uploadResults = await Promise.all(
24510
+ base64Candidates.map(async (candidate) => {
24511
+ try {
24512
+ const file = createFileFromDataImageUrl(candidate.src, candidate.index);
24513
+ const uploadResult = await uploadImageForSave(file);
24514
+ const normalized = normalizeUploadResult(uploadResult);
24515
+ return { candidate, file, ...normalized };
24516
+ } catch (error) {
24517
+ return { candidate, error: getErrorReason(error) };
24518
+ }
24519
+ })
24520
+ );
24521
+ for (const item of uploadResults) {
24522
+ if ("error" in item) {
24523
+ errors.push({
24524
+ index: item.candidate.index,
24525
+ reason: item.error ?? "Unknown upload error."
24526
+ });
24527
+ continue;
24528
+ }
24529
+ replacements.set(item.candidate.id, item.url);
24530
+ uploaded.push({
24531
+ url: item.url,
24532
+ file: item.file,
24533
+ meta: item.meta
24534
+ });
24535
+ }
24536
+ if (replacements.size === 0) {
24537
+ return { html, uploaded, errors };
24538
+ }
24539
+ let transformed = "";
24540
+ let cursor = 0;
24541
+ for (const match of imgMatches) {
24542
+ transformed += html.slice(cursor, match.start);
24543
+ const replacementKey = `${match.start}:${match.end}`;
24544
+ const replacementUrl = replacements.get(replacementKey);
24545
+ transformed += replacementUrl ? replaceSrcInTag(match, replacementUrl) : match.tag;
24546
+ cursor = match.end;
24547
+ }
24548
+ transformed += html.slice(cursor);
24549
+ return { html: transformed, uploaded, errors };
24550
+ }
24551
+
24336
24552
  // ../../components/ui/UEditor/UEditor.tsx
24337
24553
  var import_jsx_runtime86 = require("react/jsx-runtime");
24338
- var UEditor = ({
24554
+ var UEditor = import_react51.default.forwardRef(({
24339
24555
  content = "",
24340
24556
  onChange,
24341
24557
  onHtmlChange,
24342
24558
  onJsonChange,
24343
24559
  uploadImage,
24560
+ uploadImageForSave,
24344
24561
  imageInsertMode = "base64",
24345
24562
  placeholder,
24346
24563
  className,
@@ -24354,9 +24571,10 @@ var UEditor = ({
24354
24571
  minHeight = "200px",
24355
24572
  maxHeight = "auto",
24356
24573
  variant = "default"
24357
- }) => {
24574
+ }, ref) => {
24358
24575
  const t = (0, import_next_intl6.useTranslations)("UEditor");
24359
24576
  const effectivePlaceholder = placeholder ?? t("placeholder");
24577
+ const inFlightPrepareRef = (0, import_react51.useRef)(null);
24360
24578
  const extensions = (0, import_react51.useMemo)(
24361
24579
  () => buildUEditorExtensions({ placeholder: effectivePlaceholder, maxCharacters, uploadImage, imageInsertMode, editable }),
24362
24580
  [effectivePlaceholder, maxCharacters, uploadImage, imageInsertMode, editable]
@@ -24466,6 +24684,28 @@ var UEditor = ({
24466
24684
  onJsonChange?.(editor2.getJSON());
24467
24685
  }
24468
24686
  });
24687
+ (0, import_react51.useImperativeHandle)(
24688
+ ref,
24689
+ () => ({
24690
+ prepareContentForSave: async ({ throwOnError = false } = {}) => {
24691
+ if (!inFlightPrepareRef.current) {
24692
+ const htmlSnapshot = editor?.getHTML() ?? content ?? "";
24693
+ inFlightPrepareRef.current = prepareUEditorContentForSave({
24694
+ html: htmlSnapshot,
24695
+ uploadImageForSave
24696
+ }).finally(() => {
24697
+ inFlightPrepareRef.current = null;
24698
+ });
24699
+ }
24700
+ const result = await inFlightPrepareRef.current;
24701
+ if (throwOnError && result.errors.length > 0) {
24702
+ throw new UEditorPrepareContentForSaveError(result);
24703
+ }
24704
+ return result;
24705
+ }
24706
+ }),
24707
+ [content, editor, uploadImageForSave]
24708
+ );
24469
24709
  (0, import_react51.useEffect)(() => {
24470
24710
  if (editor && content !== editor.getHTML()) {
24471
24711
  if (editor.isEmpty && content) {
@@ -24513,7 +24753,8 @@ var UEditor = ({
24513
24753
  ]
24514
24754
  }
24515
24755
  );
24516
- };
24756
+ });
24757
+ UEditor.displayName = "UEditor";
24517
24758
  var UEditor_default = UEditor;
24518
24759
 
24519
24760
  // src/index.ts
@@ -24659,6 +24900,7 @@ function getUnderverseMessages(locale = "en") {
24659
24900
  Tooltip,
24660
24901
  TranslationProvider,
24661
24902
  UEditor,
24903
+ UEditorPrepareContentForSaveError,
24662
24904
  UnderverseProvider,
24663
24905
  VARIANT_STYLES_ALERT,
24664
24906
  VARIANT_STYLES_BTN,
@@ -24669,6 +24911,7 @@ function getUnderverseMessages(locale = "en") {
24669
24911
  getAnimationStyles,
24670
24912
  getUnderverseMessages,
24671
24913
  injectAnimationStyles,
24914
+ prepareUEditorContentForSave,
24672
24915
  shadcnAnimationStyles,
24673
24916
  underverseMessages,
24674
24917
  useFormField,