@bloklabs/react 1.2.6 → 1.4.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/types/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
- import type { BlokConfig, BlockToolData, OutputBlockData, OutputData } from '@bloklabs/core';
2
- import type { BlockAPI, BlockToolConstructable, ToolboxConfig } from '@bloklabs/core';
1
+ import type { BlokConfig, BlockToolData, OutputBlockData, OutputData, LooseOutputData } from '@bloklabs/core';
2
+ import type { BlocksToHtmlOptions } from '@bloklabs/core/view';
3
+ import type { BlockAPI, BlockToolConstructable, ToolboxConfigEntry } from '@bloklabs/core';
3
4
  import type { InlineToolConstructable, SanitizerConfig } from '@bloklabs/core';
5
+ import type { API, MarkSnapshot, MarkSpec } from '@bloklabs/core';
4
6
  import type { Blok, EditorWidth, BlockRenderedPayload, BlocksRenderedPayload } from '@bloklabs/core';
5
7
  import type { BlockTuneData } from '@bloklabs/core';
6
8
  import type { MarkdownImportConfig } from '@bloklabs/core/markdown';
@@ -11,11 +13,21 @@ import type React from 'react';
11
13
  * Accepts all BlokConfig properties except `holder`, plus a React-only `width` prop.
12
14
  *
13
15
  * Reactive props (sync after mount without recreation):
14
- * - `readOnly` — calls `editor.readOnly.set(value)`
16
+ * - `readOnly` — calls `editor.readOnly.set(value)`; the object form also
17
+ * syncs `hideControls` via `editor.readOnly.set(enabled, { hideControls })`
18
+ * - `hideToolbar` — calls `editor.toolbar.setHidden(value)`
19
+ * - `inlineToolbar` — calls `editor.tools.setInlineToolbar(value)` (arrays
20
+ * are compared by content, so a re-created literal is a no-op)
15
21
  * - `autofocus` — calls `editor.focus()` when changed to true
16
22
  * - `theme` — calls `editor.theme.set(value)`
17
23
  * - `width` — calls `editor.width.set(value)`
18
24
  * - `placeholder` — calls `editor.placeholder.set(value)`
25
+ * - `style.tokens` — calls `editor.tokens.set(value)` (deep-equal–deduped;
26
+ * replace semantics, so pass the complete token set on each change)
27
+ * - `i18n` — calls `editor.i18n.update({ locale, messages, direction })`
28
+ * (deep-equal–deduped; relabels the editor chrome in place, so caret, focus
29
+ * and undo history survive a language switch). `defaultLocale` is not
30
+ * forwarded — it only affects the initial locale resolution.
19
31
  * - `data` — re-renders via `editor.render(value)` when the content changes
20
32
  * (deep-equal–deduped and serialized; seeds the initial content at creation)
21
33
  *
@@ -541,6 +553,43 @@ export interface UseBlocksApi {
541
553
  */
542
554
  export declare function useBlocks(editor: Blok | null): UseBlocksApi;
543
555
 
556
+ // ---------------------------------------------------------------------------
557
+ // Scoped readiness (useBlokReady)
558
+ // ---------------------------------------------------------------------------
559
+
560
+ /** Options accepted by {@link useBlokReady}. */
561
+ export interface UseBlokReadyOptions {
562
+ /**
563
+ * Restrict the wait to editors mounted inside this element. Accepts the ref
564
+ * you already hold on the container; it is re-read on every readiness
565
+ * change, so a ref that attaches after the first render is picked up.
566
+ *
567
+ * Omit it to observe every editor on the page.
568
+ */
569
+ within?: React.RefObject<Element | null> | Element | null;
570
+ /**
571
+ * `'ready'` (default) settles when each editor has finished booting.
572
+ * `'rendered'` also waits for its content to be in the DOM and re-arms on
573
+ * every post-boot re-render.
574
+ */
575
+ settleOn?: 'ready' | 'rendered';
576
+ }
577
+
578
+ /**
579
+ * Live readiness of the Blok editors in a DOM scope.
580
+ *
581
+ * Returns `false` until every editor inside `within` is settled, and keeps
582
+ * following the scope afterwards: editors mounted later re-close the gate, and
583
+ * with `settleOn: 'rendered'` so does each re-render. A scope holding no
584
+ * editors is ready, so callers need no "nothing to wait for" special case.
585
+ *
586
+ * The first render always reports `false` — the scope element is not attached
587
+ * yet, and over-waiting is safe while under-waiting is a bug.
588
+ *
589
+ * @param options - scope and readiness depth
590
+ */
591
+ export declare function useBlokReady(options?: UseBlokReadyOptions): boolean;
592
+
544
593
  // ---------------------------------------------------------------------------
545
594
  // Block authoring (createReactBlock + portal host)
546
595
  // ---------------------------------------------------------------------------
@@ -587,15 +636,46 @@ export interface ReactBlockRenderProps<Data, Config = Record<string, unknown>> {
587
636
  BlockChildren: React.ComponentType;
588
637
  }
589
638
 
639
+ /**
640
+ * Props handed to a React block's `viewComponent` (the read-only renderer):
641
+ * the entry props minus `commit` (no write path) and `readOnly` (always true).
642
+ */
643
+ export type ReactBlockViewProps<Data, Config = Record<string, unknown>> = Omit<
644
+ ReactBlockRenderProps<Data, Config>,
645
+ 'commit' | 'readOnly'
646
+ >;
647
+
648
+ /** A toolbox entry whose `icon` may be a React element instead of an SVG string. */
649
+ export type ReactToolboxConfigEntry = Omit<ToolboxConfigEntry, 'icon'> & {
650
+ /**
651
+ * Toolbox icon. Accepts the markup string core consumes directly, or the same
652
+ * React element rendered in the block body — the factory serializes elements
653
+ * to markup once (lazily, cached), so icons are never duplicated as parallel
654
+ * raw SVG strings. Without a DOM (SSR) the element is returned as-is and
655
+ * serialization retries on the next browser-side access.
656
+ */
657
+ icon?: string | React.ReactElement;
658
+ };
659
+
660
+ /** Toolbox config for React blocks: single entry or several variants. */
661
+ export type ReactToolboxConfig = ReactToolboxConfigEntry | ReactToolboxConfigEntry[];
662
+
590
663
  /** Spec for {@link createReactBlock}. */
591
664
  export interface CreateReactBlockSpec<
592
665
  Data extends BlockToolData = BlockToolData,
593
666
  Config = Record<string, unknown>
594
667
  > {
595
668
  type: string;
596
- toolbox?: ToolboxConfig;
669
+ toolbox?: ReactToolboxConfig;
597
670
  propSchema: PropSchema;
598
671
  component: React.ComponentType<ReactBlockRenderProps<Data, Config>>;
672
+ /**
673
+ * Optional read-only renderer: rendered INSTEAD of `component` while the
674
+ * editor is read-only, so blocks need no display-vs-edit ternary. Toggling
675
+ * read-only swaps renderers (ephemeral state does not survive the swap);
676
+ * omit it to keep the single-component in-place toggle.
677
+ */
678
+ viewComponent?: React.ComponentType<ReactBlockViewProps<Data, Config>>;
599
679
  onRendered?: (block: BlockAPI) => void;
600
680
  onMoved?: (block: BlockAPI) => void;
601
681
  onRemoved?: (block: BlockAPI) => void;
@@ -644,21 +724,86 @@ export interface ReactInlineToolRenderProps<Config = Record<string, unknown>> {
644
724
  config: Readonly<Partial<Config>>;
645
725
  }
646
726
 
727
+ /** The spec's mark operations, bound to the live selection via `api.marks`. */
728
+ export interface InlineToolMarkOps<State = void> {
729
+ /** Whether the whole selection currently carries the mark. */
730
+ has(): boolean;
731
+ /** Toggle the mark on the selection; returns true when now applied. */
732
+ toggle(state?: State): boolean;
733
+ /** Apply (or update in place) the mark on the selection. */
734
+ apply(state?: State): HTMLElement[];
735
+ /** Remove the mark from the selection. */
736
+ remove(): HTMLElement[];
737
+ /** Read the mark's current declared values at the selection. */
738
+ read(): MarkSnapshot | null;
739
+ }
740
+
741
+ /**
742
+ * Everything a React inline tool's component (and any component nested in it —
743
+ * swatches, popovers) can reach via {@link useInlineTool} without prop-drilling.
744
+ */
745
+ export interface InlineToolHandle<Config = Record<string, unknown>, State = void> {
746
+ /** Live active state, same value as the component's `active` prop. */
747
+ active: boolean;
748
+ /** The tool's config, same value as the component's `config` prop. */
749
+ config: Readonly<Partial<Config>>;
750
+ /** The editor API handed to the tool's constructor (undefined outside core). */
751
+ api: API | undefined;
752
+ /**
753
+ * Operations bound to the spec's `mark` — null when the spec declares no
754
+ * mark or the editor api provides no `marks` surface.
755
+ */
756
+ mark: InlineToolMarkOps<State> | null;
757
+ }
758
+
759
+ /**
760
+ * Reach the enclosing React inline tool from any component inside its
761
+ * portaled UI: live `active` state, the tool `config`, the editor `api`, and
762
+ * the spec's mark operations bound to the live selection.
763
+ */
764
+ export declare function useInlineTool<Config = Record<string, unknown>, State = void>(): InlineToolHandle<Config, State>;
765
+
647
766
  /** Spec for {@link createReactInlineTool}. */
648
- export interface CreateReactInlineToolSpec<Config = Record<string, unknown>> {
767
+ export interface CreateReactInlineToolSpec<Config = Record<string, unknown>, State = void> {
649
768
  /** Tool type name — the MenuConfig item name (fallback when the tools-map key is unavailable). */
650
769
  type: string;
651
770
  /** Optional toolbar item title (shown in overflow/search contexts). */
652
771
  title?: string;
772
+ /**
773
+ * Translation key for the toolbar label, resolved by core i18n as
774
+ * `toolNames.{titleKey}` (or used verbatim when it contains a dot). Without
775
+ * it a custom tool's label is only localizable through the legacy
776
+ * capitalized-tool-name fallback.
777
+ */
778
+ titleKey?: string;
653
779
  /** The component rendered as the tool's toolbar icon/UI. */
654
780
  component: React.ComponentType<ReactInlineToolRenderProps<Config>>;
655
- /** Applies/removes the formatting on the LIVE selection's range (captured at activation time). */
656
- surround?: (range: Range) => void;
657
- /** Reports whether the formatting is active for the given selection. */
658
- checkState?: (selection: Selection | null) => boolean;
781
+ /**
782
+ * Declarative description of the wrapper this tool produces. When present,
783
+ * `surround` (toggle), `checkState` (whole-range coverage) and `sanitize`
784
+ * are derived from it — range-aware splitting, adjacent-run semantics and
785
+ * the trailing-whitespace fix come from the editor's mark engine
786
+ * (`api.marks`) with no DOM plumbing in the tool.
787
+ */
788
+ mark?: MarkSpec<State>;
789
+ /**
790
+ * Applies/removes the formatting on the LIVE selection's range (captured at
791
+ * activation time). Receives the editor API as its second argument.
792
+ * Takes precedence over the `mark` derivation.
793
+ */
794
+ surround?: (range: Range, api: API | undefined) => void;
795
+ /**
796
+ * Reports whether the formatting is active for the given selection.
797
+ * Receives the editor API as its second argument.
798
+ * Takes precedence over the `mark` derivation.
799
+ */
800
+ checkState?: (selection: Selection | null, api: API | undefined) => boolean;
659
801
  /** Keyboard shortcut (e.g. `CMD+SHIFT+C`). */
660
802
  shortcut?: string;
661
- /** Inline sanitizer config declaring the markup this tool produces. */
803
+ /**
804
+ * Inline sanitizer config declaring the markup this tool produces.
805
+ * Defaults to the config derived from `mark` when one is declared.
806
+ */
662
807
  sanitize?: SanitizerConfig;
663
808
  /** Whether the tool stays available in read-only mode. */
664
809
  isReadOnlySupported?: boolean;
@@ -680,13 +825,12 @@ export interface CreateReactInlineToolSpec<Config = Record<string, unknown>> {
680
825
  * type: 'descriptionColor',
681
826
  * title: 'Text color',
682
827
  * component: ({ active }) => <FontColorIcon active={active} />,
683
- * surround: (range) => applyColor(range),
684
- * checkState: (selection) => hasColor(selection),
828
+ * mark: { tag: 'span', className: 'hl-description' },
685
829
  * });
686
830
  * ```
687
831
  */
688
- export declare function createReactInlineTool<Config = Record<string, unknown>>(
689
- spec: CreateReactInlineToolSpec<Config>
832
+ export declare function createReactInlineTool<Config = Record<string, unknown>, State = void>(
833
+ spec: CreateReactInlineToolSpec<Config, State>
690
834
  ): InlineToolConstructable;
691
835
 
692
836
  /**
@@ -745,3 +889,49 @@ export interface BlockPortalHostProps {
745
889
  * for advanced setups that manage the registry themselves.
746
890
  */
747
891
  export declare function BlockPortalHost(props: BlockPortalHostProps): React.ReactElement;
892
+
893
+ /**
894
+ * Props for {@link BlokView}.
895
+ *
896
+ * @experimental Built on the `@experimental` view tree of
897
+ * `@bloklabs/core/view` — not frozen until a second framework adapter
898
+ * consumes it.
899
+ */
900
+ export interface BlokViewProps {
901
+ /** Saved document to display (strict or loose wire shape; nullish tolerated). */
902
+ data: OutputData | LooseOutputData | null | undefined;
903
+ /** View schema from `defineBlokSchema` — display under the composition that produced the document. */
904
+ schema?: BlocksToHtmlOptions['schema'];
905
+ /** Custom per-tool renderers; win over built-ins. */
906
+ renderers?: BlocksToHtmlOptions['renderers'];
907
+ /** Unknown-tool policy (default 'skip'). */
908
+ onUnknownBlock?: BlocksToHtmlOptions['onUnknownBlock'];
909
+ /** Class for the single wrapper div. */
910
+ className?: string;
911
+ }
912
+
913
+ /**
914
+ * Display a saved Blok document synchronously — one `<div>` wrapper, no
915
+ * editor chrome, no ids, no async, no effects. Content is mapped from the
916
+ * sanitized view tree to real React elements, so it renders identically
917
+ * under SSR.
918
+ *
919
+ * @experimental Not frozen until a second framework adapter consumes the
920
+ * underlying view tree.
921
+ */
922
+ export declare function BlokView(props: BlokViewProps): React.ReactNode;
923
+
924
+ /**
925
+ * Render a saved Blok document to React elements with NO wrapper element —
926
+ * a Fragment of the block elements, for placement inside labels, table
927
+ * cells, and other slots where an extra `<div>` is invalid or unwanted.
928
+ * Synchronous, effect-free (SSR-safe), memoized on the data reference and
929
+ * the individual option values.
930
+ *
931
+ * @experimental Not frozen until a second framework adapter consumes the
932
+ * underlying view tree.
933
+ */
934
+ export declare function useBlokView(
935
+ data: OutputData | LooseOutputData | null | undefined,
936
+ options?: BlocksToHtmlOptions
937
+ ): React.ReactNode;