@inshapardaz/likhari-react 0.1.18-dev.150 → 0.1.18-dev.154

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.
@@ -0,0 +1,36 @@
1
+ import { type DOMConversionMap, type DOMExportOutput, type EditorConfig, type LexicalNode, type NodeKey, type SerializedElementNode, type Spread, ElementNode } from 'lexical';
2
+ export type SerializedPoetryCoupletNode = Spread<{
3
+ centered: boolean;
4
+ }, SerializedElementNode>;
5
+ /**
6
+ * One couplet's two misra lines, wrapped so each couplet can carry its own
7
+ * rendering override independent of its PoetryBlockNode's overall layout —
8
+ * currently just `centered` (a narrower box, centered on the page, instead
9
+ * of the block's default width): "two columns with a couplet that is
10
+ * single, aligned centered" mixes LayoutContainerNode rows (two-column
11
+ * couplets) with centered PoetryCoupletNodes in the same block, each
12
+ * couplet choosing independently.
13
+ *
14
+ * Used for single-column and alternating block layouts; a two-column
15
+ * couplet uses LayoutContainerNode instead (its own two-item row already
16
+ * gives it a natural per-couplet box, so it doesn't need this wrapper).
17
+ */
18
+ export declare class PoetryCoupletNode extends ElementNode {
19
+ __centered: boolean;
20
+ constructor(centered?: boolean, key?: NodeKey);
21
+ static getType(): string;
22
+ static clone(node: PoetryCoupletNode): PoetryCoupletNode;
23
+ getCentered(): boolean;
24
+ setCentered(centered: boolean): this;
25
+ static importJSON(serializedNode: SerializedPoetryCoupletNode): PoetryCoupletNode;
26
+ exportJSON(): SerializedPoetryCoupletNode;
27
+ static importDOM(): DOMConversionMap | null;
28
+ exportDOM(): DOMExportOutput;
29
+ createDOM(config: EditorConfig): HTMLElement;
30
+ updateDOM(prevNode: PoetryCoupletNode, dom: HTMLElement, config: EditorConfig): boolean;
31
+ canBeEmpty(): false;
32
+ canIndent(): false;
33
+ }
34
+ export declare function $createPoetryCoupletNode(centered?: boolean): PoetryCoupletNode;
35
+ export declare function $isPoetryCoupletNode(node: LexicalNode | null | undefined): node is PoetryCoupletNode;
36
+ //# sourceMappingURL=PoetryCoupletNode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PoetryCoupletNode.d.ts","sourceRoot":"","sources":["../../src/blocks/PoetryCoupletNode.tsx"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,qBAAqB,EAC1B,KAAK,MAAM,EACX,WAAW,EACZ,MAAM,SAAS,CAAC;AAEjB,MAAM,MAAM,2BAA2B,GAAG,MAAM,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,EAAE,qBAAqB,CAAC,CAAC;AAE/F;;;;;;;;;;;;GAYG;AACH,qBAAa,iBAAkB,SAAQ,WAAW;IAChD,UAAU,EAAE,OAAO,CAAC;gBAER,QAAQ,UAAQ,EAAE,GAAG,CAAC,EAAE,OAAO;IAK3C,MAAM,CAAC,OAAO,IAAI,MAAM;IAIxB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,iBAAiB,GAAG,iBAAiB;IAIxD,WAAW,IAAI,OAAO;IAItB,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI;IAMpC,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,2BAA2B,GAAG,iBAAiB;IAIjF,UAAU,IAAI,2BAA2B;IAIzC,MAAM,CAAC,SAAS,IAAI,gBAAgB,GAAG,IAAI;IAU3C,SAAS,IAAI,eAAe;IAO5B,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW;IAQ5C,SAAS,CAAC,QAAQ,EAAE,iBAAiB,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO;IAQvF,UAAU,IAAI,KAAK;IAInB,SAAS,IAAI,KAAK;CAGnB;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,UAAQ,GAAG,iBAAiB,CAE5E;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,IAAI,iBAAiB,CAEpG"}
@@ -0,0 +1,85 @@
1
+ import { addClassNamesToElement } from '@lexical/utils';
2
+ import { $applyNodeReplacement, ElementNode, } from 'lexical';
3
+ /**
4
+ * One couplet's two misra lines, wrapped so each couplet can carry its own
5
+ * rendering override independent of its PoetryBlockNode's overall layout —
6
+ * currently just `centered` (a narrower box, centered on the page, instead
7
+ * of the block's default width): "two columns with a couplet that is
8
+ * single, aligned centered" mixes LayoutContainerNode rows (two-column
9
+ * couplets) with centered PoetryCoupletNodes in the same block, each
10
+ * couplet choosing independently.
11
+ *
12
+ * Used for single-column and alternating block layouts; a two-column
13
+ * couplet uses LayoutContainerNode instead (its own two-item row already
14
+ * gives it a natural per-couplet box, so it doesn't need this wrapper).
15
+ */
16
+ export class PoetryCoupletNode extends ElementNode {
17
+ constructor(centered = false, key) {
18
+ super(key);
19
+ this.__centered = centered;
20
+ }
21
+ static getType() {
22
+ return 'poetry-couplet';
23
+ }
24
+ static clone(node) {
25
+ return new PoetryCoupletNode(node.__centered, node.__key);
26
+ }
27
+ getCentered() {
28
+ return this.getLatest().__centered;
29
+ }
30
+ setCentered(centered) {
31
+ const writable = this.getWritable();
32
+ writable.__centered = centered;
33
+ return writable;
34
+ }
35
+ static importJSON(serializedNode) {
36
+ return $createPoetryCoupletNode(serializedNode.centered);
37
+ }
38
+ exportJSON() {
39
+ return { ...super.exportJSON(), type: 'poetry-couplet', version: 1, centered: this.getCentered() };
40
+ }
41
+ static importDOM() {
42
+ return {
43
+ div: (domNode) => {
44
+ if (!domNode.hasAttribute('data-likhari-poetry-couplet'))
45
+ return null;
46
+ const centered = domNode.getAttribute('data-likhari-poetry-couplet-centered') === 'true';
47
+ return { conversion: () => ({ node: $createPoetryCoupletNode(centered) }), priority: 2 };
48
+ },
49
+ };
50
+ }
51
+ exportDOM() {
52
+ const element = document.createElement('div');
53
+ element.setAttribute('data-likhari-poetry-couplet', '');
54
+ if (this.getCentered())
55
+ element.setAttribute('data-likhari-poetry-couplet-centered', 'true');
56
+ return { element };
57
+ }
58
+ createDOM(config) {
59
+ const element = document.createElement('div');
60
+ const base = config.theme.poetryCouplet ?? 'likhari-poetry-couplet';
61
+ addClassNamesToElement(element, base);
62
+ if (this.__centered)
63
+ element.classList.add(`${base}--centered`);
64
+ return element;
65
+ }
66
+ updateDOM(prevNode, dom, config) {
67
+ if (prevNode.__centered !== this.__centered) {
68
+ const base = config.theme.poetryCouplet ?? 'likhari-poetry-couplet';
69
+ dom.classList.toggle(`${base}--centered`, this.__centered);
70
+ }
71
+ return false;
72
+ }
73
+ canBeEmpty() {
74
+ return false;
75
+ }
76
+ canIndent() {
77
+ return false;
78
+ }
79
+ }
80
+ export function $createPoetryCoupletNode(centered = false) {
81
+ return $applyNodeReplacement(new PoetryCoupletNode(centered));
82
+ }
83
+ export function $isPoetryCoupletNode(node) {
84
+ return node instanceof PoetryCoupletNode;
85
+ }
@@ -1,5 +1,5 @@
1
1
  import { type DOMConversionMap, type DOMExportOutput, type EditorConfig, type LexicalNode, type NodeKey, type SerializedElementNode, type Spread, ElementNode } from 'lexical';
2
- export type PoetryLayout = 'single' | 'two-column';
2
+ export type PoetryLayout = 'single' | 'two-column' | 'alternating';
3
3
  export type PoetryAlign = 'justify' | 'left' | 'right' | 'start';
4
4
  export type SerializedPoetryBlockNode = Spread<{
5
5
  layout: PoetryLayout;
@@ -7,14 +7,18 @@ export type SerializedPoetryBlockNode = Spread<{
7
7
  width?: number;
8
8
  }, SerializedElementNode>;
9
9
  /**
10
- * A couplet (two-line verse unit), the one extensible primitive requirements
11
- * doc §4.11 recommends in place of four fixed templates. `layout` picks
12
- * single-column (two stacked misras) vs. two-column (built on the columns
13
- * primitive, §4.9 — see blocks/poetryActions.ts for how children are
14
- * restructured between the two); `align` is a literal per-instance override,
15
- * independent of the document's own alignment setting (architecture doc
16
- * §3.1). Both mutate in place via poetryActions so converting a couplet
17
- * between layouts doesn't lose selection/undo coherence.
10
+ * A poetry *section*: one or more couplets (two-line verse units), each
11
+ * wrapped as either a PoetryCoupletNode (single-column/alternating — see
12
+ * that file) or a LayoutContainerNode row (two-column, built on the
13
+ * columns primitive, §4.9) — see blocks/poetryActions.ts's $getCouplets
14
+ * for how a block's children (a free mix of either wrapper, per couplet)
15
+ * are read, and $setPoetryLayout/$exitPoetryOnEnter for how they're
16
+ * restructured/grown. `layout` is this block's *default* for couplets
17
+ * appended to it (a PoetryCoupletNode can still individually override to
18
+ * "centered" regardless); `align` applies to the whole section. Both
19
+ * mutate in place via poetryActions so converting a section between
20
+ * layouts doesn't lose selection/undo coherence. One extensible primitive
21
+ * in place of four fixed templates, per requirements doc §4.11.
18
22
  */
19
23
  export declare class PoetryBlockNode extends ElementNode {
20
24
  __layout: PoetryLayout;
@@ -1 +1 @@
1
- {"version":3,"file":"PoetryNode.d.ts","sourceRoot":"","sources":["../../src/blocks/PoetryNode.tsx"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,qBAAqB,EAC1B,KAAK,MAAM,EACX,WAAW,EACZ,MAAM,SAAS,CAAC;AAEjB,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,YAAY,CAAC;AACnD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAwBjE,MAAM,MAAM,yBAAyB,GAAG,MAAM,CAAC;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,WAAW,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,qBAAqB,CAAC,CAAC;AAEpI;;;;;;;;;GASG;AACH,qBAAa,eAAgB,SAAQ,WAAW;IAC9C,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB;8DAC0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;gBAEL,MAAM,GAAE,YAAuB,EAAE,KAAK,GAAE,WAAuB,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO;IAO1G,MAAM,CAAC,OAAO,IAAI,MAAM;IAIxB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe;IAIpD,SAAS,IAAI,YAAY;IAIzB,QAAQ,IAAI,WAAW;IAIvB,QAAQ,IAAI,MAAM,GAAG,SAAS;IAI9B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAMzC;;gEAE4D;IAC5D,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAM9C,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAMlC,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,yBAAyB,GAAG,eAAe;IAI7E,UAAU,IAAI,yBAAyB;IAYvC,MAAM,CAAC,SAAS,IAAI,gBAAgB,GAAG,IAAI;IAe3C,SAAS,IAAI,eAAe;IAW5B,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW;IAa5C,SAAS,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO;IAerF,UAAU,IAAI,KAAK;IAInB,YAAY,IAAI,OAAO;CAGxB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,GAAE,YAAuB,EAAE,KAAK,GAAE,WAAuB,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,eAAe,CAEvI;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,IAAI,eAAe,CAEhG"}
1
+ {"version":3,"file":"PoetryNode.d.ts","sourceRoot":"","sources":["../../src/blocks/PoetryNode.tsx"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,qBAAqB,EAC1B,KAAK,MAAM,EACX,WAAW,EACZ,MAAM,SAAS,CAAC;AAEjB,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,YAAY,GAAG,aAAa,CAAC;AACnE,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAwBjE,MAAM,MAAM,yBAAyB,GAAG,MAAM,CAAC;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,WAAW,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,qBAAqB,CAAC,CAAC;AAEpI;;;;;;;;;;;;;GAaG;AACH,qBAAa,eAAgB,SAAQ,WAAW;IAC9C,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB;8DAC0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;gBAEL,MAAM,GAAE,YAAuB,EAAE,KAAK,GAAE,WAAuB,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO;IAO1G,MAAM,CAAC,OAAO,IAAI,MAAM;IAIxB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe;IAIpD,SAAS,IAAI,YAAY;IAIzB,QAAQ,IAAI,WAAW;IAIvB,QAAQ,IAAI,MAAM,GAAG,SAAS;IAI9B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAMzC;;gEAE4D;IAC5D,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAM9C,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAMlC,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,yBAAyB,GAAG,eAAe;IAI7E,UAAU,IAAI,yBAAyB;IAYvC,MAAM,CAAC,SAAS,IAAI,gBAAgB,GAAG,IAAI;IAe3C,SAAS,IAAI,eAAe;IAW5B,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW;IAa5C,SAAS,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO;IAerF,UAAU,IAAI,KAAK;IAInB,YAAY,IAAI,OAAO;CAGxB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,GAAE,YAAuB,EAAE,KAAK,GAAE,WAAuB,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,eAAe,CAEvI;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,IAAI,eAAe,CAEhG"}
@@ -1,6 +1,6 @@
1
1
  import { addClassNamesToElement } from '@lexical/utils';
2
2
  import { $applyNodeReplacement, ElementNode, } from 'lexical';
3
- const VALID_LAYOUTS = new Set(['single', 'two-column']);
3
+ const VALID_LAYOUTS = new Set(['single', 'two-column', 'alternating']);
4
4
  const VALID_ALIGNS = new Set(['justify', 'left', 'right', 'start']);
5
5
  /** `text-align: justify` only stretches a line that actually wraps — a
6
6
  * misra short enough to fit on one line would otherwise just sit at its own
@@ -22,14 +22,18 @@ function applyWidthStyle(element, width) {
22
22
  element.style.maxWidth = `${width}px`;
23
23
  }
24
24
  /**
25
- * A couplet (two-line verse unit), the one extensible primitive requirements
26
- * doc §4.11 recommends in place of four fixed templates. `layout` picks
27
- * single-column (two stacked misras) vs. two-column (built on the columns
28
- * primitive, §4.9 — see blocks/poetryActions.ts for how children are
29
- * restructured between the two); `align` is a literal per-instance override,
30
- * independent of the document's own alignment setting (architecture doc
31
- * §3.1). Both mutate in place via poetryActions so converting a couplet
32
- * between layouts doesn't lose selection/undo coherence.
25
+ * A poetry *section*: one or more couplets (two-line verse units), each
26
+ * wrapped as either a PoetryCoupletNode (single-column/alternating — see
27
+ * that file) or a LayoutContainerNode row (two-column, built on the
28
+ * columns primitive, §4.9) — see blocks/poetryActions.ts's $getCouplets
29
+ * for how a block's children (a free mix of either wrapper, per couplet)
30
+ * are read, and $setPoetryLayout/$exitPoetryOnEnter for how they're
31
+ * restructured/grown. `layout` is this block's *default* for couplets
32
+ * appended to it (a PoetryCoupletNode can still individually override to
33
+ * "centered" regardless); `align` applies to the whole section. Both
34
+ * mutate in place via poetryActions so converting a section between
35
+ * layouts doesn't lose selection/undo coherence. One extensible primitive
36
+ * in place of four fixed templates, per requirements doc §4.11.
33
37
  */
34
38
  export class PoetryBlockNode extends ElementNode {
35
39
  constructor(layout = 'single', align = 'justify', width, key) {
@@ -39,7 +43,7 @@ export class PoetryBlockNode extends ElementNode {
39
43
  this.__width = width;
40
44
  }
41
45
  static getType() {
42
- return 'poetry-couplet';
46
+ return 'poetry-block';
43
47
  }
44
48
  static clone(node) {
45
49
  return new PoetryBlockNode(node.__layout, node.__align, node.__width, node.__key);
@@ -78,7 +82,7 @@ export class PoetryBlockNode extends ElementNode {
78
82
  const width = this.getWidth();
79
83
  return {
80
84
  ...super.exportJSON(),
81
- type: 'poetry-couplet',
85
+ type: 'poetry-block',
82
86
  version: 1,
83
87
  layout: this.getLayout(),
84
88
  align: this.getAlign(),
@@ -1,55 +1,100 @@
1
1
  import { type ParagraphNode } from 'lexical';
2
2
  import { type PoetryAlign, type PoetryLayout, type PoetryBlockNode } from './PoetryNode.js';
3
- /** The couplet's two misra paragraphs, in document order, regardless of
4
- * which layout currently holds them. */
3
+ /** One couplet's two misra paragraphs, in order. */
4
+ export type Couplet = [ParagraphNode, ParagraphNode];
5
+ /**
6
+ * A poetry block's children are a free mix, per couplet, of either a
7
+ * PoetryCoupletNode (single-column/alternating, optionally `centered`) or a
8
+ * LayoutContainerNode row (two-column) — "two columns with a couplet that
9
+ * is single, aligned centered" means exactly this: some couplets two-column,
10
+ * others centered singles, in the same block. This walks that mixed
11
+ * structure and returns each couplet as a misra pair, in document order —
12
+ * the one place that knows how to read either wrapper, so nothing else does.
13
+ */
14
+ export declare function $getCouplets(node: PoetryBlockNode): Couplet[];
15
+ /** Every misra in the block, flattened across all its couplets — for
16
+ * operations (like $setPoetryLayout) that don't care about couplet
17
+ * boundaries, just the full set of lines. */
5
18
  export declare function $getMisraParagraphs(node: PoetryBlockNode): ParagraphNode[];
6
19
  /** The PoetryBlockNode the current selection is inside, or null. */
7
20
  export declare function $getPoetryBlockFromSelection(): PoetryBlockNode | null;
8
21
  /**
9
- * Converts a couplet between single-column (two stacked misras) and
10
- * two-column (built on the columns primitive, §4.9) in place — pulls the
11
- * two misra paragraphs out of whichever shape currently holds them and
12
- * rebuilds the other shape around the same paragraph nodes, so content,
13
- * selection and undo history all survive the conversion.
22
+ * Converts every couplet in the block between two-column (built on the
23
+ * columns primitive, §4.9) and single-column/alternating (a
24
+ * PoetryCoupletNode per couplet) — pulls each couplet's two misra
25
+ * paragraphs out of whichever shape currently holds them and rebuilds the
26
+ * other shape around the same paragraph nodes, preserving couplet order
27
+ * (and dropping any individual 'centered' override, which only applies to
28
+ * the non-two-column shape), so content, selection and undo history all
29
+ * survive the conversion. Switching between 'single' and 'alternating' is
30
+ * purely an attribute change (both use the same PoetryCoupletNode
31
+ * structure) — no restructuring needed.
14
32
  */
15
33
  export declare function $setPoetryLayout(node: PoetryBlockNode, layout: PoetryLayout): void;
16
- /** A couplet at the very end of the document would otherwise leave no
34
+ /**
35
+ * Sets whether the couplet the selection is inside renders as a narrower,
36
+ * centered box ("single, aligned centered") instead of the block's default
37
+ * width — independent of the block's own layout. A two-column couplet is
38
+ * converted into a PoetryCoupletNode first (its two columns stacked into
39
+ * two lines), since "centered" only applies to that shape; a couplet
40
+ * that's already a PoetryCoupletNode just has its flag flipped. Returns
41
+ * false when the selection isn't inside a couplet.
42
+ */
43
+ export declare function $setCoupletCentered(centered: boolean): boolean;
44
+ /** Whether the couplet the selection is inside is currently centered —
45
+ * always false for a two-column couplet, since that override only applies
46
+ * to the PoetryCoupletNode shape. Null when the selection isn't inside a
47
+ * couplet at all (vs. inside one that just isn't centered). */
48
+ export declare function $getCoupletCenteredFromSelection(): boolean | null;
49
+ /** A block at the very end of the document would otherwise leave no
17
50
  * editable block to click or arrow down into — appends one empty paragraph
18
- * if the couplet doesn't already have a next sibling. Idempotent: once a
51
+ * if the block doesn't already have a next sibling. Idempotent: once a
19
52
  * sibling exists (that paragraph, or anything else), this is a no-op, so
20
53
  * it's safe to call from both the insert command and a node transform that
21
54
  * keeps the invariant even after later edits (e.g. deleting the trailing
22
55
  * paragraph, or JSON content loaded without one). */
23
56
  export declare function $ensureTrailingParagraph(node: PoetryBlockNode): void;
24
- /** Inserts a new two-misra couplet after the selection's top-level block,
25
- * in `layout`/`align`, and focuses its first misra. */
57
+ /**
58
+ * Inserts a poetry couplet at the caret. If the caret is already inside a
59
+ * poetry block, appends a new couplet to *that* block (in its existing
60
+ * default layout) and focuses its first misra. Otherwise creates a new
61
+ * block (in `layout`/`align`) with one couplet, after the selection's
62
+ * top-level element.
63
+ */
26
64
  export declare function $insertPoetryCouplet(layout: PoetryLayout, align: PoetryAlign): boolean;
27
- /** Removes the whole couplet the selection is inside, if any — the explicit,
28
- * always-available counterpart to $deletePoetryOnBackspace's empty-couplet
29
- * shortcut (mirrors the table menu's own "Delete table" action). */
65
+ /** Removes the couplet the selection is inside. If it's the block's only
66
+ * couplet, removes the whole block (an empty poetry section serves no
67
+ * purpose) — the explicit, always-available counterpart to
68
+ * $deletePoetryOnBackspace's empty-couplet shortcut (mirrors the table
69
+ * menu's own "Delete table" action). */
30
70
  export declare function $deletePoetryCouplet(): boolean;
31
71
  /**
32
- * Backspace at the very start of an empty couplet's first misra removes the
33
- * whole block — the natural keystroke to try first, and otherwise there was
34
- * no way to get rid of an empty couplet created by mistake (its two misra
35
- * paragraphs each have their own default canBeEmpty, so Lexical's usual
36
- * "delete the empty block" handling never reaches the couplet itself).
37
- * Returns false for every other caret position, including a non-empty
38
- * couplet, so normal Backspace handling (e.g. deleting within text) proceeds.
72
+ * Backspace at the very start of an empty couplet removes just that
73
+ * couplet — merging back into the previous one's end, or (if it's the
74
+ * block's only couplet) removing the whole block, since an empty poetry
75
+ * section serves no purpose. Otherwise there was no way to get rid of an
76
+ * empty couplet created by mistake (its misra paragraphs each have their
77
+ * own default canBeEmpty, so Lexical's usual "delete the empty block"
78
+ * handling never reaches the couplet itself). Returns false for every
79
+ * other caret position, including a non-empty couplet, so normal Backspace
80
+ * handling (e.g. deleting within text) proceeds.
39
81
  */
40
82
  export declare function $deletePoetryOnBackspace(): boolean;
41
83
  /**
42
- * A couplet is always exactly two misras — Enter must never grow it past
43
- * that. Pressed anywhere in the first misra, Enter moves the caret to the
44
- * second (the line already exists; nothing is inserted). Pressed anywhere
45
- * in the second (the last), Enter exits: ParagraphNode's own
46
- * insertNewAfter would otherwise insert the new paragraph as a sibling of
47
- * the misra — i.e. still inside the couplet, growing it to three lines
48
- * and, on the next Enter, four, and so on — so that's handled here
49
- * instead, reusing or creating a plain paragraph after the whole couplet.
50
- * Intercepts Enter at *any* caret position inside the couplet (not just
51
- * at an edge) specifically to prevent that growth; returns false only
52
- * when the selection isn't inside a couplet at all.
84
+ * Enter inside a poetry block never grows a couplet past its fixed two
85
+ * misras. The block's *last* couplet being completely empty is always the
86
+ * exit trigger, regardless of which of its two (both-empty) misras the
87
+ * caret happens to be on — pressing Enter once on a non-empty last couplet
88
+ * appends a fresh empty one to continue into ("new couplet after last"),
89
+ * and pressing Enter again on that still-untouched couplet exits ("double
90
+ * enter to exit the block"): the same empty-item convention most editors
91
+ * use for lists. Outside that case: pressed in a couplet's first misra,
92
+ * Enter moves the caret to the second (already there; nothing inserted —
93
+ * this is what stops ParagraphNode's default insertNewAfter from growing
94
+ * the couplet, which would otherwise insert the new paragraph as a sibling
95
+ * of wherever the caret was, i.e. still inside the couplet); pressed in
96
+ * the last misra of a couplet that isn't the block's last, it moves to the
97
+ * next couplet's first misra.
53
98
  */
54
99
  export declare function $exitPoetryOnEnter(): boolean;
55
100
  //# sourceMappingURL=poetryActions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"poetryActions.d.ts","sourceRoot":"","sources":["../../src/blocks/poetryActions.ts"],"names":[],"mappings":"AACA,OAAO,EAA4F,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAEvI,OAAO,EAA8C,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAErI;wCACwC;AACxC,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,eAAe,GAAG,aAAa,EAAE,CAW1E;AAED,oEAAoE;AACpE,wBAAgB,4BAA4B,IAAI,eAAe,GAAG,IAAI,CAKrE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAclF;AAED;;;;;;qDAMqD;AACrD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CAEpE;AAED;uDACuD;AACvD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAyBtF;AAED;;oEAEoE;AACpE,wBAAgB,oBAAoB,IAAI,OAAO,CAK9C;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,IAAI,OAAO,CAoBlD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAyB5C"}
1
+ {"version":3,"file":"poetryActions.d.ts","sourceRoot":"","sources":["../../src/blocks/poetryActions.ts"],"names":[],"mappings":"AACA,OAAO,EAA4F,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAGvI,OAAO,EAA8C,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAErI,oDAAoD;AACpD,MAAM,MAAM,OAAO,GAAG,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;AAErD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,EAAE,CAc7D;AAED;;6CAE6C;AAC7C,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,eAAe,GAAG,aAAa,EAAE,CAE1E;AAED,oEAAoE;AACpE,wBAAgB,4BAA4B,IAAI,eAAe,GAAG,IAAI,CAKrE;AAyCD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CA8BlF;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CA4B9D;AAED;;;+DAG+D;AAC/D,wBAAgB,gCAAgC,IAAI,OAAO,GAAG,IAAI,CAYjE;AAED;;;;;;qDAMqD;AACrD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CAEpE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAmBtF;AAED;;;;wCAIwC;AACxC,wBAAgB,oBAAoB,IAAI,OAAO,CAkB9C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,IAAI,OAAO,CA8BlD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAyC5C"}
@@ -1,21 +1,40 @@
1
1
  import { $findMatchingParent } from '@lexical/utils';
2
2
  import { $createParagraphNode, $getSelection, $isElementNode, $isParagraphNode, $isRangeSelection } from 'lexical';
3
3
  import { $createLayoutContainerNode, $createLayoutItemNode, $isLayoutContainerNode, $isLayoutItemNode } from './LayoutNode.js';
4
+ import { $createPoetryCoupletNode, $isPoetryCoupletNode } from './PoetryCoupletNode.js';
4
5
  import { $createPoetryBlockNode, $isPoetryBlockNode } from './PoetryNode.js';
5
- /** The couplet's two misra paragraphs, in document order, regardless of
6
- * which layout currently holds them. */
7
- export function $getMisraParagraphs(node) {
8
- if (node.getLayout() === 'single') {
9
- return node.getChildren().filter($isParagraphNode);
6
+ /**
7
+ * A poetry block's children are a free mix, per couplet, of either a
8
+ * PoetryCoupletNode (single-column/alternating, optionally `centered`) or a
9
+ * LayoutContainerNode row (two-column) — "two columns with a couplet that
10
+ * is single, aligned centered" means exactly this: some couplets two-column,
11
+ * others centered singles, in the same block. This walks that mixed
12
+ * structure and returns each couplet as a misra pair, in document order —
13
+ * the one place that knows how to read either wrapper, so nothing else does.
14
+ */
15
+ export function $getCouplets(node) {
16
+ const couplets = [];
17
+ for (const child of node.getChildren()) {
18
+ if ($isPoetryCoupletNode(child)) {
19
+ const [a, b] = child.getChildren().filter($isParagraphNode);
20
+ if (a && b)
21
+ couplets.push([a, b]);
22
+ }
23
+ else if ($isLayoutContainerNode(child)) {
24
+ const items = child.getChildren().filter($isLayoutItemNode);
25
+ const a = items[0]?.getFirstChild();
26
+ const b = items[1]?.getFirstChild();
27
+ if ($isParagraphNode(a) && $isParagraphNode(b))
28
+ couplets.push([a, b]);
29
+ }
10
30
  }
11
- const container = node.getChildren().find($isLayoutContainerNode);
12
- if (!container)
13
- return [];
14
- return container
15
- .getChildren()
16
- .filter($isLayoutItemNode)
17
- .map((item) => item.getFirstChild())
18
- .filter($isParagraphNode);
31
+ return couplets;
32
+ }
33
+ /** Every misra in the block, flattened across all its couplets — for
34
+ * operations (like $setPoetryLayout) that don't care about couplet
35
+ * boundaries, just the full set of lines. */
36
+ export function $getMisraParagraphs(node) {
37
+ return $getCouplets(node).flat();
19
38
  }
20
39
  /** The PoetryBlockNode the current selection is inside, or null. */
21
40
  export function $getPoetryBlockFromSelection() {
@@ -25,36 +44,143 @@ export function $getPoetryBlockFromSelection() {
25
44
  const match = $findMatchingParent(selection.anchor.getNode(), $isPoetryBlockNode);
26
45
  return match ?? null;
27
46
  }
47
+ /** Which couplet (and which of its two misras, 0 or 1) a node sits inside,
48
+ * or null if it's in neither — e.g. on the block's own boundary. */
49
+ function $findCoupletPosition(couplets, target) {
50
+ for (let c = 0; c < couplets.length; c++) {
51
+ for (const misraIndex of [0, 1]) {
52
+ const misra = couplets[c][misraIndex];
53
+ if (misra.getKey() === target.getKey() || misra.isParentOf(target))
54
+ return { coupletIndex: c, misraIndex };
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+ /** A couplet's own wrapper node (PoetryCoupletNode or LayoutContainerNode) —
60
+ * every couplet has exactly one, so removing/replacing a couplet means
61
+ * operating on this, not on its misra paragraphs individually. */
62
+ function $findCoupletWrapper(misra) {
63
+ return $findMatchingParent(misra, (n) => $isPoetryCoupletNode(n) || $isLayoutContainerNode(n));
64
+ }
65
+ /** Appends a new couplet to the block, in its default layout (two-column
66
+ * for a 'two-column' block, a plain PoetryCoupletNode otherwise) unless
67
+ * `twoColumn` is given explicitly. */
68
+ function $appendCoupletTo(node, twoColumn) {
69
+ const misraA = $createParagraphNode();
70
+ const misraB = $createParagraphNode();
71
+ const useTwoColumn = twoColumn ?? node.getLayout() === 'two-column';
72
+ if (useTwoColumn) {
73
+ const container = $createLayoutContainerNode('repeat(2, 1fr)');
74
+ container.append($createLayoutItemNode().append(misraA), $createLayoutItemNode().append(misraB));
75
+ node.append(container);
76
+ }
77
+ else {
78
+ node.append($createPoetryCoupletNode().append(misraA, misraB));
79
+ }
80
+ return [misraA, misraB];
81
+ }
28
82
  /**
29
- * Converts a couplet between single-column (two stacked misras) and
30
- * two-column (built on the columns primitive, §4.9) in place — pulls the
31
- * two misra paragraphs out of whichever shape currently holds them and
32
- * rebuilds the other shape around the same paragraph nodes, so content,
33
- * selection and undo history all survive the conversion.
83
+ * Converts every couplet in the block between two-column (built on the
84
+ * columns primitive, §4.9) and single-column/alternating (a
85
+ * PoetryCoupletNode per couplet) — pulls each couplet's two misra
86
+ * paragraphs out of whichever shape currently holds them and rebuilds the
87
+ * other shape around the same paragraph nodes, preserving couplet order
88
+ * (and dropping any individual 'centered' override, which only applies to
89
+ * the non-two-column shape), so content, selection and undo history all
90
+ * survive the conversion. Switching between 'single' and 'alternating' is
91
+ * purely an attribute change (both use the same PoetryCoupletNode
92
+ * structure) — no restructuring needed.
34
93
  */
35
94
  export function $setPoetryLayout(node, layout) {
36
- if (node.getLayout() === layout)
95
+ const from = node.getLayout();
96
+ if (from === layout)
97
+ return;
98
+ const fromTwoColumn = from === 'two-column';
99
+ const toTwoColumn = layout === 'two-column';
100
+ if (fromTwoColumn === toTwoColumn) {
101
+ // Both 'single' and 'alternating' share the same PoetryCoupletNode
102
+ // structure — just relabel the block.
103
+ node.setLayoutAttribute(layout);
37
104
  return;
38
- const misras = $getMisraParagraphs(node);
39
- for (const misra of misras)
40
- misra.remove();
105
+ }
106
+ const couplets = $getCouplets(node);
107
+ for (const [a, b] of couplets) {
108
+ a.remove();
109
+ b.remove();
110
+ }
41
111
  for (const leftover of node.getChildren())
42
112
  leftover.remove();
43
- if (layout === 'single') {
44
- for (const misra of misras)
45
- node.append(misra);
46
- }
47
- else {
48
- const container = $createLayoutContainerNode('repeat(2, 1fr)');
49
- for (const misra of misras)
50
- container.append($createLayoutItemNode().append(misra));
51
- node.append(container);
113
+ for (const [a, b] of couplets) {
114
+ if (toTwoColumn) {
115
+ const container = $createLayoutContainerNode('repeat(2, 1fr)');
116
+ container.append($createLayoutItemNode().append(a), $createLayoutItemNode().append(b));
117
+ node.append(container);
118
+ }
119
+ else {
120
+ node.append($createPoetryCoupletNode().append(a, b));
121
+ }
52
122
  }
53
123
  node.setLayoutAttribute(layout);
54
124
  }
55
- /** A couplet at the very end of the document would otherwise leave no
125
+ /**
126
+ * Sets whether the couplet the selection is inside renders as a narrower,
127
+ * centered box ("single, aligned centered") instead of the block's default
128
+ * width — independent of the block's own layout. A two-column couplet is
129
+ * converted into a PoetryCoupletNode first (its two columns stacked into
130
+ * two lines), since "centered" only applies to that shape; a couplet
131
+ * that's already a PoetryCoupletNode just has its flag flipped. Returns
132
+ * false when the selection isn't inside a couplet.
133
+ */
134
+ export function $setCoupletCentered(centered) {
135
+ const block = $getPoetryBlockFromSelection();
136
+ if (!block)
137
+ return false;
138
+ const selection = $getSelection();
139
+ if (!$isRangeSelection(selection))
140
+ return false;
141
+ const couplets = $getCouplets(block);
142
+ const position = $findCoupletPosition(couplets, selection.anchor.getNode());
143
+ if (!position)
144
+ return false;
145
+ const [a, b] = couplets[position.coupletIndex];
146
+ const wrapper = $findCoupletWrapper(a);
147
+ if (!wrapper)
148
+ return false;
149
+ if ($isPoetryCoupletNode(wrapper)) {
150
+ wrapper.setCentered(centered);
151
+ return true;
152
+ }
153
+ // A two-column couplet: stack its two columns into a centered pair,
154
+ // replacing the LayoutContainerNode row with a PoetryCoupletNode in the
155
+ // same position.
156
+ a.remove();
157
+ b.remove();
158
+ const coupletNode = $createPoetryCoupletNode(centered).append(a, b);
159
+ wrapper.insertAfter(coupletNode);
160
+ wrapper.remove();
161
+ return true;
162
+ }
163
+ /** Whether the couplet the selection is inside is currently centered —
164
+ * always false for a two-column couplet, since that override only applies
165
+ * to the PoetryCoupletNode shape. Null when the selection isn't inside a
166
+ * couplet at all (vs. inside one that just isn't centered). */
167
+ export function $getCoupletCenteredFromSelection() {
168
+ const block = $getPoetryBlockFromSelection();
169
+ if (!block)
170
+ return null;
171
+ const selection = $getSelection();
172
+ if (!$isRangeSelection(selection))
173
+ return null;
174
+ const couplets = $getCouplets(block);
175
+ const position = $findCoupletPosition(couplets, selection.anchor.getNode());
176
+ if (!position)
177
+ return null;
178
+ const wrapper = $findCoupletWrapper(couplets[position.coupletIndex][0]);
179
+ return $isPoetryCoupletNode(wrapper) ? wrapper.getCentered() : false;
180
+ }
181
+ /** A block at the very end of the document would otherwise leave no
56
182
  * editable block to click or arrow down into — appends one empty paragraph
57
- * if the couplet doesn't already have a next sibling. Idempotent: once a
183
+ * if the block doesn't already have a next sibling. Idempotent: once a
58
184
  * sibling exists (that paragraph, or anything else), this is a no-op, so
59
185
  * it's safe to call from both the insert command and a node transform that
60
186
  * keeps the invariant even after later edits (e.g. deleting the trailing
@@ -63,111 +189,150 @@ export function $ensureTrailingParagraph(node) {
63
189
  if (node.getNextSibling() === null)
64
190
  node.insertAfter($createParagraphNode());
65
191
  }
66
- /** Inserts a new two-misra couplet after the selection's top-level block,
67
- * in `layout`/`align`, and focuses its first misra. */
192
+ /**
193
+ * Inserts a poetry couplet at the caret. If the caret is already inside a
194
+ * poetry block, appends a new couplet to *that* block (in its existing
195
+ * default layout) and focuses its first misra. Otherwise creates a new
196
+ * block (in `layout`/`align`) with one couplet, after the selection's
197
+ * top-level element.
198
+ */
68
199
  export function $insertPoetryCouplet(layout, align) {
69
200
  const selection = $getSelection();
70
201
  if (!$isRangeSelection(selection))
71
202
  return false;
72
- // getTopLevelElement() stops at the nearest shadow root, and a
73
- // PoetryBlockNode is one — so from inside an existing couplet it resolves
74
- // to the misra paragraph itself, not the couplet. Without this check, a
75
- // second insert lands as a sibling of that misra (nested inside the first
76
- // couplet) instead of after the couplet as a whole.
77
- const anchorTopLevel = $getPoetryBlockFromSelection() ?? selection.anchor.getNode().getTopLevelElementOrThrow();
78
- const node = $createPoetryBlockNode(layout, align);
79
- const misraA = $createParagraphNode();
80
- const misraB = $createParagraphNode();
81
- if (layout === 'single') {
82
- node.append(misraA, misraB);
83
- }
84
- else {
85
- const container = $createLayoutContainerNode('repeat(2, 1fr)');
86
- container.append($createLayoutItemNode().append(misraA), $createLayoutItemNode().append(misraB));
87
- node.append(container);
203
+ const existing = $getPoetryBlockFromSelection();
204
+ if (existing) {
205
+ const [misraA] = $appendCoupletTo(existing);
206
+ misraA.selectStart();
207
+ return true;
88
208
  }
209
+ const anchorTopLevel = selection.anchor.getNode().getTopLevelElementOrThrow();
210
+ const node = $createPoetryBlockNode(layout, align);
211
+ const [misraA] = $appendCoupletTo(node, layout === 'two-column');
89
212
  anchorTopLevel.insertAfter(node);
90
213
  $ensureTrailingParagraph(node);
91
214
  misraA.selectStart();
92
215
  return true;
93
216
  }
94
- /** Removes the whole couplet the selection is inside, if any — the explicit,
95
- * always-available counterpart to $deletePoetryOnBackspace's empty-couplet
96
- * shortcut (mirrors the table menu's own "Delete table" action). */
217
+ /** Removes the couplet the selection is inside. If it's the block's only
218
+ * couplet, removes the whole block (an empty poetry section serves no
219
+ * purpose) — the explicit, always-available counterpart to
220
+ * $deletePoetryOnBackspace's empty-couplet shortcut (mirrors the table
221
+ * menu's own "Delete table" action). */
97
222
  export function $deletePoetryCouplet() {
98
- const couplet = $getPoetryBlockFromSelection();
99
- if (!couplet)
223
+ const block = $getPoetryBlockFromSelection();
224
+ if (!block)
100
225
  return false;
101
- couplet.remove();
226
+ const selection = $getSelection();
227
+ const couplets = $getCouplets(block);
228
+ const anchorNode = $isRangeSelection(selection) ? selection.anchor.getNode() : null;
229
+ const position = anchorNode ? $findCoupletPosition(couplets, anchorNode) : null;
230
+ const target = position ? couplets[position.coupletIndex] : couplets[couplets.length - 1];
231
+ if (!target)
232
+ return false;
233
+ if (couplets.length <= 1) {
234
+ block.remove();
235
+ return true;
236
+ }
237
+ $findCoupletWrapper(target[0])?.remove();
102
238
  return true;
103
239
  }
104
240
  /**
105
- * Backspace at the very start of an empty couplet's first misra removes the
106
- * whole block — the natural keystroke to try first, and otherwise there was
107
- * no way to get rid of an empty couplet created by mistake (its two misra
108
- * paragraphs each have their own default canBeEmpty, so Lexical's usual
109
- * "delete the empty block" handling never reaches the couplet itself).
110
- * Returns false for every other caret position, including a non-empty
111
- * couplet, so normal Backspace handling (e.g. deleting within text) proceeds.
241
+ * Backspace at the very start of an empty couplet removes just that
242
+ * couplet — merging back into the previous one's end, or (if it's the
243
+ * block's only couplet) removing the whole block, since an empty poetry
244
+ * section serves no purpose. Otherwise there was no way to get rid of an
245
+ * empty couplet created by mistake (its misra paragraphs each have their
246
+ * own default canBeEmpty, so Lexical's usual "delete the empty block"
247
+ * handling never reaches the couplet itself). Returns false for every
248
+ * other caret position, including a non-empty couplet, so normal Backspace
249
+ * handling (e.g. deleting within text) proceeds.
112
250
  */
113
251
  export function $deletePoetryOnBackspace() {
114
252
  const selection = $getSelection();
115
253
  if (!$isRangeSelection(selection) || !selection.isCollapsed())
116
254
  return false;
117
- const couplet = $getPoetryBlockFromSelection();
118
- if (!couplet)
119
- return false;
120
- const misras = $getMisraParagraphs(couplet);
121
- const firstMisra = misras[0];
122
- if (!firstMisra)
255
+ const block = $getPoetryBlockFromSelection();
256
+ if (!block)
123
257
  return false;
124
- if (misras.some((m) => !m.isEmpty()))
258
+ const couplets = $getCouplets(block);
259
+ const anchorNode = selection.anchor.getNode();
260
+ const position = $findCoupletPosition(couplets, anchorNode);
261
+ if (!position || position.misraIndex !== 0)
125
262
  return false;
263
+ const [a, b] = couplets[position.coupletIndex];
126
264
  const anchor = selection.anchor;
127
- const isAtStart = anchor.key === firstMisra.getKey() && anchor.offset === 0;
265
+ const isAtStart = anchor.key === a.getKey() && anchor.offset === 0;
128
266
  if (!isAtStart)
129
267
  return false;
130
- const previous = couplet.getPreviousSibling();
131
- couplet.remove();
132
- if ($isElementNode(previous))
133
- previous.selectEnd();
268
+ if (!a.isEmpty() || !b.isEmpty())
269
+ return false;
270
+ if (couplets.length <= 1) {
271
+ const previous = block.getPreviousSibling();
272
+ block.remove();
273
+ if ($isElementNode(previous))
274
+ previous.selectEnd();
275
+ return true;
276
+ }
277
+ $findCoupletWrapper(a)?.remove();
278
+ const remaining = $getCouplets(block);
279
+ const previousCouplet = remaining[position.coupletIndex - 1] ?? remaining[0];
280
+ previousCouplet?.[1].selectEnd();
134
281
  return true;
135
282
  }
136
283
  /**
137
- * A couplet is always exactly two misras — Enter must never grow it past
138
- * that. Pressed anywhere in the first misra, Enter moves the caret to the
139
- * second (the line already exists; nothing is inserted). Pressed anywhere
140
- * in the second (the last), Enter exits: ParagraphNode's own
141
- * insertNewAfter would otherwise insert the new paragraph as a sibling of
142
- * the misra — i.e. still inside the couplet, growing it to three lines
143
- * and, on the next Enter, four, and so on — so that's handled here
144
- * instead, reusing or creating a plain paragraph after the whole couplet.
145
- * Intercepts Enter at *any* caret position inside the couplet (not just
146
- * at an edge) specifically to prevent that growth; returns false only
147
- * when the selection isn't inside a couplet at all.
284
+ * Enter inside a poetry block never grows a couplet past its fixed two
285
+ * misras. The block's *last* couplet being completely empty is always the
286
+ * exit trigger, regardless of which of its two (both-empty) misras the
287
+ * caret happens to be on — pressing Enter once on a non-empty last couplet
288
+ * appends a fresh empty one to continue into ("new couplet after last"),
289
+ * and pressing Enter again on that still-untouched couplet exits ("double
290
+ * enter to exit the block"): the same empty-item convention most editors
291
+ * use for lists. Outside that case: pressed in a couplet's first misra,
292
+ * Enter moves the caret to the second (already there; nothing inserted —
293
+ * this is what stops ParagraphNode's default insertNewAfter from growing
294
+ * the couplet, which would otherwise insert the new paragraph as a sibling
295
+ * of wherever the caret was, i.e. still inside the couplet); pressed in
296
+ * the last misra of a couplet that isn't the block's last, it moves to the
297
+ * next couplet's first misra.
148
298
  */
149
299
  export function $exitPoetryOnEnter() {
150
300
  const selection = $getSelection();
151
301
  if (!$isRangeSelection(selection) || !selection.isCollapsed())
152
302
  return false;
153
- const couplet = $getPoetryBlockFromSelection();
154
- if (!couplet)
303
+ const block = $getPoetryBlockFromSelection();
304
+ if (!block)
155
305
  return false;
156
- const misras = $getMisraParagraphs(couplet);
157
- const anchorNode = selection.anchor.getNode();
158
- const misraIndex = misras.findIndex((m) => m.getKey() === anchorNode.getKey() || m.isParentOf(anchorNode));
159
- if (misraIndex !== -1 && misraIndex < misras.length - 1) {
160
- misras[misraIndex + 1].selectEnd();
306
+ const couplets = $getCouplets(block);
307
+ const position = $findCoupletPosition(couplets, selection.anchor.getNode());
308
+ if (!position)
309
+ return false;
310
+ const { coupletIndex, misraIndex } = position;
311
+ const [a, b] = couplets[coupletIndex];
312
+ const isLastCouplet = coupletIndex === couplets.length - 1;
313
+ const coupletEmpty = a.isEmpty() && b.isEmpty();
314
+ if (isLastCouplet && coupletEmpty) {
315
+ // Drop the couplet we ourselves auto-created on the previous Enter —
316
+ // unless it's the block's only couplet, in which case leave the
317
+ // section as the user's (empty) content rather than deleting it.
318
+ if (couplets.length > 1)
319
+ $findCoupletWrapper(a)?.remove();
320
+ const existingNext = block.getNextSibling();
321
+ const paragraph = $isParagraphNode(existingNext) ? existingNext : $createParagraphNode();
322
+ if (paragraph !== existingNext)
323
+ block.insertAfter(paragraph);
324
+ paragraph.select();
325
+ return true;
326
+ }
327
+ if (misraIndex === 0) {
328
+ b.selectEnd();
329
+ return true;
330
+ }
331
+ if (!isLastCouplet) {
332
+ couplets[coupletIndex + 1][0].selectEnd();
161
333
  return true;
162
334
  }
163
- // On the last misra (or misraIndex === -1, e.g. the caret landed on the
164
- // couplet's shadow-root boundary itself) — exit. $ensureTrailingParagraph
165
- // already guarantees a paragraph after every couplet; reuse it rather
166
- // than stacking a new empty one on repeated Enters.
167
- const existing = couplet.getNextSibling();
168
- const paragraph = $isParagraphNode(existing) ? existing : $createParagraphNode();
169
- if (paragraph !== existing)
170
- couplet.insertAfter(paragraph);
171
- paragraph.select();
335
+ const [newA] = $appendCoupletTo(block);
336
+ newA.selectStart();
172
337
  return true;
173
338
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AAiEA,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;AA4evD,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,+BA+jCxJ"}
1
+ {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AAuEA,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;AA0fvD,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,+BA0kCxJ"}
@@ -15,7 +15,7 @@ import { INSERT_PAGE_BREAK_COMMAND } from '../blocks/PageBreakNode.js';
15
15
  import { INSERT_LAYOUT_COMMAND } from '../blocks/LayoutNode.js';
16
16
  import { INSERT_FOOTNOTE_COMMAND } from '../blocks/FootnoteNode.js';
17
17
  import { $isPoetryBlockNode } from '../blocks/PoetryNode.js';
18
- import { $deletePoetryCouplet, $getPoetryBlockFromSelection, $setPoetryLayout } from '../blocks/poetryActions.js';
18
+ import { $deletePoetryCouplet, $getCoupletCenteredFromSelection, $getPoetryBlockFromSelection, $setCoupletCentered, $setPoetryLayout, } from '../blocks/poetryActions.js';
19
19
  import { INSERT_POETRY_COUPLET_COMMAND } from '../plugins/PoetryPlugin.js';
20
20
  import { TableDialog } from './TableDialog.js';
21
21
  import { LayoutDialog } from './LayoutDialog.js';
@@ -29,7 +29,7 @@ import { CANVAS_FONT_DEFAULTS, DEFAULT_FONT_OPTIONS, FONT_SIZES_PX } from '../fo
29
29
  import { setStyleProperty } from '../utils/style.js';
30
30
  import { useStrings } from '../i18n/useStrings.js';
31
31
  import { usePortalTarget } from '../PortalTargetContext.js';
32
- 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, IconColumns1, IconColumns2, IconNumber1Small, IconRowInsertBottom, IconRowInsertTop, IconRowRemove, IconTable, IconTableMinus, IconTrash, IconTableOptions, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
32
+ 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, IconArrowsShuffle, IconColumns1, IconColumns2, IconNumber1Small, IconRowInsertBottom, IconRowInsertTop, IconRowRemove, IconTable, IconTableMinus, IconTrash, IconTableOptions, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
33
33
  /** Sentinel for "no inline style": Lexical returns the default it is given when
34
34
  * the selection has none, so this tells "unstyled" (show the canvas default)
35
35
  * apart from "styled with a value the dropdown doesn't list" (show nothing). */
@@ -51,6 +51,7 @@ const INITIAL_STATE = {
51
51
  inPoetry: false,
52
52
  poetryLayout: 'single',
53
53
  poetryAlign: 'justify',
54
+ coupletCentered: false,
54
55
  canUndo: false,
55
56
  canRedo: false,
56
57
  };
@@ -162,10 +163,13 @@ function TableMenuItems({ strings, direction, rows, columns, canMergeCells, canU
162
163
  };
163
164
  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')] }));
164
165
  }
165
- /** The poetry couplet menu's contents — layout (single/two-column) and
166
- * literal alignment, both per-instance overrides (requirements doc §4.11:
167
- * "set per CoupletNode, not inherited from a document-wide setting"). */
168
- function PoetryMenuItems({ strings, alignIcons, layout, align, onAction, }) {
166
+ /** The poetry couplet menu's contents — block layout (single/two-column/
167
+ * alternating), literal alignment (both per-block overrides, requirements
168
+ * doc §4.11: "set per CoupletNode, not inherited from a document-wide
169
+ * setting"), and a per-couplet "centered" toggle independent of either —
170
+ * "two columns with a couplet that is single, aligned centered" mixes a
171
+ * centered couplet into an otherwise two-column block. */
172
+ function PoetryMenuItems({ strings, alignIcons, layout, align, centered, onAction, }) {
169
173
  const t = strings.poetryMenu;
170
174
  const a = strings.toolbar.alignOptions;
171
175
  const item = (icon, label, action, active = false, color) => {
@@ -180,7 +184,7 @@ function PoetryMenuItems({ strings, alignIcons, layout, align, onAction, }) {
180
184
  const setAlign = (next) => () => {
181
185
  $getPoetryBlockFromSelection()?.setAlign(next);
182
186
  };
183
- return (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { children: t.menuLabel }), item(IconColumns1, t.singleColumn, setLayout('single'), layout === 'single'), item(IconColumns2, t.twoColumn, setLayout('two-column'), layout === 'two-column'), _jsx(Menu.Divider, {}), item(alignIcons.justify ?? IconAlignJustified, a.justify, setAlign('justify'), align === 'justify'), item(alignIcons.start ?? IconAlignLeft, a.start, setAlign('start'), align === 'start'), item(IconAlignLeft, a.left, setAlign('left'), align === 'left'), item(IconAlignRight, a.right, setAlign('right'), align === 'right'), _jsx(Menu.Divider, {}), item(IconTrash, t.deleteCouplet, $deletePoetryCouplet, false, 'red')] }));
187
+ return (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { children: t.menuLabel }), item(IconColumns1, t.singleColumn, setLayout('single'), layout === 'single'), item(IconColumns2, t.twoColumn, setLayout('two-column'), layout === 'two-column'), item(IconArrowsShuffle, t.alternating, setLayout('alternating'), layout === 'alternating'), _jsx(Menu.Divider, {}), item(alignIcons.justify ?? IconAlignJustified, a.justify, setAlign('justify'), align === 'justify'), item(alignIcons.start ?? IconAlignLeft, a.start, setAlign('start'), align === 'start'), item(IconAlignLeft, a.left, setAlign('left'), align === 'left'), item(IconAlignRight, a.right, setAlign('right'), align === 'right'), _jsx(Menu.Divider, {}), item(IconAlignCenter, centered ? t.uncenterCouplet : t.centerCouplet, () => $setCoupletCentered(!centered)), _jsx(Menu.Divider, {}), item(IconTrash, t.deleteCouplet, $deletePoetryCouplet, false, 'red')] }));
184
188
  }
185
189
  export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAULT_FONT_OPTIONS, direction = 'ltr', locale = 'en', drafts }) {
186
190
  const [editor] = useLexicalComposerContext();
@@ -206,7 +210,16 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
206
210
  if ($isTableSelection(selection)) {
207
211
  const { rows, columns } = $getTableSelectionSize();
208
212
  const canMergeCells = $canMergeSelectedCells();
209
- setState((s) => ({ ...s, inTable: true, tableRows: rows, tableColumns: columns, canMergeCells, canUnmergeCell: false, inPoetry: false }));
213
+ setState((s) => ({
214
+ ...s,
215
+ inTable: true,
216
+ tableRows: rows,
217
+ tableColumns: columns,
218
+ canMergeCells,
219
+ canUnmergeCell: false,
220
+ inPoetry: false,
221
+ coupletCentered: false,
222
+ }));
210
223
  return;
211
224
  }
212
225
  if (!$isRangeSelection(selection))
@@ -243,6 +256,7 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
243
256
  const elementFormat = ($isElementNode(element) ? element.getFormatType() : 'start') || 'start';
244
257
  const poetryLayout = poetryBlock?.getLayout();
245
258
  const poetryAlign = poetryBlock?.getAlign();
259
+ const coupletCentered = $getCoupletCenteredFromSelection();
246
260
  setState((s) => ({
247
261
  ...s,
248
262
  blockType,
@@ -261,6 +275,7 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
261
275
  inPoetry: poetryBlock !== null,
262
276
  poetryLayout: poetryLayout ?? s.poetryLayout,
263
277
  poetryAlign: poetryAlign ?? s.poetryAlign,
278
+ coupletCentered: coupletCentered ?? false,
264
279
  }));
265
280
  });
266
281
  }, [editor]);
@@ -806,7 +821,7 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
806
821
  }, onRemove: removeLink, strings: strings }) })] })) : (_jsx(ToolbarButton, { icon: IconLink, title: strings.toolbar.insertLink, onClick: openLinkDialog })) }, "link")),
807
822
  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.footnotes && (_jsx(ToolbarButton, { icon: IconNumber1Small, title: strings.toolbar.insertFootnote, onClick: () => editor.dispatchCommand(INSERT_FOOTNOTE_COMMAND, undefined) })), config.blocks.horizontalRule && (_jsx(ToolbarButton, { icon: IconSeparatorHorizontal, title: strings.toolbar.insertHorizontalRule, onClick: () => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined) })), config.poetry.enabled && locale !== 'en' && (_jsx(ToolbarButton, { icon: IconFeather, title: strings.toolbar.insertPoetryCouplet, onClick: () => editor.dispatchCommand(INSERT_POETRY_COUPLET_COMMAND, { layout: config.poetry.defaultLayout, align: 'justify' }) }))] }, "stubInsert")),
808
823
  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: runMenuAction }) })] }) }, "tableActions")),
809
- config.poetry.enabled && state.inPoetry && (_jsx("div", { className: "likhari-toolbar-group", children: _jsxs(Menu, { position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 200, 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.poetryOptions, title: strings.toolbar.poetryOptions, onMouseDown: (e) => e.preventDefault(), children: _jsx(IconFeather, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: _jsx(PoetryMenuItems, { strings: strings, alignIcons: ALIGN_ICONS, layout: state.poetryLayout, align: state.poetryAlign, onAction: runMenuAction }) })] }) }, "poetryActions")),
824
+ config.poetry.enabled && state.inPoetry && (_jsx("div", { className: "likhari-toolbar-group", children: _jsxs(Menu, { position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 200, 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.poetryOptions, title: strings.toolbar.poetryOptions, onMouseDown: (e) => e.preventDefault(), children: _jsx(IconFeather, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: _jsx(PoetryMenuItems, { strings: strings, alignIcons: ALIGN_ICONS, layout: state.poetryLayout, align: state.poetryAlign, centered: state.coupletCentered, onAction: runMenuAction }) })] }) }, "poetryActions")),
810
825
  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")),
811
826
  ];
812
827
  return (_jsxs("div", { className: "likhari-toolbar", role: "toolbar", "aria-label": strings.toolbar.ariaLabel, ref: toolbarContainerRef, children: [fixedGroups, movableGroups
@@ -825,7 +840,7 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
825
840
  }, 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: runMenuAction }) })] })), config.poetry.enabled && (_jsxs(Menu, { opened: poetryContext !== null, onChange: (opened) => {
826
841
  if (!opened)
827
842
  setPoetryContext(null);
828
- }, position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 200, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: poetryContext?.x ?? -9999, top: poetryContext?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsx(Menu.Dropdown, { children: _jsx(PoetryMenuItems, { strings: strings, alignIcons: ALIGN_ICONS, layout: state.poetryLayout, align: state.poetryAlign, onAction: runMenuAction }) })] })), _jsxs("div", { "aria-hidden": "true", ref: toolbarMeasureRef, style: {
843
+ }, position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 200, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: poetryContext?.x ?? -9999, top: poetryContext?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsx(Menu.Dropdown, { children: _jsx(PoetryMenuItems, { strings: strings, alignIcons: ALIGN_ICONS, layout: state.poetryLayout, align: state.poetryAlign, centered: state.coupletCentered, onAction: runMenuAction }) })] })), _jsxs("div", { "aria-hidden": "true", ref: toolbarMeasureRef, style: {
829
844
  position: 'absolute',
830
845
  visibility: 'hidden',
831
846
  pointerEvents: 'none',
@@ -142,6 +142,9 @@ export interface Strings {
142
142
  menuLabel: string;
143
143
  singleColumn: string;
144
144
  twoColumn: string;
145
+ alternating: string;
146
+ centerCouplet: string;
147
+ uncenterCouplet: string;
145
148
  deleteCouplet: string;
146
149
  };
147
150
  link: {
@@ -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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"}
@@ -125,6 +125,9 @@ const en = {
125
125
  menuLabel: 'Poetry',
126
126
  singleColumn: 'Single column',
127
127
  twoColumn: 'Two column',
128
+ alternating: 'Alternating',
129
+ centerCouplet: 'Center this couplet',
130
+ uncenterCouplet: 'Un-center this couplet',
128
131
  deleteCouplet: 'Delete couplet',
129
132
  },
130
133
  link: {
@@ -333,6 +336,9 @@ const ur = {
333
336
  menuLabel: 'شاعری',
334
337
  singleColumn: 'ایک کالم',
335
338
  twoColumn: 'دو کالم',
339
+ alternating: 'باری باری',
340
+ centerCouplet: 'یہ شعر وسط میں رکھیں',
341
+ uncenterCouplet: 'وسط میں رکھنا ختم کریں',
336
342
  deleteCouplet: 'شعر حذف کریں',
337
343
  },
338
344
  link: {
@@ -541,6 +547,9 @@ const paShahmukhi = {
541
547
  menuLabel: 'شاعری',
542
548
  singleColumn: 'اک کالم',
543
549
  twoColumn: 'دو کالم',
550
+ alternating: 'واری واری',
551
+ centerCouplet: 'ایہ شعر وچکار رکھو',
552
+ uncenterCouplet: 'وچکار رکھنا بند کرو',
544
553
  deleteCouplet: 'شعر مٹاؤ',
545
554
  },
546
555
  link: {
@@ -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;AAalD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,WAAW,CAAC,EAkB5C,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;AAclD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,WAAW,CAAC,EAmB5C,CAAC"}
package/dist/nodes.js CHANGED
@@ -9,6 +9,7 @@ import { LayoutContainerNode, LayoutItemNode } from './blocks/LayoutNode.js';
9
9
  import { FootnoteReferenceNode } from './blocks/FootnoteNode.js';
10
10
  import { FootnoteItemNode, FootnoteListNode } from './blocks/FootnoteListNode.js';
11
11
  import { PoetryBlockNode } from './blocks/PoetryNode.js';
12
+ import { PoetryCoupletNode } from './blocks/PoetryCoupletNode.js';
12
13
  /**
13
14
  * Node types registered on the editor at construction time. Per
14
15
  * editor-architecture-design.md §2, node registration cannot be conditional
@@ -36,4 +37,5 @@ export const EDITOR_NODES = [
36
37
  FootnoteListNode,
37
38
  FootnoteItemNode,
38
39
  PoetryBlockNode,
40
+ PoetryCoupletNode,
39
41
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"PoetryPlugin.d.ts","sourceRoot":"","sources":["../../src/plugins/PoetryPlugin.tsx"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,cAAc,EACpB,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAmB,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAG5F,eAAO,MAAM,6BAA6B,EAAE,cAAc,CAAC;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,CAEtG,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,SA4C3B"}
1
+ {"version":3,"file":"PoetryPlugin.d.ts","sourceRoot":"","sources":["../../src/plugins/PoetryPlugin.tsx"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,cAAc,EACpB,MAAM,SAAS,CAAC;AAIjB,OAAO,EAAmB,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAG5F,eAAO,MAAM,6BAA6B,EAAE,cAAc,CAAC;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,CAEtG,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,SA4C3B"}
@@ -3,6 +3,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
3
3
  import { COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_HIGH, KEY_BACKSPACE_COMMAND, KEY_ENTER_COMMAND, createCommand, } from 'lexical';
4
4
  import { mergeRegister } from '@lexical/utils';
5
5
  import { LayoutContainerNode, LayoutItemNode } from '../blocks/LayoutNode.js';
6
+ import { PoetryCoupletNode } from '../blocks/PoetryCoupletNode.js';
6
7
  import { PoetryBlockNode } from '../blocks/PoetryNode.js';
7
8
  import { $deletePoetryOnBackspace, $ensureTrailingParagraph, $exitPoetryOnEnter, $insertPoetryCouplet } from '../blocks/poetryActions.js';
8
9
  export const INSERT_POETRY_COUPLET_COMMAND = createCommand('INSERT_POETRY_COUPLET_COMMAND');
@@ -21,8 +22,8 @@ export const INSERT_POETRY_COUPLET_COMMAND = createCommand('INSERT_POETRY_COUPLE
21
22
  export function PoetryPlugin() {
22
23
  const [editor] = useLexicalComposerContext();
23
24
  useEffect(() => {
24
- if (!editor.hasNodes([PoetryBlockNode, LayoutContainerNode, LayoutItemNode])) {
25
- throw new Error('PoetryPlugin: PoetryBlockNode (and the columns nodes it composes) not registered on the editor');
25
+ if (!editor.hasNodes([PoetryBlockNode, PoetryCoupletNode, LayoutContainerNode, LayoutItemNode])) {
26
+ throw new Error('PoetryPlugin: PoetryBlockNode/PoetryCoupletNode (and the columns nodes two-column composes) not registered on the editor');
26
27
  }
27
28
  return mergeRegister(editor.registerCommand(INSERT_POETRY_COUPLET_COMMAND, ({ layout, align }) => $insertPoetryCouplet(layout, align), COMMAND_PRIORITY_EDITOR), editor.registerNodeTransform(PoetryBlockNode, (node) => {
28
29
  if (node.getChildrenSize() === 0) {
@@ -406,32 +406,76 @@
406
406
 
407
407
  /* Misra lines sit tight against each other within a couplet — the generic
408
408
  .likhari-paragraph margin would otherwise add the same gap between a
409
- couplet's own two lines as between separate couplets, losing the pairing
410
- entirely. The couplet's own margin-bottom (above) is the only gap that
411
- should read as "extra space", and it falls after every second line. */
409
+ couplet's own two lines as between separate couplets within the same
410
+ block, losing the pairing entirely. A block's children are a free mix of
411
+ .likhari-poetry-couplet (single-column/alternating/centered) and
412
+ .likhari-layout-container (two-column) — one wrapper box per couplet,
413
+ regardless of type — so "extra space after every couplet" is just
414
+ margin-bottom on every non-last couplet of either kind. */
412
415
  .likhari-poetry .likhari-paragraph {
413
416
  margin: 0;
414
417
  }
415
418
 
416
- .likhari-poetry--two-column .likhari-layout-container {
419
+ .likhari-poetry > .likhari-poetry-couplet:not(:last-child),
420
+ .likhari-poetry > .likhari-layout-container:not(:last-child) {
421
+ margin-bottom: 1em;
422
+ }
423
+
424
+ .likhari-poetry-couplet {
425
+ box-sizing: border-box;
426
+ }
427
+
428
+ /* "Two columns with a couplet that is single, aligned centered": a couplet
429
+ can individually override to a narrower, page-centered box regardless of
430
+ the block's own layout — the same capped-width + auto-margin centering
431
+ .likhari-poetry itself uses, scoped to just this one couplet. */
432
+ .likhari-poetry-couplet--centered {
433
+ width: fit-content;
434
+ max-width: 70%;
435
+ margin-inline: auto;
436
+ }
437
+
438
+ /* "Aligned alternative": every couplet is the same fixed width, but
439
+ consecutive couplets alternate which edge they're pinned to — since both
440
+ widths overlap past the midpoint, the couplets visually stagger with
441
+ some overlap toward the center, rather than all stacking flush to one
442
+ side or centered identically. */
443
+ .likhari-poetry--alternating .likhari-poetry-couplet {
444
+ width: 65%;
445
+ max-width: none;
446
+ }
447
+
448
+ .likhari-poetry--alternating .likhari-poetry-couplet:nth-of-type(odd) {
449
+ margin-inline-end: auto;
450
+ }
451
+
452
+ .likhari-poetry--alternating .likhari-poetry-couplet:nth-of-type(even) {
453
+ margin-inline-start: auto;
454
+ }
455
+
456
+ /* Only inside a poetry block's own two-column couplets: no block margin
457
+ (the generic .likhari-layout-container rule's margin is for a
458
+ stand-alone Columns insert, not one couplet of several in a poetry
459
+ block — spacing between couplets is the margin-bottom rule above
460
+ instead) and no grid gap — the divider below sits exactly on the track
461
+ boundary, so spacing instead comes from matching padding on each side
462
+ of it, which keeps the gutter visually even on both sides of the line. */
463
+ .likhari-poetry > .likhari-layout-container {
417
464
  margin: 0;
418
- /* No grid gap — the divider below sits exactly on the track boundary, so
419
- spacing instead comes from matching padding on each side of it, which
420
- keeps the gutter visually even on both sides of the line. */
421
465
  gap: 0;
422
466
  }
423
467
 
424
- .likhari-poetry--two-column .likhari-layout-item {
468
+ .likhari-poetry .likhari-layout-item {
425
469
  text-align: inherit;
426
470
  }
427
471
 
428
- .likhari-poetry--two-column .likhari-layout-item:first-child {
472
+ .likhari-poetry .likhari-layout-item:first-child {
429
473
  padding-inline-end: 20px;
430
474
  }
431
475
 
432
476
  /* A visible divider between misra columns, same dashed language as the
433
477
  couplet's own border — matches a table's internal cell borders. */
434
- .likhari-poetry--two-column .likhari-layout-item:not(:first-child) {
478
+ .likhari-poetry .likhari-layout-item:not(:first-child) {
435
479
  border-inline-start: 1px dashed var(--editor-line-strong);
436
480
  padding-inline-start: 20px;
437
481
  }
@@ -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,kBAgDzB,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,kBAiDzB,CAAC"}
@@ -46,6 +46,7 @@ export const editorTheme = {
46
46
  footnoteList: 'likhari-footnote-list',
47
47
  footnoteItem: 'likhari-footnote-item',
48
48
  poetry: 'likhari-poetry',
49
+ poetryCouplet: 'likhari-poetry-couplet',
49
50
  table: 'likhari-table',
50
51
  tableRow: 'likhari-table-row',
51
52
  tableCell: 'likhari-table-cell',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshapardaz/likhari-react",
3
- "version": "0.1.18-dev.150",
3
+ "version": "0.1.18-dev.154",
4
4
  "description": "React implementation of the Likhari editor, built on @lexical/react.",
5
5
  "license": "MIT",
6
6
  "repository": {