@dalgoridim/headless-cms 0.1.0 → 0.2.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.
@@ -26,11 +26,10 @@ import {
26
26
  useState,
27
27
  useCallback
28
28
  } from "react";
29
- import { toast } from "sonner";
30
29
  import { jsx } from "react/jsx-runtime";
31
30
  var defaultNotifier = {
32
- success: (m) => toast.success(m),
33
- error: (m) => toast.error(m)
31
+ success: (m) => console.info(`[cms] ${m}`),
32
+ error: (m) => console.error(`[cms] ${m}`)
34
33
  };
35
34
  var PageContext = createContext(void 0);
36
35
  var dirtyKey = (collection, sectionKey) => `${collection}:${sectionKey}`;
@@ -279,99 +278,8 @@ function CmsAuthProvider({
279
278
 
280
279
  // src/client/ContentEditSpan.tsx
281
280
  import { useRef, useEffect, useState as useState2, useCallback as useCallback2 } from "react";
282
-
283
- // src/client/utils.ts
284
- import { clsx } from "clsx";
285
- import { twMerge } from "tailwind-merge";
286
- function cn(...inputs) {
287
- return twMerge(clsx(inputs));
288
- }
289
-
290
- // src/client/ContentEditSpan.tsx
291
- import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
292
- var PATTERNS = [
293
- { regex: /^\*\*(.+?)\*\*/, mark: "bold" },
294
- { regex: /^\*(.+?)\*/, mark: "italic" },
295
- { regex: /^~~br~~/, mark: "break" },
296
- { regex: /^~~(.+?)~~/, mark: "strike" },
297
- { regex: /^\^\^(.+?)\^\^/, mark: "primary" },
298
- { regex: /^__(.+?)__/, mark: "underline" },
299
- {
300
- regex: /^\[(.+?)\]\((https?:\/\/[^\s)]+)\)/,
301
- mark: "link",
302
- isLink: true
303
- }
304
- ];
305
- function parseSpecialString(input) {
306
- const out = [];
307
- let text = input;
308
- while (text.length) {
309
- let matched = false;
310
- for (const p of PATTERNS) {
311
- const m = p.regex.exec(text);
312
- if (!m) continue;
313
- matched = true;
314
- if (p.mark === "break") {
315
- out.push({ text: "\n", break: true });
316
- } else if ("isLink" in p) {
317
- out.push({ text: m[1], link: m[2] });
318
- } else {
319
- const inner = parseSpecialString(m[1]);
320
- inner.forEach(
321
- (n) => n[p.mark] = true
322
- );
323
- out.push(...inner);
324
- }
325
- text = text.slice(m[0].length);
326
- break;
327
- }
328
- if (!matched) {
329
- out.push({ text: text[0] });
330
- text = text.slice(1);
331
- }
332
- }
333
- return out;
334
- }
335
- function RenderStatic({
336
- raw,
337
- as: Component = "span",
338
- className
339
- }) {
340
- const nodes = parseSpecialString(raw);
341
- const content = /* @__PURE__ */ jsx3(Fragment, { children: nodes.map((l, i) => {
342
- if (l.break) return /* @__PURE__ */ jsx3("br", {}, i);
343
- let el = l.text;
344
- if (l.bold) el = /* @__PURE__ */ jsx3("strong", { children: el });
345
- if (l.italic) el = /* @__PURE__ */ jsx3("em", { children: el });
346
- if (l.strike) el = /* @__PURE__ */ jsx3("s", { children: el });
347
- if (l.primary || l.underline) {
348
- el = /* @__PURE__ */ jsx3(
349
- "span",
350
- {
351
- style: {
352
- color: l.primary ? "var(--color-primary)" : void 0,
353
- textDecoration: l.underline ? "underline" : void 0
354
- },
355
- children: el
356
- }
357
- );
358
- }
359
- if (l.link) {
360
- el = /* @__PURE__ */ jsx3(
361
- "a",
362
- {
363
- href: l.link,
364
- target: "_blank",
365
- rel: "noopener noreferrer",
366
- className: "inline hover:underline",
367
- children: el
368
- }
369
- );
370
- }
371
- return /* @__PURE__ */ jsx3("span", { children: el }, i);
372
- }) });
373
- return /* @__PURE__ */ jsx3(Component, { className, children: content });
374
- }
281
+ import { jsx as jsx3 } from "react/jsx-runtime";
282
+ var defaultRenderValue = (raw) => raw;
375
283
  function getNestedValue(obj, path) {
376
284
  const keys = path.split(".");
377
285
  let current = obj;
@@ -383,20 +291,22 @@ function getNestedValue(obj, path) {
383
291
  return current;
384
292
  }
385
293
  function ContentEditSpan({
386
- collection = "portfolio",
294
+ collection,
387
295
  sectionKey,
388
296
  fieldKey,
389
297
  className,
390
298
  children,
391
- as = "span"
299
+ as = "span",
300
+ renderValue = defaultRenderValue
392
301
  }) {
393
302
  var _a, _b;
394
303
  const { sections, editField } = usePageContext();
395
304
  const { isEditing } = useCmsAuth();
396
305
  const section = (_a = sections[collection]) == null ? void 0 : _a[sectionKey];
397
306
  const raw = (_b = getNestedValue(section, fieldKey)) != null ? _b : typeof children === "string" ? children : "";
307
+ const Component = as;
398
308
  if (!isEditing) {
399
- return /* @__PURE__ */ jsx3(RenderStatic, { raw, as, className });
309
+ return /* @__PURE__ */ jsx3(Component, { className, children: renderValue(raw) });
400
310
  }
401
311
  return /* @__PURE__ */ jsx3(
402
312
  EditableContentSpan,
@@ -407,18 +317,20 @@ function ContentEditSpan({
407
317
  className,
408
318
  raw,
409
319
  editField,
410
- as
320
+ as,
321
+ renderValue
411
322
  }
412
323
  );
413
324
  }
414
325
  function EditableContentSpan({
415
- collection = "portfolio",
326
+ collection,
416
327
  sectionKey,
417
328
  fieldKey,
418
329
  className,
419
330
  raw,
420
331
  editField,
421
- as: Component = "span"
332
+ as: Component = "span",
333
+ renderValue
422
334
  }) {
423
335
  const { isEditing } = useCmsAuth();
424
336
  const [isFocused, setIsFocused] = useState2(false);
@@ -466,19 +378,16 @@ function EditableContentSpan({
466
378
  Component,
467
379
  {
468
380
  ref: contentRef,
469
- className: cn(
470
- className,
471
- "outline-none transition-all duration-200",
472
- "whitespace-pre-wrap break-words overflow-wrap-anywhere",
473
- isFocused && "ring-2 ring-primary/50 ring-offset-2 ring-offset-neutral-900 rounded-sm px-2",
474
- !isFocused && isEditing && "hover:ring-1 hover:ring-primary/30 hover:ring-offset-1 hover:ring-offset-neutral-900 hover:rounded-sm hover:px-2 cursor-text"
475
- ),
381
+ className,
382
+ "data-cms-editable": "",
383
+ "data-cms-editing": isEditing ? "" : void 0,
384
+ "data-cms-focused": isFocused ? "" : void 0,
476
385
  contentEditable: isEditing,
477
386
  suppressContentEditableWarning: true,
478
387
  onInput: handleInput,
479
388
  onBlur: handleBlur,
480
389
  onFocus: handleFocus,
481
- children: !isFocused && /* @__PURE__ */ jsx3(RenderStatic, { raw: editValue, as: "span" })
390
+ children: !isFocused && renderValue(editValue)
482
391
  },
483
392
  isFocused ? "editing" : "static"
484
393
  );
@@ -486,24 +395,20 @@ function EditableContentSpan({
486
395
 
487
396
  // src/client/EditableImage.tsx
488
397
  import { useRef as useRef2, useState as useState3 } from "react";
489
- import { createPortal } from "react-dom";
490
- import { CameraIcon, Link2Icon, XIcon, CheckIcon } from "lucide-react";
491
- import { Fragment as Fragment2, jsx as jsx4, jsxs } from "react/jsx-runtime";
398
+ import { jsx as jsx4, jsxs } from "react/jsx-runtime";
492
399
  function EditableImage({
493
400
  sectionKey,
494
401
  fieldKey,
495
402
  src,
496
403
  collection,
497
404
  docId,
498
- className
405
+ className,
406
+ children
499
407
  }) {
500
408
  const { isEditing } = useCmsAuth();
501
409
  const { editField, setPendingImage, pendingImages, saving } = usePageContext();
502
410
  const [preview, setPreview] = useState3(src);
503
411
  const [hasError, setHasError] = useState3(false);
504
- const [showUrlModal, setShowUrlModal] = useState3(false);
505
- const [urlInput, setUrlInput] = useState3("");
506
- const [urlPreview, setUrlPreview] = useState3("");
507
412
  const inputRef = useRef2(null);
508
413
  const pendingImage = pendingImages.find(
509
414
  (img) => img.sectionKey === sectionKey && img.fieldKey === fieldKey
@@ -528,467 +433,111 @@ function EditableImage({
528
433
  isExternal: false
529
434
  });
530
435
  };
531
- const handleUrlConfirm = () => {
532
- if (!urlPreview) return;
533
- setPreview(urlPreview);
436
+ const openFilePicker = () => {
437
+ var _a;
438
+ if (saving) return;
439
+ (_a = inputRef.current) == null ? void 0 : _a.click();
440
+ };
441
+ const setExternalUrl = (value) => {
442
+ let valid = false;
443
+ try {
444
+ const url = new URL(value);
445
+ valid = url.protocol === "http:" || url.protocol === "https:";
446
+ } catch (e) {
447
+ valid = false;
448
+ }
449
+ if (!valid) return false;
450
+ setPreview(value);
534
451
  setHasError(false);
535
- editField(collection, sectionKey, fieldKey, urlPreview);
452
+ editField(collection, sectionKey, fieldKey, value);
536
453
  setPendingImage({
537
454
  file: null,
538
- localUrl: urlPreview,
455
+ localUrl: value,
539
456
  sectionKey,
540
457
  fieldKey,
541
458
  collection,
542
459
  docId,
543
460
  isExternal: true
544
461
  });
545
- setShowUrlModal(false);
546
- setUrlInput("");
547
- setUrlPreview("");
462
+ return true;
548
463
  };
549
- const handleUrlChange = (value) => {
550
- setUrlInput(value);
551
- try {
552
- const url = new URL(value);
553
- if (url.protocol === "http:" || url.protocol === "https:") {
554
- setUrlPreview(value);
555
- } else {
556
- setUrlPreview("");
557
- }
558
- } catch (e) {
559
- setUrlPreview("");
560
- }
464
+ const state = {
465
+ src: imgSrc,
466
+ isEditing,
467
+ saving,
468
+ hasError,
469
+ openFilePicker,
470
+ setExternalUrl,
471
+ imgProps: { src: imgSrc, onError: () => setHasError(true) }
561
472
  };
562
- const imageNode = hasError || !imgSrc ? /* @__PURE__ */ jsx4("div", { className: "flex items-center justify-center w-full h-full p-4", children: /* @__PURE__ */ jsxs("div", { className: "text-center space-y-4", children: [
563
- /* @__PURE__ */ jsx4("div", { className: "w-32 h-32 mx-auto rounded-2xl bg-neutral-800/50 flex items-center justify-center", children: /* @__PURE__ */ jsx4(
564
- "svg",
565
- {
566
- className: "w-16 h-16 text-neutral-600",
567
- fill: "none",
568
- viewBox: "0 0 24 24",
569
- stroke: "currentColor",
570
- children: /* @__PURE__ */ jsx4(
571
- "path",
572
- {
573
- strokeLinecap: "round",
574
- strokeLinejoin: "round",
575
- strokeWidth: 1.5,
576
- d: "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
577
- }
578
- )
579
- }
580
- ) }),
581
- /* @__PURE__ */ jsx4("p", { className: "text-neutral-500 text-lg", children: "No image available" })
582
- ] }) }) : (
583
- // eslint-disable-next-line @next/next/no-img-element
473
+ return /* @__PURE__ */ jsxs("div", { className, children: [
584
474
  /* @__PURE__ */ jsx4(
585
- "img",
475
+ "input",
586
476
  {
587
- src: imgSrc,
588
- alt: "",
589
- className: "w-full h-full object-cover",
590
- onError: () => setHasError(true)
477
+ ref: inputRef,
478
+ type: "file",
479
+ accept: "image/*",
480
+ disabled: saving,
481
+ onChange: handleFileChange,
482
+ style: { display: "none" }
591
483
  }
592
- )
593
- );
594
- if (!isEditing) {
595
- return /* @__PURE__ */ jsx4("div", { className, children: imageNode });
596
- }
597
- return /* @__PURE__ */ jsxs(Fragment2, { children: [
598
- /* @__PURE__ */ jsxs("div", { className: cn("relative group", className), children: [
599
- imageNode,
600
- /* @__PURE__ */ jsx4("div", { className: "absolute inset-0 flex items-center justify-center bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity", children: saving ? /* @__PURE__ */ jsx4("div", { className: "w-8 h-8 border-4 border-white border-t-transparent rounded-full animate-spin" }) : /* @__PURE__ */ jsxs("div", { className: "flex gap-12", children: [
601
- /* @__PURE__ */ jsx4(
602
- CameraIcon,
603
- {
604
- className: "w-12 h-12 text-white cursor-pointer hover:text-primary",
605
- onClick: (e) => {
606
- var _a;
607
- e.stopPropagation();
608
- (_a = inputRef.current) == null ? void 0 : _a.click();
609
- }
610
- }
611
- ),
612
- /* @__PURE__ */ jsx4(
613
- Link2Icon,
614
- {
615
- className: "w-12 h-12 text-white cursor-pointer hover:text-primary",
616
- onClick: (e) => {
617
- e.stopPropagation();
618
- setShowUrlModal(true);
619
- }
620
- }
621
- )
622
- ] }) }),
623
- /* @__PURE__ */ jsx4(
624
- "input",
625
- {
626
- ref: inputRef,
627
- type: "file",
628
- accept: "image/*",
629
- disabled: saving,
630
- onChange: handleFileChange,
631
- className: "absolute inset-0 opacity-0 pointer-events-none"
632
- }
633
- )
634
- ] }),
635
- showUrlModal && createPortal(
636
- /* @__PURE__ */ jsx4("div", { className: "fixed inset-0 bg-black/70 flex items-center justify-center z-9999 p-4", children: /* @__PURE__ */ jsxs("div", { className: "bg-neutral-900 rounded-xl p-6 max-w-sm w-full space-y-4", children: [
637
- /* @__PURE__ */ jsx4("h3", { className: "text-lg font-bold", children: "Add Image URL" }),
638
- /* @__PURE__ */ jsx4(
639
- "input",
640
- {
641
- type: "text",
642
- value: urlInput,
643
- onChange: (e) => handleUrlChange(e.target.value),
644
- placeholder: "https://example.com/image.png",
645
- className: "w-full px-3 py-2 rounded bg-neutral-800 border border-neutral-700"
646
- }
647
- ),
648
- urlPreview ? (
649
- // eslint-disable-next-line @next/next/no-img-element
650
- /* @__PURE__ */ jsx4(
651
- "img",
652
- {
653
- src: urlPreview,
654
- alt: "",
655
- className: "w-full h-40 object-contain rounded",
656
- onError: () => setHasError(true)
657
- }
658
- )
659
- ) : /* @__PURE__ */ jsx4("div", { className: "h-40 flex items-center justify-center text-neutral-500", children: "Invalid URL" }),
660
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
661
- /* @__PURE__ */ jsxs(
662
- "button",
663
- {
664
- onClick: () => setShowUrlModal(false),
665
- className: "px-3 py-1 bg-neutral-700 rounded text-sm",
666
- children: [
667
- /* @__PURE__ */ jsx4(XIcon, { className: "w-4 h-4 inline" }),
668
- " Cancel"
669
- ]
670
- }
671
- ),
672
- /* @__PURE__ */ jsxs(
673
- "button",
674
- {
675
- onClick: handleUrlConfirm,
676
- disabled: !urlPreview,
677
- className: "px-3 py-1 bg-primary rounded text-sm disabled:opacity-50",
678
- children: [
679
- /* @__PURE__ */ jsx4(CheckIcon, { className: "w-4 h-4 inline" }),
680
- " Confirm"
681
- ]
682
- }
683
- )
684
- ] })
685
- ] }) }),
686
- document.body
484
+ ),
485
+ children ? children(state) : (
486
+ // eslint-disable-next-line @next/next/no-img-element
487
+ /* @__PURE__ */ jsx4("img", __spreadProps(__spreadValues({}, state.imgProps), { alt: "" }))
687
488
  )
688
489
  ] });
689
490
  }
690
491
 
691
492
  // src/client/MarkdownEditor.tsx
692
- import { useState as useState4 } from "react";
693
- import { createPortal as createPortal2 } from "react-dom";
694
- import ReactMarkdown from "react-markdown";
695
- import remarkGfm from "remark-gfm";
696
- import {
697
- EyeIcon,
698
- EditIcon,
699
- SaveIcon,
700
- XIcon as XIcon2,
701
- TypeIcon,
702
- BoldIcon,
703
- ItalicIcon,
704
- ListIcon,
705
- LinkIcon,
706
- CodeIcon,
707
- ImageIcon,
708
- Heading1Icon,
709
- Heading2Icon
710
- } from "lucide-react";
711
- import { toast as toast2 } from "sonner";
712
- import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
713
- function MarkdownEditor({
493
+ import { useCallback as useCallback3, useRef as useRef3, useState as useState4 } from "react";
494
+ function useMarkdownEditor({
714
495
  initialValue,
715
- onSave,
716
- trigger,
717
- title = "Edit Content"
718
- }) {
719
- const [open, setOpen] = useState4(false);
720
- const [content, setContent] = useState4(initialValue);
721
- const [isPreview, setIsPreview] = useState4(false);
722
- const handleOpen = () => {
723
- setContent(initialValue);
724
- setOpen(true);
725
- };
726
- const handleSave = () => {
727
- onSave(content);
728
- setOpen(false);
729
- };
730
- const handleCancel = () => {
731
- setContent(initialValue);
732
- setOpen(false);
733
- };
734
- const insertMarkdown = (before, after = "", placeholder = "text") => {
735
- const textarea = document.querySelector(
736
- "textarea[data-markdown-editor]"
737
- );
738
- if (!textarea) return;
739
- const start = textarea.selectionStart;
740
- const end = textarea.selectionEnd;
741
- const selectedText = content.substring(start, end) || placeholder;
742
- const newText = content.substring(0, start) + before + selectedText + after + content.substring(end);
743
- setContent(newText);
744
- setTimeout(() => {
745
- textarea.focus();
746
- const newCursorPos = start + before.length + selectedText.length;
747
- textarea.setSelectionRange(newCursorPos, newCursorPos);
748
- }, 0);
749
- };
750
- return /* @__PURE__ */ jsxs2(Fragment3, { children: [
751
- trigger ? /* @__PURE__ */ jsx5("div", { onClick: handleOpen, children: trigger }) : /* @__PURE__ */ jsxs2(
752
- "button",
753
- {
754
- type: "button",
755
- onClick: handleOpen,
756
- className: "inline-flex items-center gap-2 px-3 py-1.5 text-sm rounded-lg border border-neutral-700 hover:bg-neutral-800 transition-colors",
757
- children: [
758
- /* @__PURE__ */ jsx5(EditIcon, { className: "w-4 h-4" }),
759
- "Edit Content"
760
- ]
761
- }
762
- ),
763
- open && createPortal2(
764
- /* @__PURE__ */ jsx5("div", { className: "fixed inset-0 z-[10001] flex items-center justify-center bg-black/70 p-4", children: /* @__PURE__ */ jsxs2("div", { className: "md:max-w-6xl w-full h-[90vh] flex flex-col bg-neutral-950 border border-neutral-800 rounded-xl overflow-hidden", children: [
765
- /* @__PURE__ */ jsx5("div", { className: "px-6 pt-6 pb-4 border-b border-neutral-800", children: /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between", children: [
766
- /* @__PURE__ */ jsxs2("h2", { className: "text-2xl font-bold flex items-center gap-3", children: [
767
- /* @__PURE__ */ jsx5("div", { className: "p-2 bg-primary/10 rounded-lg", children: /* @__PURE__ */ jsx5(TypeIcon, { className: "w-5 h-5 text-primary" }) }),
768
- title
769
- ] }),
770
- /* @__PURE__ */ jsx5(
771
- "button",
772
- {
773
- type: "button",
774
- onClick: () => setIsPreview(!isPreview),
775
- className: cn(
776
- "flex items-center gap-2 px-4 py-2 rounded-lg transition-all font-medium",
777
- isPreview ? "bg-primary text-white" : "bg-neutral-800 hover:bg-neutral-700 text-neutral-300"
778
- ),
779
- children: isPreview ? /* @__PURE__ */ jsxs2(Fragment3, { children: [
780
- /* @__PURE__ */ jsx5(EditIcon, { className: "w-4 h-4" }),
781
- "Edit"
782
- ] }) : /* @__PURE__ */ jsxs2(Fragment3, { children: [
783
- /* @__PURE__ */ jsx5(EyeIcon, { className: "w-4 h-4" }),
784
- "Preview"
785
- ] })
786
- }
787
- )
788
- ] }) }),
789
- /* @__PURE__ */ jsxs2("div", { className: "flex-1 overflow-hidden flex flex-col", children: [
790
- !isPreview && /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1 px-4 py-3 border-b border-neutral-800 bg-neutral-900/50 overflow-x-auto", children: [
791
- /* @__PURE__ */ jsx5(
792
- ToolbarButton,
793
- {
794
- icon: /* @__PURE__ */ jsx5(Heading1Icon, { className: "w-4 h-4" }),
795
- label: "Heading 1",
796
- onClick: () => insertMarkdown("# ", "", "Heading")
797
- }
798
- ),
799
- /* @__PURE__ */ jsx5(
800
- ToolbarButton,
801
- {
802
- icon: /* @__PURE__ */ jsx5(Heading2Icon, { className: "w-4 h-4" }),
803
- label: "Heading 2",
804
- onClick: () => insertMarkdown("## ", "", "Heading")
805
- }
806
- ),
807
- /* @__PURE__ */ jsx5("div", { className: "w-px h-6 bg-neutral-700 mx-1" }),
808
- /* @__PURE__ */ jsx5(
809
- ToolbarButton,
810
- {
811
- icon: /* @__PURE__ */ jsx5(BoldIcon, { className: "w-4 h-4" }),
812
- label: "Bold",
813
- onClick: () => insertMarkdown("**", "**", "bold text")
814
- }
815
- ),
816
- /* @__PURE__ */ jsx5(
817
- ToolbarButton,
818
- {
819
- icon: /* @__PURE__ */ jsx5(ItalicIcon, { className: "w-4 h-4" }),
820
- label: "Italic",
821
- onClick: () => insertMarkdown("*", "*", "italic text")
822
- }
823
- ),
824
- /* @__PURE__ */ jsx5("div", { className: "w-px h-6 bg-neutral-700 mx-1" }),
825
- /* @__PURE__ */ jsx5(
826
- ToolbarButton,
827
- {
828
- icon: /* @__PURE__ */ jsx5(LinkIcon, { className: "w-4 h-4" }),
829
- label: "Link",
830
- onClick: () => insertMarkdown("[", "](url)", "link text")
831
- }
832
- ),
833
- /* @__PURE__ */ jsx5(
834
- ToolbarButton,
835
- {
836
- icon: /* @__PURE__ */ jsx5(ImageIcon, { className: "w-4 h-4" }),
837
- label: "Image",
838
- onClick: () => insertMarkdown("![", "](url)", "alt text")
839
- }
840
- ),
841
- /* @__PURE__ */ jsx5("div", { className: "w-px h-6 bg-neutral-700 mx-1" }),
842
- /* @__PURE__ */ jsx5(
843
- ToolbarButton,
844
- {
845
- icon: /* @__PURE__ */ jsx5(ListIcon, { className: "w-4 h-4" }),
846
- label: "List",
847
- onClick: () => insertMarkdown("- ", "", "list item")
848
- }
849
- ),
850
- /* @__PURE__ */ jsx5(
851
- ToolbarButton,
852
- {
853
- icon: /* @__PURE__ */ jsx5(CodeIcon, { className: "w-4 h-4" }),
854
- label: "Code",
855
- onClick: () => insertMarkdown("```\n", "\n```", "code")
856
- }
857
- )
858
- ] }),
859
- /* @__PURE__ */ jsx5("div", { className: "flex-1 overflow-auto p-6", children: isPreview ? /* @__PURE__ */ jsx5("div", { className: "prose prose-invert prose-lg max-w-none", children: /* @__PURE__ */ jsx5(ReactMarkdown, { remarkPlugins: [remarkGfm], children: content }) }) : /* @__PURE__ */ jsxs2("div", { className: "h-full flex flex-col", children: [
860
- /* @__PURE__ */ jsx5(
861
- "textarea",
862
- {
863
- "data-markdown-editor": true,
864
- value: content,
865
- onChange: (e) => setContent(e.target.value),
866
- className: "h-full w-full resize-none rounded-md font-mono text-sm bg-neutral-900/50 border border-neutral-800 p-3 outline-none focus:border-primary",
867
- placeholder: "# Project Title\n\n## Overview\nWrite your description here..."
868
- }
869
- ),
870
- /* @__PURE__ */ jsxs2("div", { className: "mt-4 p-4 bg-neutral-900/50 border border-neutral-800 rounded-lg", children: [
871
- /* @__PURE__ */ jsxs2("h4", { className: "text-sm font-semibold mb-3 text-neutral-300 flex items-center gap-2", children: [
872
- /* @__PURE__ */ jsx5(CodeIcon, { className: "w-4 h-4 text-primary" }),
873
- "Markdown Guide"
874
- ] }),
875
- /* @__PURE__ */ jsxs2("div", { className: "grid grid-cols-2 md:grid-cols-3 gap-3 text-xs", children: [
876
- /* @__PURE__ */ jsx5(GuideItem, { code: "# Heading 1", desc: "Main heading" }),
877
- /* @__PURE__ */ jsx5(GuideItem, { code: "## Heading 2", desc: "Sub heading" }),
878
- /* @__PURE__ */ jsx5(GuideItem, { code: "**bold**", desc: "Bold text" }),
879
- /* @__PURE__ */ jsx5(GuideItem, { code: "*italic*", desc: "Italic text" }),
880
- /* @__PURE__ */ jsx5(GuideItem, { code: "[link](url)", desc: "Hyperlink" }),
881
- /* @__PURE__ */ jsx5(GuideItem, { code: "- list item", desc: "Bullet list" })
882
- ] })
883
- ] })
884
- ] }) })
885
- ] }),
886
- /* @__PURE__ */ jsx5("div", { className: "px-6 py-4 border-t border-neutral-800 bg-neutral-900/30", children: /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between w-full", children: [
887
- /* @__PURE__ */ jsxs2("p", { className: "text-sm text-neutral-500", children: [
888
- content.length,
889
- " characters"
890
- ] }),
891
- /* @__PURE__ */ jsxs2("div", { className: "flex gap-2", children: [
892
- /* @__PURE__ */ jsxs2(
893
- "button",
894
- {
895
- type: "button",
896
- onClick: handleCancel,
897
- className: "inline-flex items-center gap-2 px-4 py-2 text-sm rounded-md border border-neutral-700 hover:bg-neutral-800 transition-colors",
898
- children: [
899
- /* @__PURE__ */ jsx5(XIcon2, { className: "w-4 h-4" }),
900
- "Cancel"
901
- ]
902
- }
903
- ),
904
- /* @__PURE__ */ jsxs2(
905
- "button",
906
- {
907
- type: "button",
908
- onClick: handleSave,
909
- className: "inline-flex items-center gap-2 px-4 py-2 text-sm rounded-md bg-primary text-white hover:opacity-90 transition-opacity",
910
- children: [
911
- /* @__PURE__ */ jsx5(SaveIcon, { className: "w-4 h-4" }),
912
- "Save Content"
913
- ]
914
- }
915
- )
916
- ] })
917
- ] }) })
918
- ] }) }),
919
- document.body
920
- )
921
- ] });
922
- }
923
- function ToolbarButton({
924
- icon,
925
- label,
926
- onClick
927
- }) {
928
- return /* @__PURE__ */ jsx5(
929
- "button",
930
- {
931
- type: "button",
932
- onClick,
933
- title: label,
934
- className: "p-2 rounded-lg hover:bg-neutral-800 text-neutral-400 hover:text-white transition-colors",
935
- children: icon
936
- }
937
- );
938
- }
939
- function GuideItem({ code, desc }) {
940
- return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-1", children: [
941
- /* @__PURE__ */ jsx5("code", { className: "text-primary bg-primary/10 px-2 py-1 rounded text-xs font-mono", children: code }),
942
- /* @__PURE__ */ jsx5("span", { className: "text-neutral-500", children: desc })
943
- ] });
944
- }
945
- function ProjectContentEditor({
946
- content,
947
496
  onSave
948
497
  }) {
949
- const [saving, setSaving] = useState4(false);
950
- const handleSave = async (newContent) => {
951
- setSaving(true);
952
- try {
953
- await onSave(newContent);
954
- toast2.success("Content saved successfully!");
955
- } catch (error) {
956
- console.error("Failed to save content:", error);
957
- toast2.error("Failed to save content");
958
- } finally {
959
- setSaving(false);
960
- }
961
- };
962
- return /* @__PURE__ */ jsx5(
963
- MarkdownEditor,
964
- {
965
- initialValue: content,
966
- onSave: handleSave,
967
- title: "Edit Project Content",
968
- trigger: /* @__PURE__ */ jsxs2(
969
- "button",
970
- {
971
- className: "px-4 py-2 bg-neutral-800 rounded-lg hover:bg-neutral-700 transition-colors flex items-center gap-2 border border-neutral-700 hover:border-primary/50",
972
- disabled: saving,
973
- children: [
974
- /* @__PURE__ */ jsx5(EditIcon, { className: "w-4 h-4" }),
975
- saving ? "Saving..." : "Edit Full Content"
976
- ]
977
- }
978
- )
979
- }
498
+ const [value, setValue] = useState4(initialValue);
499
+ const textareaRef = useRef3(null);
500
+ const insert = useCallback3(
501
+ (before, after = "", placeholder = "text") => {
502
+ var _a, _b;
503
+ const textarea = textareaRef.current;
504
+ const start = (_a = textarea == null ? void 0 : textarea.selectionStart) != null ? _a : value.length;
505
+ const end = (_b = textarea == null ? void 0 : textarea.selectionEnd) != null ? _b : value.length;
506
+ const selected = value.substring(start, end) || placeholder;
507
+ const next = value.substring(0, start) + before + selected + after + value.substring(end);
508
+ setValue(next);
509
+ setTimeout(() => {
510
+ if (!textarea) return;
511
+ textarea.focus();
512
+ const caret = start + before.length + selected.length;
513
+ textarea.setSelectionRange(caret, caret);
514
+ }, 0);
515
+ },
516
+ [value]
517
+ );
518
+ const reset = useCallback3(
519
+ (to = initialValue) => setValue(to),
520
+ [initialValue]
980
521
  );
522
+ const save = useCallback3(() => onSave(value), [onSave, value]);
523
+ return {
524
+ value,
525
+ setValue,
526
+ textareaRef,
527
+ insert,
528
+ reset,
529
+ save,
530
+ charCount: value.length
531
+ };
981
532
  }
982
533
  export {
983
534
  CmsAuthContext,
984
535
  CmsAuthProvider,
985
536
  ContentEditSpan,
986
537
  EditableImage,
987
- MarkdownEditor,
988
538
  PageProvider,
989
- ProjectContentEditor,
990
- cn,
991
539
  useCmsAuth,
540
+ useMarkdownEditor,
992
541
  usePageContext
993
542
  };
994
543
  //# sourceMappingURL=index.js.map