@particle-academy/react-fancy 4.7.0 → 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
@@ -2139,6 +2139,7 @@ function getIconResolutionVersion() {
2139
2139
  return lucideVersion;
2140
2140
  }
2141
2141
  function resolveFromLucide(name) {
2142
+ if (typeof window === "undefined") return null;
2142
2143
  if (!lucideModule) {
2143
2144
  ensureLucideLoaded();
2144
2145
  return null;
@@ -11588,6 +11589,71 @@ function mergeExtensions(instanceExtensions) {
11588
11589
  }
11589
11590
  return merged;
11590
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";
11591
11657
  function toHtml(value, outputFormat, unsafe) {
11592
11658
  if (!value) return "";
11593
11659
  const raw = (() => {
@@ -11607,16 +11673,22 @@ function EditorRoot({
11607
11673
  outputFormat = "html",
11608
11674
  lineSpacing = 1.6,
11609
11675
  placeholder,
11676
+ mode: modeProp,
11610
11677
  extensions: instanceExtensions,
11611
11678
  unsafe = false
11612
11679
  }) {
11613
11680
  const contentRef = useRef(null);
11614
- const [, setValue] = useControllableState(controlledValue, defaultValue, onChange);
11681
+ const [value, setValue] = useControllableState(controlledValue, defaultValue, onChange);
11682
+ const mode = useFieldMode(modeProp);
11683
+ const isView = mode === "view";
11615
11684
  const initialHtml = useMemo(
11616
- () => toHtml(controlledValue ?? defaultValue, outputFormat, unsafe),
11617
- // 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.
11618
11690
  // eslint-disable-next-line react-hooks/exhaustive-deps
11619
- []
11691
+ [isView, outputFormat, unsafe]
11620
11692
  );
11621
11693
  const extensions = useMemo(
11622
11694
  () => mergeExtensions(instanceExtensions),
@@ -11679,10 +11751,35 @@ function EditorRoot({
11679
11751
  _initialHtml: initialHtml,
11680
11752
  _onInput: handleInput
11681
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
+ }
11682
11778
  return /* @__PURE__ */ jsx(EditorContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
11683
11779
  "div",
11684
11780
  {
11685
11781
  "data-react-fancy-editor": "",
11782
+ "data-mode": "edit",
11686
11783
  className: cn(
11687
11784
  "overflow-hidden rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900",
11688
11785
  className
@@ -11698,71 +11795,6 @@ var Editor = Object.assign(EditorRoot, {
11698
11795
  Toolbar: ToolbarWithSeparator,
11699
11796
  Content: EditorContent
11700
11797
  });
11701
- function RenderedContent({
11702
- html,
11703
- extensions: instanceExtensions,
11704
- unsafe = false
11705
- }) {
11706
- const extensions = useMemo(
11707
- () => mergeExtensions(instanceExtensions),
11708
- [instanceExtensions]
11709
- );
11710
- const segments = useMemo(
11711
- () => parseSegments(html, extensions),
11712
- [html, extensions]
11713
- );
11714
- const renderHtml = (content) => unsafe ? content : sanitizeHtml(content);
11715
- if (segments.length === 1 && segments[0].type === "html") {
11716
- return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(segments[0].content) } });
11717
- }
11718
- if (segments.length === 0) {
11719
- return null;
11720
- }
11721
- return /* @__PURE__ */ jsx(Fragment, { children: segments.map((segment, i) => {
11722
- if (segment.type === "html") {
11723
- return segment.content ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(segment.content) } }, i) : null;
11724
- }
11725
- const ext = extensions.find(
11726
- (e) => e.tag.toLowerCase() === segment.tag
11727
- );
11728
- if (!ext) return null;
11729
- const Component = ext.component;
11730
- const isBlock = ext.block !== false;
11731
- const Wrapper = isBlock ? "div" : "span";
11732
- return /* @__PURE__ */ jsx(Wrapper, { "data-render-extension": segment.tag, children: /* @__PURE__ */ jsx(Component, { content: segment.content, attributes: segment.attributes }) }, i);
11733
- }) });
11734
- }
11735
- RenderedContent.displayName = "RenderedContent";
11736
- function ContentRenderer({
11737
- value,
11738
- format = "auto",
11739
- lineSpacing = 1.6,
11740
- className,
11741
- extensions: instanceExtensions,
11742
- unsafe = false
11743
- }) {
11744
- const extensions = useMemo(
11745
- () => mergeExtensions(instanceExtensions),
11746
- [instanceExtensions]
11747
- );
11748
- const html = useMemo(() => {
11749
- const safe = value ?? "";
11750
- const resolvedFormat = format === "auto" ? detectFormat(safe) : format;
11751
- const raw = resolvedFormat === "markdown" ? marked.parse(safe, { async: false }) : safe;
11752
- return unsafe ? raw : sanitizeHtml(raw);
11753
- }, [value, format, unsafe]);
11754
- const hasExtensions = extensions.length > 0;
11755
- return /* @__PURE__ */ jsx(
11756
- "div",
11757
- {
11758
- "data-react-fancy-content-renderer": "",
11759
- style: { lineHeight: lineSpacing },
11760
- className: cn("text-sm", proseClasses, className),
11761
- children: hasExtensions ? /* @__PURE__ */ jsx(RenderedContent, { html, extensions, unsafe }) : /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: html } })
11762
- }
11763
- );
11764
- }
11765
- ContentRenderer.displayName = "ContentRenderer";
11766
11798
  var MenuContext = createContext(null);
11767
11799
  function useMenu() {
11768
11800
  const ctx = useContext(MenuContext);