@particle-academy/react-fancy 4.13.1 → 4.14.1

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() {
@@ -12874,6 +12963,7 @@ function TreeNode({ node, depth }) {
12874
12963
  indentSize,
12875
12964
  showIcons,
12876
12965
  draggable,
12966
+ dragCursor,
12877
12967
  dragState,
12878
12968
  setDragState,
12879
12969
  onNodeMove,
@@ -13004,7 +13094,12 @@ function TreeNode({ node, depth }) {
13004
13094
  isSelected ? "bg-blue-500/15 text-blue-600 dark:text-blue-400" : "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800",
13005
13095
  node.disabled && "pointer-events-none opacity-40",
13006
13096
  isDragging && "opacity-50",
13007
- canDrag && "cursor-grab active:cursor-grabbing",
13097
+ // Grab affordance. By default (`dragCursor="none"`) the row keeps its
13098
+ // normal click cursor on hover — no open-hand "move me" cursor before
13099
+ // any drag — and only shows the closed-hand `grabbing` cursor while a
13100
+ // drag is actually underway (mousedown/drag). `dragCursor="grab"` opts
13101
+ // back into the always-on grab cursor for whole-row drag handles (#14).
13102
+ canDrag && (dragCursor === "grab" ? "cursor-grab active:cursor-grabbing" : "active:cursor-grabbing"),
13008
13103
  isDropTarget && dropPosition === "inside" && "bg-blue-500/10 ring-1 ring-blue-500/30 ring-inset"
13009
13104
  ),
13010
13105
  style: { paddingLeft },
@@ -13060,6 +13155,7 @@ function TreeNavRoot({
13060
13155
  defaultExpandAll = false,
13061
13156
  indentSize = 16,
13062
13157
  showIcons = true,
13158
+ dragCursor = "none",
13063
13159
  className
13064
13160
  }) {
13065
13161
  const [internalExpanded, setInternalExpanded] = react.useState(() => {
@@ -13104,6 +13200,7 @@ function TreeNavRoot({
13104
13200
  indentSize,
13105
13201
  showIcons,
13106
13202
  draggable,
13203
+ dragCursor,
13107
13204
  dragState,
13108
13205
  setDragState,
13109
13206
  onNodeMove,
@@ -13121,6 +13218,7 @@ function TreeNavRoot({
13121
13218
  indentSize,
13122
13219
  showIcons,
13123
13220
  draggable,
13221
+ dragCursor,
13124
13222
  dragState,
13125
13223
  onNodeMove,
13126
13224
  acceptExternalDrops,