@particle-academy/react-fancy 4.13.1 → 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
@@ -11344,6 +11344,31 @@ function useEditor() {
11344
11344
  }
11345
11345
  return ctx;
11346
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";
11347
11372
  var DEFAULT_ACTIONS = [
11348
11373
  { icon: "B", label: "Bold", command: "bold" },
11349
11374
  { icon: "I", label: "Italic", command: "italic" },
@@ -11354,9 +11379,10 @@ function EditorToolbar({
11354
11379
  actions = DEFAULT_ACTIONS,
11355
11380
  onAction,
11356
11381
  children,
11357
- className
11382
+ className,
11383
+ showSourceToggle = true
11358
11384
  }) {
11359
- const { exec } = useEditor();
11385
+ const { exec, showSource } = useEditor();
11360
11386
  return /* @__PURE__ */ jsxRuntime.jsx(
11361
11387
  "div",
11362
11388
  {
@@ -11365,26 +11391,31 @@ function EditorToolbar({
11365
11391
  "flex items-center gap-0.5 border-b border-zinc-200 px-2 py-1.5 dark:border-zinc-700",
11366
11392
  className
11367
11393
  ),
11368
- children: children ?? actions.map((action) => /* @__PURE__ */ jsxRuntime.jsx(
11369
- "button",
11370
- {
11371
- type: "button",
11372
- onClick: () => {
11373
- if (onAction) {
11374
- onAction(action.command);
11375
- } else {
11376
- exec(action.command, action.commandArg);
11377
- }
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
11378
11414
  },
11379
- title: action.label,
11380
- className: cn(
11381
- "inline-flex h-8 w-8 items-center justify-center rounded-md text-sm transition-colors",
11382
- action.active ? "bg-zinc-200 dark:bg-zinc-700" : "hover:bg-zinc-100 dark:hover:bg-zinc-800"
11383
- ),
11384
- children: action.icon
11385
- },
11386
- `${action.command}-${action.commandArg ?? ""}`
11387
- ))
11415
+ `${action.command}-${action.commandArg ?? ""}`
11416
+ )),
11417
+ showSourceToggle && /* @__PURE__ */ jsxRuntime.jsx(EditorSourceToggle, { className: "ml-auto" })
11418
+ ] })
11388
11419
  }
11389
11420
  );
11390
11421
  }
@@ -11590,20 +11621,58 @@ function extensionEditorClasses(extensions) {
11590
11621
  ].join(" ");
11591
11622
  }).join(" ");
11592
11623
  }
11593
- function EditorContent({ className, maxHeight }) {
11594
- const { contentRef, lineSpacing, placeholder, extensions, _initialHtml, _onInput } = useEditor();
11595
- 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);
11596
11638
  react.useEffect(() => {
11639
+ if (showSource) {
11640
+ seededHtml.current = null;
11641
+ return;
11642
+ }
11597
11643
  const el = contentRef.current;
11598
- if (el && _initialHtml && !initialized.current) {
11644
+ if (el && seededHtml.current !== _initialHtml) {
11599
11645
  el.innerHTML = _initialHtml;
11600
- initialized.current = true;
11646
+ seededHtml.current = _initialHtml;
11601
11647
  }
11602
- }, [contentRef, _initialHtml]);
11648
+ }, [contentRef, _initialHtml, showSource]);
11603
11649
  const extClasses = react.useMemo(
11604
11650
  () => extensionEditorClasses(extensions),
11605
11651
  [extensions]
11606
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
+ }
11607
11676
  return /* @__PURE__ */ jsxRuntime.jsx(
11608
11677
  "div",
11609
11678
  {
@@ -11795,20 +11864,29 @@ function EditorRoot({
11795
11864
  placeholder,
11796
11865
  mode: modeProp,
11797
11866
  extensions: instanceExtensions,
11798
- unsafe = false
11867
+ unsafe = false,
11868
+ showSource: showSourceProp,
11869
+ defaultShowSource = false,
11870
+ onShowSourceChange
11799
11871
  }) {
11800
11872
  const contentRef = react.useRef(null);
11801
11873
  const [value, setValue] = useControllableState(controlledValue, defaultValue, onChange);
11874
+ const [showSource, setShowSource] = useControllableState(
11875
+ showSourceProp,
11876
+ defaultShowSource,
11877
+ onShowSourceChange
11878
+ );
11802
11879
  const mode = useFieldMode(modeProp);
11803
11880
  const isView = mode === "view";
11804
11881
  const initialHtml = react.useMemo(
11805
11882
  () => toHtml(value, outputFormat, unsafe, valueFormat),
11806
- // Seed the contentEditable from the LIVE value when (re)entering edit mode.
11807
- // EditorContent reads this once on mount, and it only mounts in edit mode —
11808
- // so this captures the current value on view→edit, not a stale mount snapshot,
11809
- // 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).
11810
11888
  // eslint-disable-next-line react-hooks/exhaustive-deps
11811
- [isView, outputFormat, unsafe, valueFormat]
11889
+ [isView, outputFormat, unsafe, valueFormat, showSource]
11812
11890
  );
11813
11891
  const extensions = react.useMemo(
11814
11892
  () => mergeExtensions(instanceExtensions),
@@ -11845,6 +11923,12 @@ function EditorRoot({
11845
11923
  },
11846
11924
  [handleInput]
11847
11925
  );
11926
+ const handleSourceInput = react.useCallback(
11927
+ (next) => {
11928
+ setValue(next);
11929
+ },
11930
+ [setValue]
11931
+ );
11848
11932
  const wrapSelection = react.useCallback(
11849
11933
  (before, after) => {
11850
11934
  const sel = window.getSelection();
@@ -11868,8 +11952,12 @@ function EditorRoot({
11868
11952
  lineSpacing,
11869
11953
  placeholder,
11870
11954
  extensions,
11955
+ showSource,
11956
+ setShowSource,
11871
11957
  _initialHtml: initialHtml,
11872
- _onInput: handleInput
11958
+ _onInput: handleInput,
11959
+ _sourceValue: value,
11960
+ _onSourceInput: handleSourceInput
11873
11961
  };
11874
11962
  if (isView) {
11875
11963
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -11913,7 +12001,8 @@ var ToolbarWithSeparator = Object.assign(EditorToolbar, {
11913
12001
  });
11914
12002
  var Editor = Object.assign(EditorRoot, {
11915
12003
  Toolbar: ToolbarWithSeparator,
11916
- Content: EditorContent
12004
+ Content: EditorContent,
12005
+ SourceToggle: EditorSourceToggle
11917
12006
  });
11918
12007
  var MenuContext = react.createContext(null);
11919
12008
  function useMenu() {