@particle-academy/react-fancy 4.7.1 → 4.8.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/index.d.cts CHANGED
@@ -2064,6 +2064,14 @@ interface EditorProps {
2064
2064
  outputFormat?: "html" | "markdown";
2065
2065
  lineSpacing?: number;
2066
2066
  placeholder?: string;
2067
+ /**
2068
+ * View/edit mode — like the react-fancy inputs. Resolution: this prop →
2069
+ * nearest `<Form>` / `<FormProvider>` context → `"edit"`. In `"view"` mode the
2070
+ * editor renders its value read-only through `<ContentRenderer>` (matching
2071
+ * `outputFormat` — markdown/html) instead of the editable toolbar + content.
2072
+ * @default "edit"
2073
+ */
2074
+ mode?: FieldMode;
2067
2075
  /** Per-instance render extensions. Merged with any globally-registered extensions. */
2068
2076
  extensions?: RenderExtension[];
2069
2077
  /**
@@ -2091,7 +2099,7 @@ declare namespace EditorContent {
2091
2099
  var displayName: string;
2092
2100
  }
2093
2101
 
2094
- declare function EditorRoot({ children, className, value: controlledValue, defaultValue, onChange, outputFormat, lineSpacing, placeholder, extensions: instanceExtensions, unsafe, }: EditorProps): react.JSX.Element;
2102
+ declare function EditorRoot({ children, className, value: controlledValue, defaultValue, onChange, outputFormat, lineSpacing, placeholder, mode: modeProp, extensions: instanceExtensions, unsafe, }: EditorProps): react.JSX.Element;
2095
2103
  declare const Editor: typeof EditorRoot & {
2096
2104
  Toolbar: typeof EditorToolbar & {
2097
2105
  Separator: typeof EditorToolbarSeparator;
package/dist/index.d.ts CHANGED
@@ -2064,6 +2064,14 @@ interface EditorProps {
2064
2064
  outputFormat?: "html" | "markdown";
2065
2065
  lineSpacing?: number;
2066
2066
  placeholder?: string;
2067
+ /**
2068
+ * View/edit mode — like the react-fancy inputs. Resolution: this prop →
2069
+ * nearest `<Form>` / `<FormProvider>` context → `"edit"`. In `"view"` mode the
2070
+ * editor renders its value read-only through `<ContentRenderer>` (matching
2071
+ * `outputFormat` — markdown/html) instead of the editable toolbar + content.
2072
+ * @default "edit"
2073
+ */
2074
+ mode?: FieldMode;
2067
2075
  /** Per-instance render extensions. Merged with any globally-registered extensions. */
2068
2076
  extensions?: RenderExtension[];
2069
2077
  /**
@@ -2091,7 +2099,7 @@ declare namespace EditorContent {
2091
2099
  var displayName: string;
2092
2100
  }
2093
2101
 
2094
- declare function EditorRoot({ children, className, value: controlledValue, defaultValue, onChange, outputFormat, lineSpacing, placeholder, extensions: instanceExtensions, unsafe, }: EditorProps): react.JSX.Element;
2102
+ declare function EditorRoot({ children, className, value: controlledValue, defaultValue, onChange, outputFormat, lineSpacing, placeholder, mode: modeProp, extensions: instanceExtensions, unsafe, }: EditorProps): react.JSX.Element;
2095
2103
  declare const Editor: typeof EditorRoot & {
2096
2104
  Toolbar: typeof EditorToolbar & {
2097
2105
  Separator: typeof EditorToolbarSeparator;
package/dist/index.js CHANGED
@@ -11589,6 +11589,71 @@ function mergeExtensions(instanceExtensions) {
11589
11589
  }
11590
11590
  return merged;
11591
11591
  }
11592
+ function RenderedContent({
11593
+ html,
11594
+ extensions: instanceExtensions,
11595
+ unsafe = false
11596
+ }) {
11597
+ const extensions = useMemo(
11598
+ () => mergeExtensions(instanceExtensions),
11599
+ [instanceExtensions]
11600
+ );
11601
+ const segments = useMemo(
11602
+ () => parseSegments(html, extensions),
11603
+ [html, extensions]
11604
+ );
11605
+ const renderHtml = (content) => unsafe ? content : sanitizeHtml(content);
11606
+ if (segments.length === 1 && segments[0].type === "html") {
11607
+ return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(segments[0].content) } });
11608
+ }
11609
+ if (segments.length === 0) {
11610
+ return null;
11611
+ }
11612
+ return /* @__PURE__ */ jsx(Fragment, { children: segments.map((segment, i) => {
11613
+ if (segment.type === "html") {
11614
+ return segment.content ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(segment.content) } }, i) : null;
11615
+ }
11616
+ const ext = extensions.find(
11617
+ (e) => e.tag.toLowerCase() === segment.tag
11618
+ );
11619
+ if (!ext) return null;
11620
+ const Component = ext.component;
11621
+ const isBlock = ext.block !== false;
11622
+ const Wrapper = isBlock ? "div" : "span";
11623
+ return /* @__PURE__ */ jsx(Wrapper, { "data-render-extension": segment.tag, children: /* @__PURE__ */ jsx(Component, { content: segment.content, attributes: segment.attributes }) }, i);
11624
+ }) });
11625
+ }
11626
+ RenderedContent.displayName = "RenderedContent";
11627
+ function ContentRenderer({
11628
+ value,
11629
+ format = "auto",
11630
+ lineSpacing = 1.6,
11631
+ className,
11632
+ extensions: instanceExtensions,
11633
+ unsafe = false
11634
+ }) {
11635
+ const extensions = useMemo(
11636
+ () => mergeExtensions(instanceExtensions),
11637
+ [instanceExtensions]
11638
+ );
11639
+ const html = useMemo(() => {
11640
+ const safe = value ?? "";
11641
+ const resolvedFormat = format === "auto" ? detectFormat(safe) : format;
11642
+ const raw = resolvedFormat === "markdown" ? marked.parse(safe, { async: false }) : safe;
11643
+ return unsafe ? raw : sanitizeHtml(raw);
11644
+ }, [value, format, unsafe]);
11645
+ const hasExtensions = extensions.length > 0;
11646
+ return /* @__PURE__ */ jsx(
11647
+ "div",
11648
+ {
11649
+ "data-react-fancy-content-renderer": "",
11650
+ style: { lineHeight: lineSpacing },
11651
+ className: cn("text-sm", proseClasses, className),
11652
+ children: hasExtensions ? /* @__PURE__ */ jsx(RenderedContent, { html, extensions, unsafe }) : /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: html } })
11653
+ }
11654
+ );
11655
+ }
11656
+ ContentRenderer.displayName = "ContentRenderer";
11592
11657
  function toHtml(value, outputFormat, unsafe) {
11593
11658
  if (!value) return "";
11594
11659
  const raw = (() => {
@@ -11608,16 +11673,22 @@ function EditorRoot({
11608
11673
  outputFormat = "html",
11609
11674
  lineSpacing = 1.6,
11610
11675
  placeholder,
11676
+ mode: modeProp,
11611
11677
  extensions: instanceExtensions,
11612
11678
  unsafe = false
11613
11679
  }) {
11614
11680
  const contentRef = useRef(null);
11615
- const [, setValue] = useControllableState(controlledValue, defaultValue, onChange);
11681
+ const [value, setValue] = useControllableState(controlledValue, defaultValue, onChange);
11682
+ const mode = useFieldMode(modeProp);
11683
+ const isView = mode === "view";
11616
11684
  const initialHtml = useMemo(
11617
- () => toHtml(controlledValue ?? defaultValue, outputFormat, unsafe),
11618
- // Only compute once on mount don't re-run when value changes from user input
11685
+ () => toHtml(value, outputFormat, unsafe),
11686
+ // Seed the contentEditable from the LIVE value when (re)entering edit mode.
11687
+ // EditorContent reads this once on mount, and it only mounts in edit mode —
11688
+ // so this captures the current value on view→edit, not a stale mount snapshot,
11689
+ // and does NOT re-run on every keystroke.
11619
11690
  // eslint-disable-next-line react-hooks/exhaustive-deps
11620
- []
11691
+ [isView, outputFormat, unsafe]
11621
11692
  );
11622
11693
  const extensions = useMemo(
11623
11694
  () => mergeExtensions(instanceExtensions),
@@ -11680,10 +11751,35 @@ function EditorRoot({
11680
11751
  _initialHtml: initialHtml,
11681
11752
  _onInput: handleInput
11682
11753
  };
11754
+ if (isView) {
11755
+ return /* @__PURE__ */ jsx(
11756
+ "div",
11757
+ {
11758
+ "data-react-fancy-editor": "",
11759
+ "data-mode": "view",
11760
+ className: cn(
11761
+ "overflow-hidden rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900",
11762
+ className
11763
+ ),
11764
+ children: /* @__PURE__ */ jsx(
11765
+ ContentRenderer,
11766
+ {
11767
+ value,
11768
+ format: outputFormat,
11769
+ lineSpacing,
11770
+ extensions: instanceExtensions,
11771
+ unsafe,
11772
+ className: "px-4 py-3"
11773
+ }
11774
+ )
11775
+ }
11776
+ );
11777
+ }
11683
11778
  return /* @__PURE__ */ jsx(EditorContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
11684
11779
  "div",
11685
11780
  {
11686
11781
  "data-react-fancy-editor": "",
11782
+ "data-mode": "edit",
11687
11783
  className: cn(
11688
11784
  "overflow-hidden rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900",
11689
11785
  className
@@ -11699,71 +11795,6 @@ var Editor = Object.assign(EditorRoot, {
11699
11795
  Toolbar: ToolbarWithSeparator,
11700
11796
  Content: EditorContent
11701
11797
  });
11702
- function RenderedContent({
11703
- html,
11704
- extensions: instanceExtensions,
11705
- unsafe = false
11706
- }) {
11707
- const extensions = useMemo(
11708
- () => mergeExtensions(instanceExtensions),
11709
- [instanceExtensions]
11710
- );
11711
- const segments = useMemo(
11712
- () => parseSegments(html, extensions),
11713
- [html, extensions]
11714
- );
11715
- const renderHtml = (content) => unsafe ? content : sanitizeHtml(content);
11716
- if (segments.length === 1 && segments[0].type === "html") {
11717
- return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(segments[0].content) } });
11718
- }
11719
- if (segments.length === 0) {
11720
- return null;
11721
- }
11722
- return /* @__PURE__ */ jsx(Fragment, { children: segments.map((segment, i) => {
11723
- if (segment.type === "html") {
11724
- return segment.content ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(segment.content) } }, i) : null;
11725
- }
11726
- const ext = extensions.find(
11727
- (e) => e.tag.toLowerCase() === segment.tag
11728
- );
11729
- if (!ext) return null;
11730
- const Component = ext.component;
11731
- const isBlock = ext.block !== false;
11732
- const Wrapper = isBlock ? "div" : "span";
11733
- return /* @__PURE__ */ jsx(Wrapper, { "data-render-extension": segment.tag, children: /* @__PURE__ */ jsx(Component, { content: segment.content, attributes: segment.attributes }) }, i);
11734
- }) });
11735
- }
11736
- RenderedContent.displayName = "RenderedContent";
11737
- function ContentRenderer({
11738
- value,
11739
- format = "auto",
11740
- lineSpacing = 1.6,
11741
- className,
11742
- extensions: instanceExtensions,
11743
- unsafe = false
11744
- }) {
11745
- const extensions = useMemo(
11746
- () => mergeExtensions(instanceExtensions),
11747
- [instanceExtensions]
11748
- );
11749
- const html = useMemo(() => {
11750
- const safe = value ?? "";
11751
- const resolvedFormat = format === "auto" ? detectFormat(safe) : format;
11752
- const raw = resolvedFormat === "markdown" ? marked.parse(safe, { async: false }) : safe;
11753
- return unsafe ? raw : sanitizeHtml(raw);
11754
- }, [value, format, unsafe]);
11755
- const hasExtensions = extensions.length > 0;
11756
- return /* @__PURE__ */ jsx(
11757
- "div",
11758
- {
11759
- "data-react-fancy-content-renderer": "",
11760
- style: { lineHeight: lineSpacing },
11761
- className: cn("text-sm", proseClasses, className),
11762
- children: hasExtensions ? /* @__PURE__ */ jsx(RenderedContent, { html, extensions, unsafe }) : /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: html } })
11763
- }
11764
- );
11765
- }
11766
- ContentRenderer.displayName = "ContentRenderer";
11767
11798
  var MenuContext = createContext(null);
11768
11799
  function useMenu() {
11769
11800
  const ctx = useContext(MenuContext);