@particle-academy/react-fancy 4.13.0 → 4.14.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/README.md CHANGED
@@ -162,7 +162,7 @@ npx vite build # Build demo app (verifies imports work)
162
162
  |-----------|-------------|------|
163
163
  | Composer | Chat-style message input composing textarea + actions | [docs](docs/Composer.md) |
164
164
  | Chart | SVG-based Bar, Line, Area, Pie, Donut, and Sparkline charts | [docs](docs/Chart.md) |
165
- | Editor | Toolbar chrome wrapper for contentEditable | [docs](docs/Editor.md) |
165
+ | Editor | Toolbar chrome wrapper for contentEditable, with a Source toggle to edit raw HTML/Markdown | [docs](docs/Editor.md) |
166
166
  | Kanban | Drag-and-drop board with columns and cards | [docs](docs/Kanban.md) |
167
167
  | Canvas | Interactive node canvas with pan, zoom, and connections | [docs](docs/Canvas.md) |
168
168
  | Diagram | Entity-relationship diagram with draggable nodes and relation lines | [docs](docs/Diagram.md) |
package/dist/index.cjs CHANGED
@@ -7884,11 +7884,12 @@ function usePopover() {
7884
7884
  }
7885
7885
  return ctx;
7886
7886
  }
7887
- function PopoverTrigger({ children, className }) {
7887
+ function PopoverTrigger({ children, className, ...rest }) {
7888
7888
  const { setOpen, open, anchorRef, hover, onHoverEnter, onHoverLeave } = usePopover();
7889
7889
  return /* @__PURE__ */ jsxRuntime.jsx(
7890
7890
  "span",
7891
7891
  {
7892
+ ...rest,
7892
7893
  ref: anchorRef,
7893
7894
  "data-react-fancy-popover-trigger": "",
7894
7895
  className: cn("inline-flex", className),
@@ -7902,7 +7903,7 @@ function PopoverTrigger({ children, className }) {
7902
7903
  );
7903
7904
  }
7904
7905
  PopoverTrigger.displayName = "PopoverTrigger";
7905
- function PopoverContent({ children, className }) {
7906
+ function PopoverContent({ children, className, style, ...rest }) {
7906
7907
  const { open, setOpen, anchorRef, floatingRef, placement, offset, hover, onHoverEnter, onHoverLeave } = usePopover();
7907
7908
  const outsideRef = react.useRef(null);
7908
7909
  const position = useFloatingPosition(anchorRef, floatingRef, {
@@ -7918,6 +7919,7 @@ function PopoverContent({ children, className }) {
7918
7919
  return /* @__PURE__ */ jsxRuntime.jsx(Portal, { children: /* @__PURE__ */ jsxRuntime.jsx(
7919
7920
  "div",
7920
7921
  {
7922
+ ...rest,
7921
7923
  ref: (node) => {
7922
7924
  outsideRef.current = node;
7923
7925
  floatingRef.current = node;
@@ -7928,7 +7930,7 @@ function PopoverContent({ children, className }) {
7928
7930
  positioned ? "fancy-scale-in" : "invisible",
7929
7931
  className
7930
7932
  ),
7931
- style: { left: position.x, top: position.y },
7933
+ style: { ...style, left: position.x, top: position.y },
7932
7934
  onMouseEnter: hover ? onHoverEnter : void 0,
7933
7935
  onMouseLeave: hover ? onHoverLeave : void 0,
7934
7936
  children
@@ -11342,6 +11344,31 @@ function useEditor() {
11342
11344
  }
11343
11345
  return ctx;
11344
11346
  }
11347
+ function EditorSourceToggle({
11348
+ className,
11349
+ icon = "</>",
11350
+ title = "Source",
11351
+ activeTitle = "Rich text"
11352
+ }) {
11353
+ const { showSource, setShowSource } = useEditor();
11354
+ return /* @__PURE__ */ jsxRuntime.jsx(
11355
+ "button",
11356
+ {
11357
+ type: "button",
11358
+ onClick: () => setShowSource(!showSource),
11359
+ title: showSource ? activeTitle : title,
11360
+ "aria-pressed": showSource,
11361
+ "data-react-fancy-editor-source-toggle": "",
11362
+ className: cn(
11363
+ "inline-flex h-8 items-center justify-center rounded-md px-2 font-mono text-xs transition-colors",
11364
+ showSource ? "bg-zinc-200 dark:bg-zinc-700" : "hover:bg-zinc-100 dark:hover:bg-zinc-800",
11365
+ className
11366
+ ),
11367
+ children: icon
11368
+ }
11369
+ );
11370
+ }
11371
+ EditorSourceToggle.displayName = "EditorSourceToggle";
11345
11372
  var DEFAULT_ACTIONS = [
11346
11373
  { icon: "B", label: "Bold", command: "bold" },
11347
11374
  { icon: "I", label: "Italic", command: "italic" },
@@ -11352,9 +11379,10 @@ function EditorToolbar({
11352
11379
  actions = DEFAULT_ACTIONS,
11353
11380
  onAction,
11354
11381
  children,
11355
- className
11382
+ className,
11383
+ showSourceToggle = true
11356
11384
  }) {
11357
- const { exec } = useEditor();
11385
+ const { exec, showSource } = useEditor();
11358
11386
  return /* @__PURE__ */ jsxRuntime.jsx(
11359
11387
  "div",
11360
11388
  {
@@ -11363,26 +11391,31 @@ function EditorToolbar({
11363
11391
  "flex items-center gap-0.5 border-b border-zinc-200 px-2 py-1.5 dark:border-zinc-700",
11364
11392
  className
11365
11393
  ),
11366
- children: children ?? actions.map((action) => /* @__PURE__ */ jsxRuntime.jsx(
11367
- "button",
11368
- {
11369
- type: "button",
11370
- onClick: () => {
11371
- if (onAction) {
11372
- onAction(action.command);
11373
- } else {
11374
- exec(action.command, action.commandArg);
11375
- }
11394
+ children: children ?? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11395
+ actions.map((action) => /* @__PURE__ */ jsxRuntime.jsx(
11396
+ "button",
11397
+ {
11398
+ type: "button",
11399
+ disabled: showSource,
11400
+ onClick: () => {
11401
+ if (onAction) {
11402
+ onAction(action.command);
11403
+ } else {
11404
+ exec(action.command, action.commandArg);
11405
+ }
11406
+ },
11407
+ title: action.label,
11408
+ className: cn(
11409
+ "inline-flex h-8 w-8 items-center justify-center rounded-md text-sm transition-colors",
11410
+ action.active ? "bg-zinc-200 dark:bg-zinc-700" : "hover:bg-zinc-100 dark:hover:bg-zinc-800",
11411
+ showSource && "cursor-not-allowed opacity-40 hover:bg-transparent"
11412
+ ),
11413
+ children: action.icon
11376
11414
  },
11377
- title: action.label,
11378
- className: cn(
11379
- "inline-flex h-8 w-8 items-center justify-center rounded-md text-sm transition-colors",
11380
- action.active ? "bg-zinc-200 dark:bg-zinc-700" : "hover:bg-zinc-100 dark:hover:bg-zinc-800"
11381
- ),
11382
- children: action.icon
11383
- },
11384
- `${action.command}-${action.commandArg ?? ""}`
11385
- ))
11415
+ `${action.command}-${action.commandArg ?? ""}`
11416
+ )),
11417
+ showSourceToggle && /* @__PURE__ */ jsxRuntime.jsx(EditorSourceToggle, { className: "ml-auto" })
11418
+ ] })
11386
11419
  }
11387
11420
  );
11388
11421
  }
@@ -11588,20 +11621,58 @@ function extensionEditorClasses(extensions) {
11588
11621
  ].join(" ");
11589
11622
  }).join(" ");
11590
11623
  }
11591
- function EditorContent({ className, maxHeight }) {
11592
- const { contentRef, lineSpacing, placeholder, extensions, _initialHtml, _onInput } = useEditor();
11593
- const initialized = react.useRef(false);
11624
+ function EditorContent({ className, maxHeight, sourceClassName }) {
11625
+ const {
11626
+ contentRef,
11627
+ lineSpacing,
11628
+ placeholder,
11629
+ extensions,
11630
+ outputFormat,
11631
+ showSource,
11632
+ _initialHtml,
11633
+ _onInput,
11634
+ _sourceValue,
11635
+ _onSourceInput
11636
+ } = useEditor();
11637
+ const seededHtml = react.useRef(null);
11594
11638
  react.useEffect(() => {
11639
+ if (showSource) {
11640
+ seededHtml.current = null;
11641
+ return;
11642
+ }
11595
11643
  const el = contentRef.current;
11596
- if (el && _initialHtml && !initialized.current) {
11644
+ if (el && seededHtml.current !== _initialHtml) {
11597
11645
  el.innerHTML = _initialHtml;
11598
- initialized.current = true;
11646
+ seededHtml.current = _initialHtml;
11599
11647
  }
11600
- }, [contentRef, _initialHtml]);
11648
+ }, [contentRef, _initialHtml, showSource]);
11601
11649
  const extClasses = react.useMemo(
11602
11650
  () => extensionEditorClasses(extensions),
11603
11651
  [extensions]
11604
11652
  );
11653
+ if (showSource) {
11654
+ return /* @__PURE__ */ jsxRuntime.jsx(
11655
+ "textarea",
11656
+ {
11657
+ "data-react-fancy-editor-source": "",
11658
+ value: _sourceValue,
11659
+ onChange: (e) => _onSourceInput(e.target.value),
11660
+ spellCheck: false,
11661
+ placeholder,
11662
+ "aria-label": `${outputFormat === "markdown" ? "Markdown" : "HTML"} source`,
11663
+ style: {
11664
+ maxHeight: maxHeight ? `${maxHeight}px` : void 0
11665
+ },
11666
+ className: cn(
11667
+ "block min-h-[120px] w-full resize-y px-4 py-3 font-mono text-xs leading-relaxed outline-none",
11668
+ "bg-zinc-50 text-zinc-800 focus:outline-none dark:bg-zinc-900 dark:text-zinc-200",
11669
+ "placeholder:text-zinc-400",
11670
+ maxHeight && "overflow-y-auto",
11671
+ sourceClassName
11672
+ )
11673
+ }
11674
+ );
11675
+ }
11605
11676
  return /* @__PURE__ */ jsxRuntime.jsx(
11606
11677
  "div",
11607
11678
  {
@@ -11793,20 +11864,29 @@ function EditorRoot({
11793
11864
  placeholder,
11794
11865
  mode: modeProp,
11795
11866
  extensions: instanceExtensions,
11796
- unsafe = false
11867
+ unsafe = false,
11868
+ showSource: showSourceProp,
11869
+ defaultShowSource = false,
11870
+ onShowSourceChange
11797
11871
  }) {
11798
11872
  const contentRef = react.useRef(null);
11799
11873
  const [value, setValue] = useControllableState(controlledValue, defaultValue, onChange);
11874
+ const [showSource, setShowSource] = useControllableState(
11875
+ showSourceProp,
11876
+ defaultShowSource,
11877
+ onShowSourceChange
11878
+ );
11800
11879
  const mode = useFieldMode(modeProp);
11801
11880
  const isView = mode === "view";
11802
11881
  const initialHtml = react.useMemo(
11803
11882
  () => toHtml(value, outputFormat, unsafe, valueFormat),
11804
- // Seed the contentEditable from the LIVE value when (re)entering edit mode.
11805
- // EditorContent reads this once on mount, and it only mounts in edit mode —
11806
- // so this captures the current value on view→edit, not a stale mount snapshot,
11807
- // and does NOT re-run on every keystroke.
11883
+ // Seed the contentEditable from the LIVE value when (re)entering edit mode
11884
+ // and when returning from the raw-source view, which may have rewritten the
11885
+ // value out from under the rich-text surface. EditorContent re-seeds when
11886
+ // this recomputes; it does NOT re-run on every keystroke (`value` is not a
11887
+ // dep, so typing in the rich-text surface never re-seeds it).
11808
11888
  // eslint-disable-next-line react-hooks/exhaustive-deps
11809
- [isView, outputFormat, unsafe, valueFormat]
11889
+ [isView, outputFormat, unsafe, valueFormat, showSource]
11810
11890
  );
11811
11891
  const extensions = react.useMemo(
11812
11892
  () => mergeExtensions(instanceExtensions),
@@ -11843,6 +11923,12 @@ function EditorRoot({
11843
11923
  },
11844
11924
  [handleInput]
11845
11925
  );
11926
+ const handleSourceInput = react.useCallback(
11927
+ (next) => {
11928
+ setValue(next);
11929
+ },
11930
+ [setValue]
11931
+ );
11846
11932
  const wrapSelection = react.useCallback(
11847
11933
  (before, after) => {
11848
11934
  const sel = window.getSelection();
@@ -11866,8 +11952,12 @@ function EditorRoot({
11866
11952
  lineSpacing,
11867
11953
  placeholder,
11868
11954
  extensions,
11955
+ showSource,
11956
+ setShowSource,
11869
11957
  _initialHtml: initialHtml,
11870
- _onInput: handleInput
11958
+ _onInput: handleInput,
11959
+ _sourceValue: value,
11960
+ _onSourceInput: handleSourceInput
11871
11961
  };
11872
11962
  if (isView) {
11873
11963
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -11911,7 +12001,8 @@ var ToolbarWithSeparator = Object.assign(EditorToolbar, {
11911
12001
  });
11912
12002
  var Editor = Object.assign(EditorRoot, {
11913
12003
  Toolbar: ToolbarWithSeparator,
11914
- Content: EditorContent
12004
+ Content: EditorContent,
12005
+ SourceToggle: EditorSourceToggle
11915
12006
  });
11916
12007
  var MenuContext = react.createContext(null);
11917
12008
  function useMenu() {