@inshapardaz/likhari-react 0.1.13 → 0.1.14-dev.115

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.
@@ -1 +1 @@
1
- {"version":3,"file":"EditorRoot.d.ts","sourceRoot":"","sources":["../src/EditorRoot.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAY1D,OAAO,KAAK,EAA8B,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAEjF,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AACvF,OAAO,EAAwB,KAAK,mBAAmB,EAAE,KAAK,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAKzH,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAGjE,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAYhF,OAAO,EAAgC,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAMnE,yEAAyE;AACzE,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,YAAY,GAAG,KAAK,CAAC;AAEnE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,QAAQ,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,oBAAoB,CAAC;IACtC,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAClD;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,KAAK,IAAI,CAAC;IACrD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC;;;;;;;;;;;;;;;OAeG;IACH,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,uGAAuG;IACvG,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3E,6EAA6E;IAC7E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAC;IACrC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IAClD,iBAAiB,IAAI,OAAO,CAAC;IAC7B,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,KAAK,IAAI,IAAI,CAAC;CACf;AAQD,eAAO,MAAM,UAAU,uGAqXrB,CAAC"}
1
+ {"version":3,"file":"EditorRoot.d.ts","sourceRoot":"","sources":["../src/EditorRoot.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAY1D,OAAO,KAAK,EAA8B,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAEjF,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AACvF,OAAO,EAAwB,KAAK,mBAAmB,EAAE,KAAK,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAKzH,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAIjE,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAYhF,OAAO,EAAgC,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAMnE,yEAAyE;AACzE,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,YAAY,GAAG,KAAK,CAAC;AAEnE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,QAAQ,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,oBAAoB,CAAC;IACtC,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAClD;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,KAAK,IAAI,CAAC;IACrD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC;;;;;;;;;;;;;;;OAeG;IACH,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,uGAAuG;IACvG,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3E,6EAA6E;IAC7E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAC;IACrC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IAClD,iBAAiB,IAAI,OAAO,CAAC;IAC7B,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,KAAK,IAAI,IAAI,CAAC;CACf;AAQD,eAAO,MAAM,UAAU,uGAkXrB,CAAC"}
@@ -21,6 +21,7 @@ import { Toolbar } from './components/Toolbar.js';
21
21
  import { injectUrduWebFontsCss } from './fonts/index.js';
22
22
  import { LinkPastePlugin } from './plugins/LinkPastePlugin.js';
23
23
  import { PageBreakPlugin } from './plugins/PageBreakPlugin.js';
24
+ import { LayoutPlugin } from './plugins/LayoutPlugin.js';
24
25
  import { DraftRestore } from './components/DraftRestore.js';
25
26
  import { LeaveDialog } from './components/LeaveDialog.js';
26
27
  import { clearDraft, createDraftId, hasDraftContent, pruneDrafts, writeDraft, } from './persistence/draftStorage.js';
@@ -277,7 +278,7 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialCo
277
278
  const portalTargetSelector = `#${CSS.escape(portalTargetId)}`;
278
279
  return (_jsx(EditorThemeProvider, { theme: theme, colorScheme: colorScheme, accentColor: accentColor, scopeElementId: portalTargetId, children: _jsxs("div", { className: "likhari-root", dir: dir, ref: rootElementRef, "data-document-id": documentId, style: { height: typeof height === 'number' ? `${height}px` : height }, children: [_jsx("div", { id: portalTargetId, className: "likhari-portal-target" }), _jsx(PortalTargetContext.Provider, { value: portalTargetSelector, children: _jsxs(UiStringsContext.Provider, { value: strings, children: [_jsx(ImageOptionsContext.Provider, { value: imageOptions, children: _jsxs(LexicalComposer, { initialConfig: initialConfig, children: [_jsx(Toolbar, { config: config, onSave: handleSave, isDirty: isDirty, showSave: showSave, fontOptions: fontOptions, direction: dir, locale: locale, drafts: autosave
279
280
  ? { currentId: draftId, maxBytes: autosaveMaxBytes, adoptOnRestore: !documentId, onRestored: handleDraftRestored }
280
- : undefined }), documentId && (_jsx(DraftRestore, { draftId: documentId, mode: restoreDraft, initialJson: initialContentJsonRef.current, locale: locale, onRestored: handleDraftRestored })), _jsx("div", { className: "likhari-canvas", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "likhari-content-editable", dir: dir, "aria-label": strings.editor.contentLabel }), placeholder: _jsx("div", { className: "likhari-placeholder", children: resolvedPlaceholder }), ErrorBoundary: LexicalErrorBoundary }) }), config.history && _jsx(HistoryPlugin, {}), (config.lists.bullet || config.lists.numbered || config.lists.check) && _jsx(ListPlugin, {}), config.lists.check && _jsx(CheckListPlugin, {}), config.links && _jsx(LinkPlugin, {}), config.links && _jsx(LinkPastePlugin, {}), config.blocks.pageBreak && _jsx(PageBreakPlugin, {}), config.tables && _jsx(TablePlugin, { hasCellMerge: false, hasTabHandler: true }), config.blocks.horizontalRule && _jsx(HorizontalRulePlugin, {}), _jsx(EditorInstancePlugin, { instanceRef: editorInstanceRef }), _jsx(OnChangePlugin, { onChange: handleChange })] }) }), _jsx(LeaveDialog, { opened: leavePrompt !== null, canSave: Boolean(onSave), canSaveDraft: autosave || navigationGuard === 'save-draft', draftFailed: leavePrompt?.draftFailed ?? false, onSave: () => {
281
+ : undefined }), documentId && (_jsx(DraftRestore, { draftId: documentId, mode: restoreDraft, initialJson: initialContentJsonRef.current, locale: locale, onRestored: handleDraftRestored })), _jsx("div", { className: "likhari-canvas", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "likhari-content-editable", dir: dir, "aria-label": strings.editor.contentLabel }), placeholder: _jsx("div", { className: "likhari-placeholder", children: resolvedPlaceholder }), ErrorBoundary: LexicalErrorBoundary }) }), config.history && _jsx(HistoryPlugin, {}), (config.lists.bullet || config.lists.numbered || config.lists.check) && _jsx(ListPlugin, {}), config.lists.check && _jsx(CheckListPlugin, {}), config.links && _jsx(LinkPlugin, {}), config.links && _jsx(LinkPastePlugin, {}), config.blocks.pageBreak && _jsx(PageBreakPlugin, {}), config.columns && _jsx(LayoutPlugin, {}), config.tables && _jsx(TablePlugin, { hasCellMerge: true, hasTabHandler: true }), config.blocks.horizontalRule && _jsx(HorizontalRulePlugin, {}), _jsx(EditorInstancePlugin, { instanceRef: editorInstanceRef }), _jsx(OnChangePlugin, { onChange: handleChange })] }) }), _jsx(LeaveDialog, { opened: leavePrompt !== null, canSave: Boolean(onSave), canSaveDraft: autosave || navigationGuard === 'save-draft', draftFailed: leavePrompt?.draftFailed ?? false, onSave: () => {
281
282
  handleSave();
282
283
  settleLeave(true);
283
284
  }, onSaveDraft: () => {
@@ -0,0 +1,54 @@
1
+ import { type DOMConversionMap, type DOMExportOutput, type EditorConfig, type LexicalCommand, type LexicalNode, type NodeKey, type SerializedElementNode, type Spread, ElementNode } from 'lexical';
2
+ /** Payload: how many (equal-width) columns to create. Ratio support beyond
3
+ * equal widths is deferred (lexical-editor-spec.md §4.9 mentions it as a
4
+ * "configurable" possibility, not a committed requirement for Phase 3). */
5
+ export declare const INSERT_LAYOUT_COMMAND: LexicalCommand<{
6
+ columnCount: number;
7
+ }>;
8
+ export type SerializedLayoutContainerNode = Spread<{
9
+ templateColumns: string;
10
+ }, SerializedElementNode>;
11
+ export type SerializedLayoutItemNode = SerializedElementNode;
12
+ /** A multi-column row (editor-architecture-design.md §3.2): an ElementNode
13
+ * holding one LayoutItemNode per column, laid out with CSS grid. Reused as
14
+ * the structural basis for two-column poetry layouts (spec §4.11). */
15
+ export declare class LayoutContainerNode extends ElementNode {
16
+ __templateColumns: string;
17
+ constructor(templateColumns?: string, key?: NodeKey);
18
+ static getType(): string;
19
+ static clone(node: LayoutContainerNode): LayoutContainerNode;
20
+ getTemplateColumns(): string;
21
+ setTemplateColumns(templateColumns: string): this;
22
+ static importJSON(serializedNode: SerializedLayoutContainerNode): LayoutContainerNode;
23
+ exportJSON(): SerializedLayoutContainerNode;
24
+ static importDOM(): DOMConversionMap | null;
25
+ exportDOM(): DOMExportOutput;
26
+ createDOM(config: EditorConfig): HTMLElement;
27
+ updateDOM(prevNode: LayoutContainerNode, dom: HTMLElement): boolean;
28
+ canBeEmpty(): false;
29
+ isShadowRoot(): boolean;
30
+ }
31
+ export declare function $createLayoutContainerNode(templateColumns?: string): LayoutContainerNode;
32
+ export declare function $isLayoutContainerNode(node: LexicalNode | null | undefined): node is LayoutContainerNode;
33
+ /** One column's content area inside a LayoutContainerNode. Always keeps at
34
+ * least an empty paragraph (mirrors TableCellNode), so a column can never
35
+ * collapse to an unselectable, unfocusable void. */
36
+ export declare class LayoutItemNode extends ElementNode {
37
+ static getType(): string;
38
+ static clone(node: LayoutItemNode): LayoutItemNode;
39
+ static importJSON(): LayoutItemNode;
40
+ exportJSON(): SerializedLayoutItemNode;
41
+ static importDOM(): DOMConversionMap | null;
42
+ exportDOM(): DOMExportOutput;
43
+ createDOM(config: EditorConfig): HTMLElement;
44
+ updateDOM(): boolean;
45
+ canBeEmpty(): false;
46
+ canIndent(): false;
47
+ }
48
+ export declare function $createLayoutItemNode(): LayoutItemNode;
49
+ export declare function $isLayoutItemNode(node: LexicalNode | null | undefined): node is LayoutItemNode;
50
+ /** Builds a container with `columnCount` empty items — the shape every
51
+ * LAYOUT insert produces, and what the node-transform below re-normalizes
52
+ * a malformed tree back toward. */
53
+ export declare function $createLayoutWithColumns(columnCount: number): LayoutContainerNode;
54
+ //# sourceMappingURL=LayoutNode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LayoutNode.d.ts","sourceRoot":"","sources":["../../src/blocks/LayoutNode.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,qBAAqB,EAC1B,KAAK,MAAM,EAEX,WAAW,EACZ,MAAM,SAAS,CAAC;AAGjB;;2EAE2E;AAC3E,eAAO,MAAM,qBAAqB,EAAE,cAAc,CAAC;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,CAA0C,CAAC;AAErH,MAAM,MAAM,6BAA6B,GAAG,MAAM,CAAC;IAAE,eAAe,EAAE,MAAM,CAAA;CAAE,EAAE,qBAAqB,CAAC,CAAC;AACvG,MAAM,MAAM,wBAAwB,GAAG,qBAAqB,CAAC;AAE7D;;sEAEsE;AACtE,qBAAa,mBAAoB,SAAQ,WAAW;IAClD,iBAAiB,EAAE,MAAM,CAAC;gBAEd,eAAe,SAAmB,EAAE,GAAG,CAAC,EAAE,OAAO;IAK7D,MAAM,CAAC,OAAO,IAAI,MAAM;IAIxB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,mBAAmB,GAAG,mBAAmB;IAI5D,kBAAkB,IAAI,MAAM;IAI5B,kBAAkB,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI;IAMjD,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,6BAA6B,GAAG,mBAAmB;IAIrF,UAAU,IAAI,6BAA6B;IAI3C,MAAM,CAAC,SAAS,IAAI,gBAAgB,GAAG,IAAI;IAU3C,SAAS,IAAI,eAAe;IAQ5B,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW;IAO5C,SAAS,CAAC,QAAQ,EAAE,mBAAmB,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO;IAOnE,UAAU,IAAI,KAAK;IAInB,YAAY,IAAI,OAAO;CAGxB;AAED,wBAAgB,0BAA0B,CAAC,eAAe,SAAmB,GAAG,mBAAmB,CAElG;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,IAAI,mBAAmB,CAExG;AAED;;oDAEoD;AACpD,qBAAa,cAAe,SAAQ,WAAW;IAC7C,MAAM,CAAC,OAAO,IAAI,MAAM;IAIxB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc;IAIlD,MAAM,CAAC,UAAU,IAAI,cAAc;IAInC,UAAU,IAAI,wBAAwB;IAItC,MAAM,CAAC,SAAS,IAAI,gBAAgB,GAAG,IAAI;IAS3C,SAAS,IAAI,eAAe;IAM5B,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW;IAM5C,SAAS,IAAI,OAAO;IAIpB,UAAU,IAAI,KAAK;IAInB,SAAS,IAAI,KAAK;CAGnB;AAED,wBAAgB,qBAAqB,IAAI,cAAc,CAEtD;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,IAAI,cAAc,CAE9F;AAED;;mCAEmC;AACnC,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,CAOjF"}
@@ -0,0 +1,138 @@
1
+ import { $applyNodeReplacement, $createParagraphNode, createCommand, ElementNode, } from 'lexical';
2
+ import { addClassNamesToElement } from '@lexical/utils';
3
+ /** Payload: how many (equal-width) columns to create. Ratio support beyond
4
+ * equal widths is deferred (lexical-editor-spec.md §4.9 mentions it as a
5
+ * "configurable" possibility, not a committed requirement for Phase 3). */
6
+ export const INSERT_LAYOUT_COMMAND = createCommand('INSERT_LAYOUT_COMMAND');
7
+ /** A multi-column row (editor-architecture-design.md §3.2): an ElementNode
8
+ * holding one LayoutItemNode per column, laid out with CSS grid. Reused as
9
+ * the structural basis for two-column poetry layouts (spec §4.11). */
10
+ export class LayoutContainerNode extends ElementNode {
11
+ constructor(templateColumns = 'repeat(2, 1fr)', key) {
12
+ super(key);
13
+ this.__templateColumns = templateColumns;
14
+ }
15
+ static getType() {
16
+ return 'layout-container';
17
+ }
18
+ static clone(node) {
19
+ return new LayoutContainerNode(node.__templateColumns, node.__key);
20
+ }
21
+ getTemplateColumns() {
22
+ return this.getLatest().__templateColumns;
23
+ }
24
+ setTemplateColumns(templateColumns) {
25
+ const writable = this.getWritable();
26
+ writable.__templateColumns = templateColumns;
27
+ return writable;
28
+ }
29
+ static importJSON(serializedNode) {
30
+ return $createLayoutContainerNode(serializedNode.templateColumns);
31
+ }
32
+ exportJSON() {
33
+ return { ...super.exportJSON(), type: 'layout-container', version: 1, templateColumns: this.getTemplateColumns() };
34
+ }
35
+ static importDOM() {
36
+ return {
37
+ div: (domNode) => {
38
+ if (!domNode.hasAttribute('data-likhari-layout-container'))
39
+ return null;
40
+ const templateColumns = domNode.style.gridTemplateColumns || undefined;
41
+ return { conversion: () => ({ node: $createLayoutContainerNode(templateColumns) }), priority: 2 };
42
+ },
43
+ };
44
+ }
45
+ exportDOM() {
46
+ const element = document.createElement('div');
47
+ element.setAttribute('data-likhari-layout-container', '');
48
+ element.style.display = 'grid';
49
+ element.style.gridTemplateColumns = this.getTemplateColumns();
50
+ return { element };
51
+ }
52
+ createDOM(config) {
53
+ const element = document.createElement('div');
54
+ addClassNamesToElement(element, config.theme.layoutContainer ?? 'likhari-layout-container');
55
+ element.style.gridTemplateColumns = this.__templateColumns;
56
+ return element;
57
+ }
58
+ updateDOM(prevNode, dom) {
59
+ if (prevNode.__templateColumns !== this.__templateColumns) {
60
+ dom.style.gridTemplateColumns = this.__templateColumns;
61
+ }
62
+ return false;
63
+ }
64
+ canBeEmpty() {
65
+ return false;
66
+ }
67
+ isShadowRoot() {
68
+ return true;
69
+ }
70
+ }
71
+ export function $createLayoutContainerNode(templateColumns = 'repeat(2, 1fr)') {
72
+ return $applyNodeReplacement(new LayoutContainerNode(templateColumns));
73
+ }
74
+ export function $isLayoutContainerNode(node) {
75
+ return node instanceof LayoutContainerNode;
76
+ }
77
+ /** One column's content area inside a LayoutContainerNode. Always keeps at
78
+ * least an empty paragraph (mirrors TableCellNode), so a column can never
79
+ * collapse to an unselectable, unfocusable void. */
80
+ export class LayoutItemNode extends ElementNode {
81
+ static getType() {
82
+ return 'layout-item';
83
+ }
84
+ static clone(node) {
85
+ return new LayoutItemNode(node.__key);
86
+ }
87
+ static importJSON() {
88
+ return $createLayoutItemNode();
89
+ }
90
+ exportJSON() {
91
+ return { ...super.exportJSON(), type: 'layout-item', version: 1 };
92
+ }
93
+ static importDOM() {
94
+ return {
95
+ div: (domNode) => {
96
+ if (!domNode.hasAttribute('data-likhari-layout-item'))
97
+ return null;
98
+ return { conversion: () => ({ node: $createLayoutItemNode() }), priority: 2 };
99
+ },
100
+ };
101
+ }
102
+ exportDOM() {
103
+ const element = document.createElement('div');
104
+ element.setAttribute('data-likhari-layout-item', '');
105
+ return { element };
106
+ }
107
+ createDOM(config) {
108
+ const element = document.createElement('div');
109
+ addClassNamesToElement(element, config.theme.layoutItem ?? 'likhari-layout-item');
110
+ return element;
111
+ }
112
+ updateDOM() {
113
+ return false;
114
+ }
115
+ canBeEmpty() {
116
+ return false;
117
+ }
118
+ canIndent() {
119
+ return false;
120
+ }
121
+ }
122
+ export function $createLayoutItemNode() {
123
+ return $applyNodeReplacement(new LayoutItemNode());
124
+ }
125
+ export function $isLayoutItemNode(node) {
126
+ return node instanceof LayoutItemNode;
127
+ }
128
+ /** Builds a container with `columnCount` empty items — the shape every
129
+ * LAYOUT insert produces, and what the node-transform below re-normalizes
130
+ * a malformed tree back toward. */
131
+ export function $createLayoutWithColumns(columnCount) {
132
+ const count = Math.max(2, Math.min(6, Math.round(columnCount)));
133
+ const container = $createLayoutContainerNode(`repeat(${count}, 1fr)`);
134
+ for (let i = 0; i < count; i++) {
135
+ container.append($createLayoutItemNode().append($createParagraphNode()));
136
+ }
137
+ return container;
138
+ }
@@ -0,0 +1,12 @@
1
+ import { LayoutContainerNode, LayoutItemNode } from './LayoutNode.js';
2
+ /** Inserts a `columnCount`-column layout right after the selection's
3
+ * top-level block, and focuses its first column. Returns false (so the
4
+ * command falls through) when there's no usable selection to anchor on. */
5
+ export declare function $insertLayout(columnCount: number): boolean;
6
+ /** Keeps LayoutContainerNode/LayoutItemNode nesting valid against malformed
7
+ * trees from paste, undo or JSON import — a LayoutItemNode can only live
8
+ * directly inside a LayoutContainerNode and vice versa. */
9
+ export declare function $normalizeLayoutItem(node: LayoutItemNode): void;
10
+ /** See $normalizeLayoutItem — the container-side half of the same rule. */
11
+ export declare function $normalizeLayoutContainer(node: LayoutContainerNode): void;
12
+ //# sourceMappingURL=layoutActions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layoutActions.d.ts","sourceRoot":"","sources":["../../src/blocks/layoutActions.ts"],"names":[],"mappings":"AACA,OAAO,EAAuE,mBAAmB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAExI;;2EAE2E;AAC3E,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAQ1D;AAED;;2DAE2D;AAC3D,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAM/D;AAED,2EAA2E;AAC3E,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CASzE"}
@@ -0,0 +1,39 @@
1
+ import { $getSelection, $isRangeSelection } from 'lexical';
2
+ import { $createLayoutWithColumns, $isLayoutContainerNode, $isLayoutItemNode } from './LayoutNode.js';
3
+ /** Inserts a `columnCount`-column layout right after the selection's
4
+ * top-level block, and focuses its first column. Returns false (so the
5
+ * command falls through) when there's no usable selection to anchor on. */
6
+ export function $insertLayout(columnCount) {
7
+ const selection = $getSelection();
8
+ if (!$isRangeSelection(selection))
9
+ return false;
10
+ const anchorTopLevel = selection.anchor.getNode().getTopLevelElementOrThrow();
11
+ const container = $createLayoutWithColumns(columnCount);
12
+ anchorTopLevel.insertAfter(container);
13
+ container.getFirstDescendant()?.selectStart();
14
+ return true;
15
+ }
16
+ /** Keeps LayoutContainerNode/LayoutItemNode nesting valid against malformed
17
+ * trees from paste, undo or JSON import — a LayoutItemNode can only live
18
+ * directly inside a LayoutContainerNode and vice versa. */
19
+ export function $normalizeLayoutItem(node) {
20
+ const parent = node.getParent();
21
+ if ($isLayoutContainerNode(parent))
22
+ return;
23
+ // Orphaned: unwrap into plain content rather than leaving a stray node.
24
+ for (const child of node.getChildren())
25
+ node.insertBefore(child);
26
+ node.remove();
27
+ }
28
+ /** See $normalizeLayoutItem — the container-side half of the same rule. */
29
+ export function $normalizeLayoutContainer(node) {
30
+ const children = node.getChildren();
31
+ if (children.length === 0) {
32
+ node.remove();
33
+ return;
34
+ }
35
+ for (const child of children) {
36
+ if (!$isLayoutItemNode(child))
37
+ child.remove();
38
+ }
39
+ }
@@ -0,0 +1,11 @@
1
+ export interface LayoutDialogValue {
2
+ columnCount: number;
3
+ }
4
+ export interface LayoutDialogProps {
5
+ opened: boolean;
6
+ onSubmit: (value: LayoutDialogValue) => void;
7
+ onClose: () => void;
8
+ }
9
+ /** Insert-columns dialog: how many equal-width columns to create. */
10
+ export declare function LayoutDialog({ opened, onSubmit, onClose }: LayoutDialogProps): import("react").JSX.Element;
11
+ //# sourceMappingURL=LayoutDialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LayoutDialog.d.ts","sourceRoot":"","sources":["../../src/components/LayoutDialog.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC7C,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAMD,qEAAqE;AACrE,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,iBAAiB,+BAmC5E"}
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Button, Group, Modal, NumberInput } from '@mantine/core';
4
+ import { useUiStrings } from '../i18n/useStrings.js';
5
+ import { usePortalTarget } from '../PortalTargetContext.js';
6
+ const MIN_COLUMNS = 2;
7
+ const MAX_COLUMNS = 6;
8
+ const DEFAULT_COLUMNS = 2;
9
+ /** Insert-columns dialog: how many equal-width columns to create. */
10
+ export function LayoutDialog({ opened, onSubmit, onClose }) {
11
+ const strings = useUiStrings();
12
+ const portalTarget = usePortalTarget();
13
+ const [columnCount, setColumnCount] = useState(DEFAULT_COLUMNS);
14
+ useEffect(() => {
15
+ if (!opened)
16
+ return;
17
+ setColumnCount(DEFAULT_COLUMNS);
18
+ }, [opened]);
19
+ const handleSubmit = (e) => {
20
+ e.preventDefault();
21
+ onSubmit({ columnCount });
22
+ };
23
+ return (_jsx(Modal, { opened: opened, onClose: onClose, title: strings.layoutDialog.title, centered: true, size: "sm", portalProps: { target: portalTarget }, children: _jsxs("form", { onSubmit: handleSubmit, children: [_jsx(NumberInput, { label: strings.layoutDialog.columnCount, min: MIN_COLUMNS, max: MAX_COLUMNS, value: columnCount, onChange: (v) => setColumnCount(typeof v === 'number' ? v : DEFAULT_COLUMNS), "data-autofocus": true }), _jsxs(Group, { justify: "flex-end", gap: "xs", mt: "md", children: [_jsx(Button, { variant: "default", onClick: onClose, children: strings.common.cancel }), _jsx(Button, { type: "submit", children: strings.common.insert })] })] }) }));
24
+ }
@@ -10,8 +10,8 @@ export interface TableDialogProps {
10
10
  }
11
11
  /**
12
12
  * Insert-table dialog: rows x columns and an optional header row. Cell
13
- * merge/split is intentionally not offered anywhere in this dialog or the
14
- * table it creates — see the comment on `TablePlugin` in `EditorRoot.tsx`.
13
+ * merge/split is a selection-time action (table context menu), not a
14
+ * dimension of the table at creation, so it has no place in this dialog.
15
15
  */
16
16
  export declare function TableDialog({ opened, onSubmit, onClose }: TableDialogProps): import("react").JSX.Element;
17
17
  //# sourceMappingURL=TableDialog.d.ts.map
@@ -9,8 +9,8 @@ const DEFAULT_ROWS = 3;
9
9
  const DEFAULT_COLUMNS = 3;
10
10
  /**
11
11
  * Insert-table dialog: rows x columns and an optional header row. Cell
12
- * merge/split is intentionally not offered anywhere in this dialog or the
13
- * table it creates — see the comment on `TablePlugin` in `EditorRoot.tsx`.
12
+ * merge/split is a selection-time action (table context menu), not a
13
+ * dimension of the table at creation, so it has no place in this dialog.
14
14
  */
15
15
  export function TableDialog({ opened, onSubmit, onClose }) {
16
16
  const strings = useUiStrings();
@@ -1 +1 @@
1
- {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AAuDA,OAAO,EAAgB,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEzE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAA6D,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAItG,OAAO,KAAK,EAAE,MAAM,EAAW,MAAM,iBAAiB,CAAC;AA+YvD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,2BAA2B,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,wFAAwF;IACxF,SAAS,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IAC1B,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kGAAkG;IAClG,MAAM,CAAC,EAAE,oBAAoB,CAAC;CAC/B;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAkC,EAAE,SAAiB,EAAE,MAAa,EAAE,MAAM,EAAE,EAAE,YAAY,+BAg7BxJ"}
1
+ {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AA6DA,OAAO,EAAgB,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEzE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAA6D,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAItG,OAAO,KAAK,EAAE,MAAM,EAAW,MAAM,iBAAiB,CAAC;AA0avD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,2BAA2B,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,wFAAwF;IACxF,SAAS,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IAC1B,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kGAAkG;IAClG,MAAM,CAAC,EAAE,oBAAoB,CAAC;CAC/B;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAkC,EAAE,SAAiB,EAAE,MAAa,EAAE,MAAM,EAAE,EAAE,YAAY,+BAw9BxJ"}
@@ -12,9 +12,11 @@ import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontal
12
12
  import { ImageDialog } from '../image/ImageDialog.js';
13
13
  import { $createImageNode } from '../image/ImageNode.js';
14
14
  import { INSERT_PAGE_BREAK_COMMAND } from '../blocks/PageBreakNode.js';
15
+ import { INSERT_LAYOUT_COMMAND } from '../blocks/LayoutNode.js';
15
16
  import { TableDialog } from './TableDialog.js';
17
+ import { LayoutDialog } from './LayoutDialog.js';
16
18
  import { $getTableCellNodeFromLexicalNode, $isTableSelection, INSERT_TABLE_COMMAND } from '@lexical/table';
17
- import { $deleteTable, $deleteTableColumns, $deleteTableRows, $getTableSelectionSize, $insertTableColumns, $insertTableRows, } from '../table/tableActions.js';
19
+ import { $canMergeSelectedCells, $canUnmergeSelectedCell, $deleteTable, $deleteTableColumns, $deleteTableRows, $getTableSelectionSize, $insertTableColumns, $insertTableRows, $mergeTableCells, $unmergeTableCell, } from '../table/tableActions.js';
18
20
  import { $createLinkNode, $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
19
21
  import { LinkDialog } from './LinkDialog.js';
20
22
  import { DraftsDialog } from './DraftsDialog.js';
@@ -23,7 +25,7 @@ import { CANVAS_FONT_DEFAULTS, DEFAULT_FONT_OPTIONS, FONT_SIZES_PX } from '../fo
23
25
  import { setStyleProperty } from '../utils/style.js';
24
26
  import { useStrings } from '../i18n/useStrings.js';
25
27
  import { usePortalTarget } from '../PortalTargetContext.js';
26
- import { IconAbc, IconAlignCenter, IconAlignJustified, IconAlignLeft, IconAlignRight, IconArrowBackUp, IconArrowForwardUp, IconBold, IconClearFormatting, IconDeviceFloppy, IconDots, IconHistory, IconExternalLink, IconFeather, IconH1, IconH2, IconH3, IconH4, IconH5, IconH6, IconIndentDecrease, IconIndentIncrease, IconItalic, IconLetterCase, IconLetterCaseLower, IconLetterCaseUpper, IconLink, IconLinkOff, IconList, IconListCheck, IconListNumbers, IconPageBreak, IconPencil, IconPhoto, IconPilcrow, IconQuote, IconSeparatorHorizontal, IconSparkles, IconStrikethrough, IconSubscript, IconSuperscript, IconColumnInsertLeft, IconColumnInsertRight, IconColumnRemove, IconRowInsertBottom, IconRowInsertTop, IconRowRemove, IconTable, IconTableMinus, IconTableOptions, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
28
+ import { IconAbc, IconAlignCenter, IconAlignJustified, IconAlignLeft, IconAlignRight, IconArrowBackUp, IconArrowForwardUp, IconBold, IconClearFormatting, IconDeviceFloppy, IconDots, IconHistory, IconExternalLink, IconFeather, IconH1, IconH2, IconH3, IconH4, IconH5, IconH6, IconIndentDecrease, IconIndentIncrease, IconItalic, IconLetterCase, IconLetterCaseLower, IconLetterCaseUpper, IconLink, IconLinkOff, IconList, IconListCheck, IconListNumbers, IconPageBreak, IconPencil, IconPhoto, IconPilcrow, IconQuote, IconSeparatorHorizontal, IconSparkles, IconStrikethrough, IconSubscript, IconSuperscript, IconArrowMergeBoth, IconArrowsSplit2, IconColumnInsertLeft, IconColumnInsertRight, IconColumnRemove, IconColumns, IconRowInsertBottom, IconRowInsertTop, IconRowRemove, IconTable, IconTableMinus, IconTableOptions, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
27
29
  /** Sentinel for "no inline style": Lexical returns the default it is given when
28
30
  * the selection has none, so this tells "unstyled" (show the canvas default)
29
31
  * apart from "styled with a value the dropdown doesn't list" (show nothing). */
@@ -40,12 +42,14 @@ const INITIAL_STATE = {
40
42
  inTable: false,
41
43
  tableRows: 1,
42
44
  tableColumns: 1,
45
+ canMergeCells: false,
46
+ canUnmergeCell: false,
43
47
  canUndo: false,
44
48
  canRedo: false,
45
49
  };
46
50
  const ICON_SIZE = 17;
47
51
  const ICON_STROKE = 1.75;
48
- const ALIGN_ICONS = {
52
+ const ALIGN_ICONS_LTR = {
49
53
  start: IconAlignLeft,
50
54
  left: IconAlignLeft,
51
55
  center: IconAlignCenter,
@@ -53,6 +57,14 @@ const ALIGN_ICONS = {
53
57
  right: IconAlignRight,
54
58
  justify: IconAlignJustified,
55
59
  };
60
+ // UI spec §5: "start"/"end" are direction-relative — in RTL, start points
61
+ // right, so the icon has to swap glyphs. "left"/"right" are the poetry
62
+ // preset's literal per-couplet overrides and never mirror.
63
+ const ALIGN_ICONS_RTL = {
64
+ ...ALIGN_ICONS_LTR,
65
+ start: IconAlignRight,
66
+ end: IconAlignLeft,
67
+ };
56
68
  const HEADING_ICONS = {
57
69
  1: IconH1,
58
70
  2: IconH2,
@@ -131,7 +143,7 @@ function LinkMenuItems({ url, onEdit, onRemove, strings }) {
131
143
  /** The table actions menu's contents — shared by the toolbar button and the
132
144
  * right-click menu. Each acts on the selected cell, or on every row/column a
133
145
  * multi-cell selection covers. */
134
- function TableMenuItems({ strings, direction, rows, columns, onAction, }) {
146
+ function TableMenuItems({ strings, direction, rows, columns, canMergeCells, canUnmergeCell, onAction, }) {
135
147
  const t = strings.tableMenu;
136
148
  // "Before" is the start side, which is on the right in an RTL table.
137
149
  const rtl = direction === 'rtl';
@@ -141,7 +153,7 @@ function TableMenuItems({ strings, direction, rows, columns, onAction, }) {
141
153
  const Icon = icon;
142
154
  return (_jsx(Menu.Item, { color: color, leftSection: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onAction(action), children: label }));
143
155
  };
144
- return (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { children: t.menuLabel }), item(IconRowInsertTop, manyRows ? t.insertRowsBefore : t.insertRowBefore, () => $insertTableRows(false)), item(IconRowInsertBottom, manyRows ? t.insertRowsAfter : t.insertRowAfter, () => $insertTableRows(true)), item(rtl ? IconColumnInsertRight : IconColumnInsertLeft, manyColumns ? t.insertColumnsBefore : t.insertColumnBefore, () => $insertTableColumns(false)), item(rtl ? IconColumnInsertLeft : IconColumnInsertRight, manyColumns ? t.insertColumnsAfter : t.insertColumnAfter, () => $insertTableColumns(true)), _jsx(Menu.Divider, {}), item(IconRowRemove, manyRows ? t.deleteRows : t.deleteRow, $deleteTableRows), item(IconColumnRemove, manyColumns ? t.deleteColumns : t.deleteColumn, $deleteTableColumns), item(IconTableMinus, t.deleteTable, $deleteTable, 'red')] }));
156
+ return (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { children: t.menuLabel }), item(IconRowInsertTop, manyRows ? t.insertRowsBefore : t.insertRowBefore, () => $insertTableRows(false)), item(IconRowInsertBottom, manyRows ? t.insertRowsAfter : t.insertRowAfter, () => $insertTableRows(true)), item(rtl ? IconColumnInsertRight : IconColumnInsertLeft, manyColumns ? t.insertColumnsBefore : t.insertColumnBefore, () => $insertTableColumns(false)), item(rtl ? IconColumnInsertLeft : IconColumnInsertRight, manyColumns ? t.insertColumnsAfter : t.insertColumnAfter, () => $insertTableColumns(true)), (canMergeCells || canUnmergeCell) && _jsx(Menu.Divider, {}), canMergeCells && item(IconArrowMergeBoth, t.mergeCells, $mergeTableCells), canUnmergeCell && item(IconArrowsSplit2, t.unmergeCell, $unmergeTableCell), _jsx(Menu.Divider, {}), item(IconRowRemove, manyRows ? t.deleteRows : t.deleteRow, $deleteTableRows), item(IconColumnRemove, manyColumns ? t.deleteColumns : t.deleteColumn, $deleteTableColumns), item(IconTableMinus, t.deleteTable, $deleteTable, 'red')] }));
145
157
  }
146
158
  export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAULT_FONT_OPTIONS, direction = 'ltr', locale = 'en', drafts }) {
147
159
  const [editor] = useLexicalComposerContext();
@@ -149,6 +161,8 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
149
161
  const strings = useStrings(locale);
150
162
  const portalTarget = usePortalTarget();
151
163
  const [draftsOpen, setDraftsOpen] = useState(false);
164
+ const rtl = direction === 'rtl';
165
+ const ALIGN_ICONS = rtl ? ALIGN_ICONS_RTL : ALIGN_ICONS_LTR;
152
166
  // Responsive "priority+" overflow: the toolbar's outer container and a
153
167
  // hidden nowrap clone (rendered with every movable group forced inline)
154
168
  // are both measured; whenever the clone's natural width exceeds the
@@ -164,7 +178,8 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
164
178
  // A selection of several table cells isn't a range selection.
165
179
  if ($isTableSelection(selection)) {
166
180
  const { rows, columns } = $getTableSelectionSize();
167
- setState((s) => ({ ...s, inTable: true, tableRows: rows, tableColumns: columns }));
181
+ const canMergeCells = $canMergeSelectedCells();
182
+ setState((s) => ({ ...s, inTable: true, tableRows: rows, tableColumns: columns, canMergeCells, canUnmergeCell: false }));
168
183
  return;
169
184
  }
170
185
  if (!$isRangeSelection(selection))
@@ -172,6 +187,7 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
172
187
  const anchorNode = selection.anchor.getNode();
173
188
  const inTable = $getTableCellNodeFromLexicalNode(anchorNode) !== null;
174
189
  const tableSize = inTable ? $getTableSelectionSize() : { rows: 1, columns: 1 };
190
+ const canUnmergeCell = inTable && $canUnmergeSelectedCell();
175
191
  const element = anchorNode.getKey() === 'root' ? anchorNode : (anchorNode.getTopLevelElement() ?? anchorNode);
176
192
  const listParent = $findMatchingParent(anchorNode, $isListNode);
177
193
  const listType = listParent && $isListNode(listParent) ? listParent.getListType() : null;
@@ -210,6 +226,8 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
210
226
  inTable,
211
227
  tableRows: tableSize.rows,
212
228
  tableColumns: tableSize.columns,
229
+ canMergeCells: false,
230
+ canUnmergeCell,
213
231
  }));
214
232
  });
215
233
  }, [editor]);
@@ -329,6 +347,22 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
329
347
  });
330
348
  closeTableDialog();
331
349
  };
350
+ const [layoutDialogOpen, setLayoutDialogOpen] = useState(false);
351
+ const openLayoutDialog = () => {
352
+ snapshotSelection();
353
+ setLayoutDialogOpen(true);
354
+ };
355
+ const closeLayoutDialog = () => {
356
+ setLayoutDialogOpen(false);
357
+ editor.focus();
358
+ };
359
+ const insertLayout = ({ columnCount }) => {
360
+ const saved = menuSelectionRef.current;
361
+ if (saved)
362
+ editor.update(() => $setSelection(saved.clone()), { discrete: true });
363
+ editor.dispatchCommand(INSERT_LAYOUT_COMMAND, { columnCount });
364
+ closeLayoutDialog();
365
+ };
332
366
  /** Applies a font property per `config.font.scope`: to the selection, to
333
367
  * every text node in the document, or (for 'both') to the selection when
334
368
  * there is a range and to the whole document when there isn't. */
@@ -531,6 +565,7 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
531
565
  config.tables ||
532
566
  config.blocks.horizontalRule ||
533
567
  config.blocks.pageBreak ||
568
+ config.columns ||
534
569
  config.poetry.enabled;
535
570
  const showLanguageGroup = config.language.autocorrect || config.language.textCleanup || config.language.spellCheck;
536
571
  // The "script & cleanup" and "indent/outdent" groups (UI spec §3.3) render
@@ -573,17 +608,19 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
573
608
  if (fmt.clearFormatting) {
574
609
  scriptCleanupItems.push({ key: 'clear', icon: IconClearFormatting, label: strings.toolbar.clearFormatting, onClick: clearFormatting });
575
610
  }
611
+ // UI spec §5: indent/outdent arrows are direction-relative and swap in RTL
612
+ // (outdent moves content toward the start edge, which is the right in RTL).
576
613
  const indentItems = config.indent
577
614
  ? [
578
615
  {
579
616
  key: 'outdent',
580
- icon: IconIndentDecrease,
617
+ icon: rtl ? IconIndentIncrease : IconIndentDecrease,
581
618
  label: strings.toolbar.outdent,
582
619
  onClick: () => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined),
583
620
  },
584
621
  {
585
622
  key: 'indent',
586
- icon: IconIndentIncrease,
623
+ icon: rtl ? IconIndentDecrease : IconIndentIncrease,
587
624
  label: strings.toolbar.indent,
588
625
  onClick: () => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined),
589
626
  },
@@ -701,8 +738,8 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
701
738
  restoreSelection();
702
739
  openLinkDialog();
703
740
  }, onRemove: removeLink, strings: strings }) })] })) : (_jsx(ToolbarButton, { icon: IconLink, title: strings.toolbar.insertLink, onClick: openLinkDialog })) }, "link")),
704
- showStubInsertGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [(config.images.linked || config.images.embedded) && (_jsx(ToolbarButton, { icon: IconPhoto, title: strings.toolbar.insertImage, onClick: openImageDialog })), config.blocks.pageBreak && (_jsx(ToolbarButton, { icon: IconPageBreak, title: strings.toolbar.insertPageBreak, onClick: () => editor.dispatchCommand(INSERT_PAGE_BREAK_COMMAND, undefined) })), config.tables && _jsx(ToolbarButton, { icon: IconTable, title: strings.toolbar.insertTable, onClick: openTableDialog }), config.blocks.horizontalRule && (_jsx(ToolbarButton, { icon: IconSeparatorHorizontal, title: strings.toolbar.insertHorizontalRule, onClick: () => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined) })), config.poetry.enabled && _jsx(StubButton, { icon: IconFeather, title: strings.toolbar.poetryBlocks, comingSoon: strings.toolbar.comingSoon })] }, "stubInsert")),
705
- config.tables && state.inTable && (_jsx("div", { className: "likhari-toolbar-group", children: _jsxs(Menu, { position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 240, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "data-active": "true", "aria-pressed": "true", "aria-haspopup": "menu", "aria-label": strings.toolbar.tableOptions, title: strings.toolbar.tableOptions, onMouseDown: (e) => e.preventDefault(), children: _jsx(IconTableOptions, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: _jsx(TableMenuItems, { strings: strings, direction: direction, rows: state.tableRows, columns: state.tableColumns, onAction: runTableAction }) })] }) }, "tableActions")),
741
+ showStubInsertGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [(config.images.linked || config.images.embedded) && (_jsx(ToolbarButton, { icon: IconPhoto, title: strings.toolbar.insertImage, onClick: openImageDialog })), config.blocks.pageBreak && (_jsx(ToolbarButton, { icon: IconPageBreak, title: strings.toolbar.insertPageBreak, onClick: () => editor.dispatchCommand(INSERT_PAGE_BREAK_COMMAND, undefined) })), config.tables && _jsx(ToolbarButton, { icon: IconTable, title: strings.toolbar.insertTable, onClick: openTableDialog }), config.columns && _jsx(ToolbarButton, { icon: IconColumns, title: strings.toolbar.insertColumns, onClick: openLayoutDialog }), config.blocks.horizontalRule && (_jsx(ToolbarButton, { icon: IconSeparatorHorizontal, title: strings.toolbar.insertHorizontalRule, onClick: () => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined) })), config.poetry.enabled && _jsx(StubButton, { icon: IconFeather, title: strings.toolbar.poetryBlocks, comingSoon: strings.toolbar.comingSoon })] }, "stubInsert")),
742
+ config.tables && state.inTable && (_jsx("div", { className: "likhari-toolbar-group", children: _jsxs(Menu, { position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 240, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "data-active": "true", "aria-pressed": "true", "aria-haspopup": "menu", "aria-label": strings.toolbar.tableOptions, title: strings.toolbar.tableOptions, onMouseDown: (e) => e.preventDefault(), children: _jsx(IconTableOptions, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: _jsx(TableMenuItems, { strings: strings, direction: direction, rows: state.tableRows, columns: state.tableColumns, canMergeCells: state.canMergeCells, canUnmergeCell: state.canUnmergeCell, onAction: runTableAction }) })] }) }, "tableActions")),
706
743
  showLanguageGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.language.autocorrect && _jsx(StubButton, { icon: IconWand, title: strings.toolbar.autocorrect, comingSoon: strings.toolbar.comingSoon }), config.language.textCleanup && _jsx(StubButton, { icon: IconSparkles, title: strings.toolbar.textCleanup, comingSoon: strings.toolbar.comingSoon }), config.language.spellCheck && _jsx(StubButton, { icon: IconAbc, title: strings.toolbar.spellChecker, comingSoon: strings.toolbar.comingSoon })] }, "language")),
707
744
  ];
708
745
  return (_jsxs("div", { className: "likhari-toolbar", role: "toolbar", "aria-label": strings.toolbar.ariaLabel, ref: toolbarContainerRef, children: [fixedGroups, movableGroups
@@ -718,7 +755,7 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
718
755
  }, onRemove: removeLink, strings: strings }) })] })), config.tables && (_jsxs(Menu, { opened: tableContext !== null, onChange: (opened) => {
719
756
  if (!opened)
720
757
  setTableContext(null);
721
- }, position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 240, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: tableContext?.x ?? -9999, top: tableContext?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsx(Menu.Dropdown, { children: _jsx(TableMenuItems, { strings: strings, direction: direction, rows: tableContext?.rows ?? 1, columns: tableContext?.columns ?? 1, onAction: runTableAction }) })] })), _jsxs("div", { "aria-hidden": "true", ref: toolbarMeasureRef, style: {
758
+ }, position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 240, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: tableContext?.x ?? -9999, top: tableContext?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsx(Menu.Dropdown, { children: _jsx(TableMenuItems, { strings: strings, direction: direction, rows: tableContext?.rows ?? 1, columns: tableContext?.columns ?? 1, canMergeCells: state.canMergeCells, canUnmergeCell: state.canUnmergeCell, onAction: runTableAction }) })] })), _jsxs("div", { "aria-hidden": "true", ref: toolbarMeasureRef, style: {
722
759
  position: 'absolute',
723
760
  visibility: 'hidden',
724
761
  pointerEvents: 'none',
@@ -727,5 +764,5 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
727
764
  display: 'flex',
728
765
  flexWrap: 'nowrap',
729
766
  gap: 8,
730
- }, children: [fixedGroups, movableGroups.map((group) => (_jsx(MovableGroupButtons, { group: group }, group.key)))] }), (config.images.linked || config.images.embedded) && (_jsx(ImageDialog, { mode: "insert", opened: imageDialogOpen, onSubmit: insertImage, onClose: closeImageDialog })), config.tables && _jsx(TableDialog, { opened: tableDialogOpen, onSubmit: insertTable, onClose: closeTableDialog }), config.links && (_jsx(LinkDialog, { opened: linkDialogOpen, initialUrl: state.linkUrl, showTextField: linkNeedsText, onSubmit: applyLink, onRemove: state.isLink ? removeLink : undefined, onClose: closeLinkDialog })), drafts && _jsx(DraftsDialog, { opened: draftsOpen, locale: locale, onClose: () => setDraftsOpen(false), ...drafts })] }));
767
+ }, children: [fixedGroups, movableGroups.map((group) => (_jsx(MovableGroupButtons, { group: group }, group.key)))] }), (config.images.linked || config.images.embedded) && (_jsx(ImageDialog, { mode: "insert", opened: imageDialogOpen, onSubmit: insertImage, onClose: closeImageDialog })), config.tables && _jsx(TableDialog, { opened: tableDialogOpen, onSubmit: insertTable, onClose: closeTableDialog }), config.columns && _jsx(LayoutDialog, { opened: layoutDialogOpen, onSubmit: insertLayout, onClose: closeLayoutDialog }), config.links && (_jsx(LinkDialog, { opened: linkDialogOpen, initialUrl: state.linkUrl, showTextField: linkNeedsText, onSubmit: applyLink, onRemove: state.isLink ? removeLink : undefined, onClose: closeLinkDialog })), drafts && _jsx(DraftsDialog, { opened: draftsOpen, locale: locale, onClose: () => setDraftsOpen(false), ...drafts })] }));
731
768
  }
@@ -46,6 +46,7 @@ export interface Strings {
46
46
  insertImage: string;
47
47
  insertPageBreak: string;
48
48
  insertTable: string;
49
+ insertColumns: string;
49
50
  insertHorizontalRule: string;
50
51
  poetryBlocks: string;
51
52
  comingSoon: (label: string) => string;
@@ -131,6 +132,8 @@ export interface Strings {
131
132
  insertColumnsAfter: string;
132
133
  deleteRows: string;
133
134
  deleteColumns: string;
135
+ mergeCells: string;
136
+ unmergeCell: string;
134
137
  };
135
138
  link: {
136
139
  menuLabel: string;
@@ -154,6 +157,10 @@ export interface Strings {
154
157
  columns: string;
155
158
  headerRow: string;
156
159
  };
160
+ layoutDialog: {
161
+ title: string;
162
+ columnCount: string;
163
+ };
157
164
  imageDialog: {
158
165
  titleInsert: string;
159
166
  titleEdit: string;
@@ -1 +1 @@
1
- {"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../../src/i18n/strings.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../../src/i18n/strings.ts"],"names":[],"mappings":"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"}
@@ -37,6 +37,7 @@ const en = {
37
37
  insertImage: 'Insert image',
38
38
  insertPageBreak: 'Insert page break',
39
39
  insertTable: 'Insert table',
40
+ insertColumns: 'Insert columns',
40
41
  insertHorizontalRule: 'Insert horizontal rule',
41
42
  poetryBlocks: 'Poetry blocks',
42
43
  comingSoon: (label) => `${label} (coming soon)`,
@@ -115,6 +116,8 @@ const en = {
115
116
  insertColumnsAfter: 'Insert columns after',
116
117
  deleteRows: 'Delete rows',
117
118
  deleteColumns: 'Delete columns',
119
+ mergeCells: 'Merge cells',
120
+ unmergeCell: 'Unmerge cell',
118
121
  },
119
122
  link: {
120
123
  menuLabel: 'Link',
@@ -138,6 +141,10 @@ const en = {
138
141
  columns: 'Columns',
139
142
  headerRow: 'Header row',
140
143
  },
144
+ layoutDialog: {
145
+ title: 'Insert columns',
146
+ columnCount: 'Number of columns',
147
+ },
141
148
  imageDialog: {
142
149
  titleInsert: 'Insert image',
143
150
  titleEdit: 'Edit image',
@@ -230,6 +237,7 @@ const ur = {
230
237
  insertImage: 'تصویر شامل کریں',
231
238
  insertPageBreak: 'صفحے کی تقسیم شامل کریں',
232
239
  insertTable: 'جدول شامل کریں',
240
+ insertColumns: 'کالم شامل کریں',
233
241
  insertHorizontalRule: 'افقی لکیر شامل کریں',
234
242
  poetryBlocks: 'شاعری کے بلاکس',
235
243
  comingSoon: (label) => `${label} (جلد آ رہا ہے)`,
@@ -308,6 +316,8 @@ const ur = {
308
316
  insertColumnsAfter: 'بعد میں کالمز شامل کریں',
309
317
  deleteRows: 'قطاریں حذف کریں',
310
318
  deleteColumns: 'کالمز حذف کریں',
319
+ mergeCells: 'خانے ضم کریں',
320
+ unmergeCell: 'خانہ الگ کریں',
311
321
  },
312
322
  link: {
313
323
  menuLabel: 'لنک',
@@ -331,6 +341,10 @@ const ur = {
331
341
  columns: 'کالم',
332
342
  headerRow: 'سرِ فہرست قطار',
333
343
  },
344
+ layoutDialog: {
345
+ title: 'کالم شامل کریں',
346
+ columnCount: 'کالموں کی تعداد',
347
+ },
334
348
  imageDialog: {
335
349
  titleInsert: 'تصویر شامل کریں',
336
350
  titleEdit: 'تصویر میں ترمیم کریں',
@@ -423,6 +437,7 @@ const paShahmukhi = {
423
437
  insertImage: 'تصویر پاؤ',
424
438
  insertPageBreak: 'صفحے دی ونڈ پاؤ',
425
439
  insertTable: 'ٹیبل پاؤ',
440
+ insertColumns: 'کالم پاؤ',
426
441
  insertHorizontalRule: 'لیٹی لکیر پاؤ',
427
442
  poetryBlocks: 'شاعری بلاک',
428
443
  comingSoon: (label) => `${label} (چھیتی آ رہا اے)`,
@@ -501,6 +516,8 @@ const paShahmukhi = {
501
516
  insertColumnsAfter: 'بعد وچ کالماں پاؤ',
502
517
  deleteRows: 'قطاراں مٹاؤ',
503
518
  deleteColumns: 'کالماں مٹاؤ',
519
+ mergeCells: 'خانے رلاؤ',
520
+ unmergeCell: 'خانہ الگ کرو',
504
521
  },
505
522
  link: {
506
523
  menuLabel: 'لنک',
@@ -524,6 +541,10 @@ const paShahmukhi = {
524
541
  columns: 'کالم',
525
542
  headerRow: 'سرکڑی قطار',
526
543
  },
544
+ layoutDialog: {
545
+ title: 'کالم پاؤ',
546
+ columnCount: 'کالماں دی گنتی',
547
+ },
527
548
  imageDialog: {
528
549
  titleInsert: 'تصویر پاؤ',
529
550
  titleEdit: 'تصویر وچ تبدیلی کرو',
@@ -1 +1 @@
1
- {"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AASlD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,WAAW,CAAC,EAY5C,CAAC"}
1
+ {"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAUlD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,WAAW,CAAC,EAc5C,CAAC"}
package/dist/nodes.js CHANGED
@@ -5,6 +5,7 @@ import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
5
5
  import { TableCellNode, TableNode, TableRowNode } from '@lexical/table';
6
6
  import { ImageNode } from './image/ImageNode.js';
7
7
  import { PageBreakNode } from './blocks/PageBreakNode.js';
8
+ import { LayoutContainerNode, LayoutItemNode } from './blocks/LayoutNode.js';
8
9
  /**
9
10
  * Node types registered on the editor at construction time. Per
10
11
  * editor-architecture-design.md §2, node registration cannot be conditional
@@ -26,4 +27,6 @@ export const EDITOR_NODES = [
26
27
  TableCellNode,
27
28
  ImageNode,
28
29
  PageBreakNode,
30
+ LayoutContainerNode,
31
+ LayoutItemNode,
29
32
  ];
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Registers INSERT_LAYOUT_COMMAND (mirrors PageBreakPlugin) and two node
3
+ * transforms that keep the container/item pairing structurally valid —
4
+ * needed because paste, undo and JSON import can all produce a tree the UI
5
+ * itself never would. The actual logic lives in blocks/layoutActions.ts so
6
+ * it's testable without mounting a React tree.
7
+ */
8
+ export declare function LayoutPlugin(): null;
9
+ //# sourceMappingURL=LayoutPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LayoutPlugin.d.ts","sourceRoot":"","sources":["../../src/plugins/LayoutPlugin.tsx"],"names":[],"mappings":"AAOA;;;;;;GAMG;AACH,wBAAgB,YAAY,SAgB3B"}
@@ -0,0 +1,23 @@
1
+ import { useEffect } from 'react';
2
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
3
+ import { mergeRegister } from '@lexical/utils';
4
+ import { COMMAND_PRIORITY_EDITOR } from 'lexical';
5
+ import { INSERT_LAYOUT_COMMAND, LayoutContainerNode, LayoutItemNode } from '../blocks/LayoutNode.js';
6
+ import { $insertLayout, $normalizeLayoutContainer, $normalizeLayoutItem } from '../blocks/layoutActions.js';
7
+ /**
8
+ * Registers INSERT_LAYOUT_COMMAND (mirrors PageBreakPlugin) and two node
9
+ * transforms that keep the container/item pairing structurally valid —
10
+ * needed because paste, undo and JSON import can all produce a tree the UI
11
+ * itself never would. The actual logic lives in blocks/layoutActions.ts so
12
+ * it's testable without mounting a React tree.
13
+ */
14
+ export function LayoutPlugin() {
15
+ const [editor] = useLexicalComposerContext();
16
+ useEffect(() => {
17
+ if (!editor.hasNodes([LayoutContainerNode, LayoutItemNode])) {
18
+ throw new Error('LayoutPlugin: LayoutContainerNode/LayoutItemNode not registered on the editor');
19
+ }
20
+ return mergeRegister(editor.registerCommand(INSERT_LAYOUT_COMMAND, ({ columnCount }) => $insertLayout(columnCount), COMMAND_PRIORITY_EDITOR), editor.registerNodeTransform(LayoutItemNode, $normalizeLayoutItem), editor.registerNodeTransform(LayoutContainerNode, $normalizeLayoutContainer));
21
+ }, [editor]);
22
+ return null;
23
+ }
@@ -18,4 +18,17 @@ export declare function $deleteTableRows(): void;
18
18
  export declare function $deleteTableColumns(): void;
19
19
  /** Removes the whole table the selection is in. */
20
20
  export declare function $deleteTable(): void;
21
+ /** True for a selection spanning more than one cell — the only shape a merge can apply to. */
22
+ export declare function $canMergeSelectedCells(): boolean;
23
+ /** True for a caret parked in a single cell that already spans more than one row/column. */
24
+ export declare function $canUnmergeSelectedCell(): boolean;
25
+ /**
26
+ * Merges every cell the (multi-cell) selection spans into a single cell at
27
+ * its top-left corner, moving their content there and growing its colSpan/
28
+ * rowSpan to cover the vacated space. No-ops on an irregular selection shape
29
+ * (one that would cut an already-merged cell in half).
30
+ */
31
+ export declare function $mergeTableCells(): void;
32
+ /** Splits the selected (already-merged) cell back into separate 1x1 cells. */
33
+ export declare function $unmergeTableCell(): void;
21
34
  //# sourceMappingURL=tableActions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tableActions.d.ts","sourceRoot":"","sources":["../../src/table/tableActions.ts"],"names":[],"mappings":"AA2BA,0GAA0G;AAC1G,wBAAgB,sBAAsB,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAS1E;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,OAAO,GAAG,IAAI,CAS3D;AAED;wEACwE;AACxE,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,OAAO,GAAG,IAAI,CAQ9D;AAED,wFAAwF;AACxF,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAED,kDAAkD;AAClD,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED,mDAAmD;AACnD,wBAAgB,YAAY,IAAI,IAAI,CAInC"}
1
+ {"version":3,"file":"tableActions.d.ts","sourceRoot":"","sources":["../../src/table/tableActions.ts"],"names":[],"mappings":"AA8BA,0GAA0G;AAC1G,wBAAgB,sBAAsB,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAS1E;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,OAAO,GAAG,IAAI,CAS3D;AAED;wEACwE;AACxE,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,OAAO,GAAG,IAAI,CAQ9D;AAED,wFAAwF;AACxF,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAED,kDAAkD;AAClD,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED,mDAAmD;AACnD,wBAAgB,YAAY,IAAI,IAAI,CAInC;AAED,8FAA8F;AAC9F,wBAAgB,sBAAsB,IAAI,OAAO,CAIhD;AAED,4FAA4F;AAC5F,wBAAgB,uBAAuB,IAAI,OAAO,CAKjD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAgEvC;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
@@ -1,5 +1,5 @@
1
- import { $deleteTableColumn__EXPERIMENTAL, $deleteTableRow__EXPERIMENTAL, $getTableCellNodeFromLexicalNode, $getTableColumnIndexFromTableCellNode, $getTableNodeFromLexicalNodeOrThrow, $getTableRowIndexFromTableCellNode, $insertTableColumn__EXPERIMENTAL, $insertTableRow__EXPERIMENTAL, $isTableCellNode, $isTableSelection, } from '@lexical/table';
2
- import { $getSelection, $isRangeSelection } from 'lexical';
1
+ import { $computeTableMap, $deleteTableColumn__EXPERIMENTAL, $deleteTableRow__EXPERIMENTAL, $getTableCellNodeFromLexicalNode, $getTableCellNodeRect, $getTableColumnIndexFromTableCellNode, $getTableNodeFromLexicalNodeOrThrow, $getTableRowIndexFromTableCellNode, $insertTableColumn__EXPERIMENTAL, $insertTableRow__EXPERIMENTAL, $isTableCellNode, $isTableSelection, $unmergeCell, } from '@lexical/table';
2
+ import { $createParagraphNode, $getSelection, $isRangeSelection } from 'lexical';
3
3
  /** Table cells the selection covers: the caret's own cell, or every cell of a
4
4
  * multi-cell selection. Empty when the selection isn't in a table. */
5
5
  function $selectedCells() {
@@ -69,3 +69,87 @@ export function $deleteTable() {
69
69
  if (anchor)
70
70
  $getTableNodeFromLexicalNodeOrThrow(anchor).remove();
71
71
  }
72
+ /** True for a selection spanning more than one cell — the only shape a merge can apply to. */
73
+ export function $canMergeSelectedCells() {
74
+ const selection = $getSelection();
75
+ if (!$isTableSelection(selection))
76
+ return false;
77
+ return selection.getNodes().filter($isTableCellNode).length > 1;
78
+ }
79
+ /** True for a caret parked in a single cell that already spans more than one row/column. */
80
+ export function $canUnmergeSelectedCell() {
81
+ const selection = $getSelection();
82
+ if (!$isRangeSelection(selection))
83
+ return false;
84
+ const cell = $getTableCellNodeFromLexicalNode(selection.anchor.getNode());
85
+ return cell !== null && (cell.getColSpan() > 1 || cell.getRowSpan() > 1);
86
+ }
87
+ /**
88
+ * Merges every cell the (multi-cell) selection spans into a single cell at
89
+ * its top-left corner, moving their content there and growing its colSpan/
90
+ * rowSpan to cover the vacated space. No-ops on an irregular selection shape
91
+ * (one that would cut an already-merged cell in half).
92
+ */
93
+ export function $mergeTableCells() {
94
+ const selection = $getSelection();
95
+ if (!$isTableSelection(selection))
96
+ return;
97
+ if (selection.getNodes().filter($isTableCellNode).length < 2)
98
+ return;
99
+ const anchorCell = $getTableCellNodeFromLexicalNode(selection.anchor.getNode());
100
+ const focusCell = $getTableCellNodeFromLexicalNode(selection.focus.getNode());
101
+ if (!anchorCell || !focusCell)
102
+ return;
103
+ const tableNode = $getTableNodeFromLexicalNodeOrThrow(anchorCell);
104
+ const [map, anchorValue, focusValue] = $computeTableMap(tableNode, anchorCell, focusCell);
105
+ // Bounding rectangle of the two endpoint cells, each expanded by its own
106
+ // span, so a selection that starts or ends on an already-merged cell still
107
+ // captures that cell's full extent.
108
+ const boundary = {
109
+ minRow: Math.min(anchorValue.startRow, focusValue.startRow),
110
+ minColumn: Math.min(anchorValue.startColumn, focusValue.startColumn),
111
+ maxRow: Math.max(anchorValue.startRow + anchorValue.cell.getRowSpan() - 1, focusValue.startRow + focusValue.cell.getRowSpan() - 1),
112
+ maxColumn: Math.max(anchorValue.startColumn + anchorValue.cell.getColSpan() - 1, focusValue.startColumn + focusValue.cell.getColSpan() - 1),
113
+ };
114
+ // Collect the distinct cell nodes the rectangle covers, bailing out if any
115
+ // of them extends past the rectangle (merging would silently truncate it).
116
+ const cellsInRect = new Set();
117
+ for (let r = boundary.minRow; r <= boundary.maxRow; r++) {
118
+ for (let c = boundary.minColumn; c <= boundary.maxColumn; c++) {
119
+ cellsInRect.add(map[r][c].cell);
120
+ }
121
+ }
122
+ for (const cell of cellsInRect) {
123
+ const rect = $getTableCellNodeRect(cell);
124
+ if (!rect)
125
+ continue;
126
+ const exceedsTop = rect.rowIndex < boundary.minRow;
127
+ const exceedsLeft = rect.columnIndex < boundary.minColumn;
128
+ const exceedsBottom = rect.rowIndex + rect.rowSpan - 1 > boundary.maxRow;
129
+ const exceedsRight = rect.columnIndex + rect.colSpan - 1 > boundary.maxColumn;
130
+ if (exceedsTop || exceedsLeft || exceedsBottom || exceedsRight)
131
+ return;
132
+ }
133
+ const topLeft = map[boundary.minRow][boundary.minColumn].cell;
134
+ for (const cell of cellsInRect) {
135
+ if (cell === topLeft)
136
+ continue;
137
+ for (const child of cell.getChildren()) {
138
+ topLeft.append(child);
139
+ }
140
+ const row = cell.getParent();
141
+ cell.remove();
142
+ if (row && row.getChildrenSize() === 0)
143
+ row.remove();
144
+ }
145
+ if (topLeft.getChildrenSize() === 0) {
146
+ topLeft.append($createParagraphNode());
147
+ }
148
+ topLeft.setRowSpan(boundary.maxRow - boundary.minRow + 1);
149
+ topLeft.setColSpan(boundary.maxColumn - boundary.minColumn + 1);
150
+ topLeft.selectEnd();
151
+ }
152
+ /** Splits the selected (already-merged) cell back into separate 1x1 cells. */
153
+ export function $unmergeTableCell() {
154
+ $unmergeCell();
155
+ }
@@ -186,6 +186,25 @@
186
186
  line-height: 2.3;
187
187
  }
188
188
 
189
+ /* Per-block direction (requirements doc §3.2, "mixed-direction documents"):
190
+ Lexical detects each block's own direction from its first strong-direction
191
+ character and tags it with one of these theme classes independently of
192
+ the document's overall locale — an Urdu paragraph inside an English essay,
193
+ or an English heading inside an Urdu document, gets its own script-correct
194
+ font rather than inheriting the canvas-wide default. Set directly on the
195
+ block, so it wins over the inherited rule above regardless of specificity. */
196
+ .likhari-rtl {
197
+ font-family: 'Noto Nastaliq Urdu', serif;
198
+ font-size: 20px;
199
+ line-height: 2.3;
200
+ }
201
+
202
+ .likhari-ltr {
203
+ font-family: 'IBM Plex Serif', Georgia, serif;
204
+ font-size: 15px;
205
+ line-height: 1.75;
206
+ }
207
+
189
208
  .likhari-placeholder {
190
209
  position: absolute;
191
210
  top: 20px;
@@ -298,10 +317,24 @@
298
317
  }
299
318
  }
300
319
 
301
- /* Tables — cell merge/split is deliberately out of scope for now (a flagged
302
- open risk, lexical-editor-spec.md §12 item 3, needing its own scope
303
- decision); TablePlugin is configured with hasCellMerge={false} to keep
304
- every table a plain grid until that's resolved. */
320
+ /* Columns layout (lexical-editor-spec.md §4.9) — a CSS grid row whose
321
+ column count/widths come from the node's own inline style, not this
322
+ stylesheet, so each container can differ. */
323
+ .likhari-layout-container {
324
+ display: grid;
325
+ gap: 24px;
326
+ margin: 0 0 1em 0;
327
+ }
328
+
329
+ .likhari-layout-item {
330
+ min-width: 0;
331
+ overflow-wrap: break-word;
332
+ }
333
+
334
+ .likhari-layout-item > :last-child {
335
+ margin-bottom: 0;
336
+ }
337
+
305
338
  .likhari-table {
306
339
  border-collapse: collapse;
307
340
  margin: 1em 0;
@@ -1 +1 @@
1
- {"version":3,"file":"editorTheme.d.ts","sourceRoot":"","sources":["../../src/theme/editorTheme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,kBAoCzB,CAAC"}
1
+ {"version":3,"file":"editorTheme.d.ts","sourceRoot":"","sources":["../../src/theme/editorTheme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,kBA4CzB,CAAC"}
@@ -4,6 +4,12 @@
4
4
  * (the "headless + default theme" requirement, UI spec §10).
5
5
  */
6
6
  export const editorTheme = {
7
+ // Applied by Lexical's reconciler to each top-level block based on that
8
+ // block's own detected direction — see the comment in editor.css. This is
9
+ // the per-block half of mixed-direction document support (requirements
10
+ // doc §3.2); the EditorRoot `dir` prop handles the whole-document default.
11
+ ltr: 'likhari-ltr',
12
+ rtl: 'likhari-rtl',
7
13
  paragraph: 'likhari-paragraph',
8
14
  quote: 'likhari-quote',
9
15
  heading: {
@@ -34,6 +40,8 @@ export const editorTheme = {
34
40
  hr: 'likhari-hr',
35
41
  image: 'likhari-image-block',
36
42
  pageBreak: 'likhari-page-break',
43
+ layoutContainer: 'likhari-layout-container',
44
+ layoutItem: 'likhari-layout-item',
37
45
  table: 'likhari-table',
38
46
  tableRow: 'likhari-table-row',
39
47
  tableCell: 'likhari-table-cell',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshapardaz/likhari-react",
3
- "version": "0.1.13",
3
+ "version": "0.1.14-dev.115",
4
4
  "description": "React implementation of the Likhari editor, built on @lexical/react.",
5
5
  "license": "MIT",
6
6
  "repository": {