@lumir-company/editor 0.4.18 → 0.4.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1466,6 +1466,16 @@ var ColumnList = createStronglyTypedTiptapNode({
1466
1466
  name: "columnList",
1467
1467
  group: "childContainer bnBlock blockGroupChild",
1468
1468
  content: "column column+",
1469
+ addAttributes() {
1470
+ return {
1471
+ // 블록별 중앙 세로 구분선 표시 여부(드래그핸들 메뉴에서 토글). data-divider로 렌더.
1472
+ showDivider: {
1473
+ default: false,
1474
+ parseHTML: (element) => element.getAttribute("data-divider") === "true",
1475
+ renderHTML: (attributes) => attributes.showDivider ? { "data-divider": "true" } : {}
1476
+ }
1477
+ };
1478
+ },
1469
1479
  parseHTML() {
1470
1480
  return [{ tag: 'div[data-node-type="columnList"]' }];
1471
1481
  },
@@ -1914,7 +1924,8 @@ var HtmlPreviewBlock = createReactBlockSpec3(
1914
1924
  );
1915
1925
  var ColumnListBlock = createBlockSpecFromStronglyTypedTiptapNode(
1916
1926
  ColumnList,
1917
- {}
1927
+ // showDivider를 블록 prop으로 등록 → onContentChange JSON 직렬화 + 재로드 라운드트립.
1928
+ { showDivider: { default: false } }
1918
1929
  );
1919
1930
  var ColumnBlock = createBlockSpecFromStronglyTypedTiptapNode(Column, {});
1920
1931
  var schema = BlockNoteSchema.create({
@@ -3412,6 +3423,8 @@ var ALLOWED_VIDEO_EXTENSIONS = [
3412
3423
  ];
3413
3424
  var ROW_RESIZE_MIN_HEIGHT = 24;
3414
3425
  var ROW_RESIZE_HANDLE_WIDTH = 5;
3426
+ var TABLE_SCALE_MIN_COL_WIDTH = 24;
3427
+ var TABLE_SCALE_MAX = 6;
3415
3428
 
3416
3429
  // src/extensions/rowResizing.ts
3417
3430
  var rowResizingPluginKey = new PluginKey3(
@@ -3690,9 +3703,165 @@ function handleDecorations(state, pluginState) {
3690
3703
  return DecorationSet2.create(state.doc, decorations);
3691
3704
  }
3692
3705
 
3693
- // src/extensions/tableCellAttrPreserve.ts
3706
+ // src/extensions/tableScaling.ts
3694
3707
  import { Plugin as Plugin4, PluginKey as PluginKey4 } from "prosemirror-state";
3695
- var tableCellAttrPreserveKey = new PluginKey4(
3708
+ import { Decoration as Decoration3, DecorationSet as DecorationSet3 } from "prosemirror-view";
3709
+ import { TableMap as TableMap2 } from "prosemirror-tables";
3710
+ var tableScalingPluginKey = new PluginKey4(
3711
+ "lumirTableScaling"
3712
+ );
3713
+ function tableScaling() {
3714
+ return new Plugin4({
3715
+ key: tableScalingPluginKey,
3716
+ state: {
3717
+ init: () => null,
3718
+ apply(tr, prev) {
3719
+ const meta = tr.getMeta(tableScalingPluginKey);
3720
+ if (meta !== void 0) return meta.preview;
3721
+ if (prev && tr.docChanged) {
3722
+ return { ...prev, tablePos: tr.mapping.map(prev.tablePos, -1) };
3723
+ }
3724
+ return prev;
3725
+ }
3726
+ },
3727
+ props: {
3728
+ decorations(state) {
3729
+ const p = tableScalingPluginKey.getState(state);
3730
+ return p ? buildHeightDecorations(state, p) : null;
3731
+ }
3732
+ },
3733
+ view: (view) => ({
3734
+ update: () => {
3735
+ const p = tableScalingPluginKey.getState(view.state);
3736
+ if (p) applyColgroupPreview(view, p);
3737
+ }
3738
+ })
3739
+ });
3740
+ }
3741
+ function setTableScalePreview(view, preview) {
3742
+ view.dispatch(view.state.tr.setMeta(tableScalingPluginKey, { preview }));
3743
+ }
3744
+ function measureTableForScale(view, tablePos) {
3745
+ const tableEl = findTableEl(view.nodeDOM(tablePos));
3746
+ const node = view.state.doc.nodeAt(tablePos);
3747
+ if (!tableEl || !node || node.type.name !== "table") return null;
3748
+ const map = TableMap2.get(node);
3749
+ const rect = tableEl.getBoundingClientRect();
3750
+ const body = tableEl.tBodies[0];
3751
+ const rowHeights = body ? Array.from(body.rows).map((tr) => tr.getBoundingClientRect().height) : [];
3752
+ const colWidths = measureColWidths(tableEl, map.width);
3753
+ if (rowHeights.length !== map.height || colWidths.length !== map.width) {
3754
+ return null;
3755
+ }
3756
+ return {
3757
+ tablePos,
3758
+ colWidths,
3759
+ rowHeights,
3760
+ origW: rect.width,
3761
+ origH: rect.height,
3762
+ scale: 1
3763
+ };
3764
+ }
3765
+ function commitTableScale(view, preview) {
3766
+ const { state } = view;
3767
+ const { tablePos, colWidths, rowHeights, scale } = preview;
3768
+ const table = state.doc.nodeAt(tablePos);
3769
+ if (!table || table.type.name !== "table") return;
3770
+ const map = TableMap2.get(table);
3771
+ const start = tablePos + 1;
3772
+ const tr = state.tr;
3773
+ const seen = /* @__PURE__ */ new Set();
3774
+ for (const relPos of map.map) {
3775
+ if (seen.has(relPos)) continue;
3776
+ seen.add(relPos);
3777
+ const node = table.nodeAt(relPos);
3778
+ if (!node) continue;
3779
+ const rect = map.findCell(relPos);
3780
+ const colwidth = [];
3781
+ for (let c = rect.left; c < rect.right; c++) {
3782
+ colwidth.push(Math.round((colWidths[c] ?? 0) * scale));
3783
+ }
3784
+ let h = 0;
3785
+ for (let r = rect.top; r < rect.bottom; r++) h += rowHeights[r] ?? 0;
3786
+ const rowHeight = Math.round(h * scale);
3787
+ tr.setNodeMarkup(start + relPos, void 0, {
3788
+ ...node.attrs,
3789
+ colwidth: colwidth.some((w) => w > 0) ? colwidth : null,
3790
+ rowHeight: rowHeight > 0 ? rowHeight : null
3791
+ });
3792
+ }
3793
+ if (tr.docChanged) view.dispatch(tr);
3794
+ }
3795
+ function measureColWidths(tableEl, width) {
3796
+ const widths = new Array(width).fill(0);
3797
+ const colgroup = tableEl.querySelector("colgroup");
3798
+ if (colgroup && colgroup.children.length === width) {
3799
+ let allSet = true;
3800
+ for (let i = 0; i < width; i++) {
3801
+ const w = parseFloat(colgroup.children[i].style.width);
3802
+ if (Number.isFinite(w) && w > 0) widths[i] = w;
3803
+ else allSet = false;
3804
+ }
3805
+ if (allSet) return widths;
3806
+ }
3807
+ const firstRow = tableEl.tBodies[0]?.rows[0];
3808
+ if (firstRow) {
3809
+ let col = 0;
3810
+ for (const cell of Array.from(firstRow.cells)) {
3811
+ const span = cell.colSpan || 1;
3812
+ const w = cell.getBoundingClientRect().width / span;
3813
+ for (let s = 0; s < span && col < width; s++) widths[col++] = w;
3814
+ }
3815
+ }
3816
+ return widths;
3817
+ }
3818
+ function applyColgroupPreview(view, p) {
3819
+ const tableEl = findTableEl(view.nodeDOM(p.tablePos));
3820
+ const colgroup = tableEl?.querySelector("colgroup");
3821
+ if (!colgroup) return;
3822
+ const cols = colgroup.children;
3823
+ for (let i = 0; i < cols.length && i < p.colWidths.length; i++) {
3824
+ cols[i].style.width = Math.round(p.colWidths[i] * p.scale) + "px";
3825
+ }
3826
+ }
3827
+ function buildHeightDecorations(state, p) {
3828
+ const table = state.doc.nodeAt(p.tablePos);
3829
+ if (!table || table.type.name !== "table") return DecorationSet3.empty;
3830
+ const map = TableMap2.get(table);
3831
+ const start = p.tablePos + 1;
3832
+ const decorations = [];
3833
+ const seen = /* @__PURE__ */ new Set();
3834
+ for (const relPos of map.map) {
3835
+ if (seen.has(relPos)) continue;
3836
+ seen.add(relPos);
3837
+ const node = table.nodeAt(relPos);
3838
+ if (!node) continue;
3839
+ const rect = map.findCell(relPos);
3840
+ let h = 0;
3841
+ for (let r = rect.top; r < rect.bottom; r++) h += p.rowHeights[r] ?? 0;
3842
+ const from = start + relPos;
3843
+ const to = from + node.nodeSize;
3844
+ decorations.push(
3845
+ Decoration3.node(from, to, {
3846
+ class: "lumir-table-scale-dragging",
3847
+ style: `height: ${Math.round(h * p.scale)}px`
3848
+ })
3849
+ );
3850
+ }
3851
+ return DecorationSet3.create(state.doc, decorations);
3852
+ }
3853
+ function findTableEl(dom) {
3854
+ if (!dom) return null;
3855
+ const el = dom;
3856
+ if (el.nodeName === "TABLE") return el;
3857
+ const inner = el.querySelector?.("table");
3858
+ if (inner) return inner;
3859
+ return el.closest?.("table") ?? null;
3860
+ }
3861
+
3862
+ // src/extensions/tableCellAttrPreserve.ts
3863
+ import { Plugin as Plugin5, PluginKey as PluginKey5 } from "prosemirror-state";
3864
+ var tableCellAttrPreserveKey = new PluginKey5(
3696
3865
  "lumirTableCellAttrPreserve"
3697
3866
  );
3698
3867
  var PRESERVED_ATTRS = [
@@ -3749,7 +3918,7 @@ function collectCellAttrs(doc) {
3749
3918
  return result;
3750
3919
  }
3751
3920
  function tableCellAttrPreserve() {
3752
- return new Plugin4({
3921
+ return new Plugin5({
3753
3922
  key: tableCellAttrPreserveKey,
3754
3923
  appendTransaction(transactions, oldState, newState) {
3755
3924
  if (!transactions.some((tr2) => tr2.docChanged)) {
@@ -3855,6 +4024,7 @@ var RowHeightExtension = Extension2.create({
3855
4024
  const plugins = [tableCellAttrPreserve()];
3856
4025
  if (this.options.resizable) {
3857
4026
  plugins.push(rowResizing());
4027
+ plugins.push(tableScaling());
3858
4028
  }
3859
4029
  return plugins;
3860
4030
  }
@@ -3862,11 +4032,11 @@ var RowHeightExtension = Extension2.create({
3862
4032
 
3863
4033
  // src/extensions/TableAlignmentExtension.ts
3864
4034
  import { Extension as Extension3 } from "@tiptap/core";
3865
- import { Plugin as Plugin5, PluginKey as PluginKey5 } from "prosemirror-state";
3866
- import { Decoration as Decoration3, DecorationSet as DecorationSet3 } from "prosemirror-view";
3867
- var tableAlignmentDecoKey = new PluginKey5("lumirTableAlignmentDeco");
4035
+ import { Plugin as Plugin6, PluginKey as PluginKey6 } from "prosemirror-state";
4036
+ import { Decoration as Decoration4, DecorationSet as DecorationSet4 } from "prosemirror-view";
4037
+ var tableAlignmentDecoKey = new PluginKey6("lumirTableAlignmentDeco");
3868
4038
  function tableAlignmentDecorationPlugin() {
3869
- return new Plugin5({
4039
+ return new Plugin6({
3870
4040
  key: tableAlignmentDecoKey,
3871
4041
  props: {
3872
4042
  decorations(state) {
@@ -3876,7 +4046,7 @@ function tableAlignmentDecorationPlugin() {
3876
4046
  const align = node.attrs.tableAlignment;
3877
4047
  if (align && align !== "left") {
3878
4048
  decorations.push(
3879
- Decoration3.node(pos, pos + node.nodeSize, {
4049
+ Decoration4.node(pos, pos + node.nodeSize, {
3880
4050
  "data-table-alignment": align
3881
4051
  })
3882
4052
  );
@@ -3885,7 +4055,7 @@ function tableAlignmentDecorationPlugin() {
3885
4055
  }
3886
4056
  return void 0;
3887
4057
  });
3888
- return DecorationSet3.create(state.doc, decorations);
4058
+ return DecorationSet4.create(state.doc, decorations);
3889
4059
  }
3890
4060
  }
3891
4061
  });
@@ -3916,9 +4086,56 @@ var TableAlignmentExtension = Extension3.create({
3916
4086
  }
3917
4087
  });
3918
4088
 
4089
+ // src/extensions/TableSelectAllExtension.ts
4090
+ import { Extension as Extension4 } from "@tiptap/core";
4091
+ import { CellSelection, TableMap as TableMap3 } from "prosemirror-tables";
4092
+ var TableSelectAllExtension = Extension4.create({
4093
+ name: "lumirTableSelectAll",
4094
+ priority: 1e3,
4095
+ addKeyboardShortcuts() {
4096
+ return {
4097
+ "Mod-a": () => {
4098
+ const view = this.editor.view;
4099
+ const { state } = view;
4100
+ const sel = state.selection;
4101
+ const $from = sel.$from;
4102
+ let depth = -1;
4103
+ for (let d = $from.depth; d > 0; d--) {
4104
+ if ($from.node(d).type.name === "table") {
4105
+ depth = d;
4106
+ break;
4107
+ }
4108
+ }
4109
+ if (depth < 0) return false;
4110
+ const table = $from.node(depth);
4111
+ const tableStart = $from.start(depth);
4112
+ const map = TableMap3.get(table);
4113
+ const firstRel = map.map[0];
4114
+ const lastRel = map.map[map.map.length - 1];
4115
+ if (sel instanceof CellSelection) {
4116
+ const a = sel.$anchorCell.pos - tableStart;
4117
+ const h = sel.$headCell.pos - tableStart;
4118
+ if (Math.min(a, h) === firstRel && Math.max(a, h) === lastRel) {
4119
+ return false;
4120
+ }
4121
+ }
4122
+ const tr = state.tr.setSelection(
4123
+ CellSelection.create(
4124
+ state.doc,
4125
+ tableStart + firstRel,
4126
+ tableStart + lastRel
4127
+ )
4128
+ );
4129
+ view.dispatch(tr);
4130
+ return true;
4131
+ }
4132
+ };
4133
+ }
4134
+ });
4135
+
3919
4136
  // src/blocks/columns/insertColumns.ts
3920
4137
  import { TextSelection } from "prosemirror-state";
3921
- function insertTwoColumns(editor) {
4138
+ function insertTwoColumns(editor, showDivider = false) {
3922
4139
  const tiptap = editor?._tiptapEditor;
3923
4140
  if (!tiptap) {
3924
4141
  return false;
@@ -3945,7 +4162,7 @@ function insertTwoColumns(editor) {
3945
4162
  const insertPos = $from.after(depth);
3946
4163
  const mkBlock = () => blockContainer.create(null, paragraph.create());
3947
4164
  const mkColumn = () => column.create(null, mkBlock());
3948
- const list = columnList.create(null, [mkColumn(), mkColumn()]);
4165
+ const list = columnList.create({ showDivider }, [mkColumn(), mkColumn()]);
3949
4166
  try {
3950
4167
  let tr = state.tr.insert(insertPos, list);
3951
4168
  try {
@@ -4830,9 +5047,14 @@ import { useEffect as useEffect9, useMemo as useMemo6 } from "react";
4830
5047
  function useFocusedCellHandlePositioning(cellEl, tbodyEl, orientation, show) {
4831
5048
  const { refs, floatingStyles, context, update } = useFloating({
4832
5049
  open: show,
4833
- placement: orientation === "row" ? "left" : orientation === "col" ? "top" : "right",
4834
- // col/row: 가장자리 선(zero-size)에, cell: 우측 보더에 14px hit-area 중앙 정렬(-7).
4835
- middleware: [offset(-7)],
5050
+ placement: orientation === "row" ? "left" : orientation === "col" ? "top" : orientation === "corner" ? "bottom-start" : "right",
5051
+ // col/row/cell: 가장자리에 14px hit-area 중앙 정렬(-7).
5052
+ // corner: 18px hit-zone이 표 우하단 모서리에 걸치도록 위/좌로 살짝 당김(모서리 hover 자연).
5053
+ middleware: [
5054
+ offset(
5055
+ orientation === "corner" ? { mainAxis: -6, crossAxis: -6 } : -7
5056
+ )
5057
+ ],
4836
5058
  whileElementsMounted: autoUpdate
4837
5059
  });
4838
5060
  const { isMounted, styles } = useTransitionStyles(context);
@@ -4860,6 +5082,9 @@ function useFocusedCellHandlePositioning(cellEl, tbodyEl, orientation, show) {
4860
5082
  if (orientation === "row") {
4861
5083
  return new DOMRect(t.left, c.top, 0, c.height);
4862
5084
  }
5085
+ if (orientation === "corner") {
5086
+ return new DOMRect(t.right, t.bottom, 0, 0);
5087
+ }
4863
5088
  return c;
4864
5089
  }
4865
5090
  });
@@ -4877,6 +5102,33 @@ function useFocusedCellHandlePositioning(cellEl, tbodyEl, orientation, show) {
4877
5102
  [floatingStyles, isMounted, refs.setFloating, styles]
4878
5103
  );
4879
5104
  }
5105
+ function useTableCornerPositioning(referencePosTable, show) {
5106
+ const { refs, floatingStyles, context } = useFloating({
5107
+ open: show,
5108
+ placement: "bottom-start",
5109
+ // 18px hit-zone을 모서리에서 안쪽(위/좌)으로 당겨 표 위에 걸치게 한다.
5110
+ middleware: [offset({ mainAxis: -12, crossAxis: -12 })],
5111
+ whileElementsMounted: autoUpdate
5112
+ });
5113
+ const { isMounted, styles } = useTransitionStyles(context);
5114
+ useEffect9(() => {
5115
+ if (!referencePosTable) {
5116
+ refs.setReference(null);
5117
+ return;
5118
+ }
5119
+ refs.setReference({
5120
+ getBoundingClientRect: () => new DOMRect(referencePosTable.right, referencePosTable.bottom, 0, 0)
5121
+ });
5122
+ }, [referencePosTable, refs]);
5123
+ return useMemo6(
5124
+ () => ({
5125
+ isMounted,
5126
+ ref: refs.setFloating,
5127
+ style: { ...styles, ...floatingStyles }
5128
+ }),
5129
+ [floatingStyles, isMounted, refs.setFloating, styles]
5130
+ );
5131
+ }
4880
5132
 
4881
5133
  // src/components/LumirTableHandlesController.tsx
4882
5134
  import { Fragment as Fragment7, jsx as jsx26, jsxs as jsxs18 } from "react/jsx-runtime";
@@ -5027,6 +5279,10 @@ function LumirTableHandlesController() {
5027
5279
  const onEndExtend = useCallback20(() => {
5028
5280
  editor.tableHandles?.unfreezeHandles();
5029
5281
  }, [editor]);
5282
+ const tableCorner = useTableCornerPositioning(
5283
+ coreState?.referencePosTable ?? null,
5284
+ !!coreState?.widgetContainer
5285
+ );
5030
5286
  const menuHandlers = useMemo7(() => {
5031
5287
  const mk = (kind) => ({
5032
5288
  freeze: () => {
@@ -5076,6 +5332,48 @@ function LumirTableHandlesController() {
5076
5332
  }, [editor, recompute]);
5077
5333
  const noop = useCallback20(() => {
5078
5334
  }, []);
5335
+ const onScaleStart = useCallback20(
5336
+ (e) => {
5337
+ e.preventDefault();
5338
+ e.stopPropagation();
5339
+ const view = editor.prosemirrorView;
5340
+ const blockId = coreState?.block?.id;
5341
+ if (!view || !blockId) return;
5342
+ const tablePos = findTableNodePos(editor._tiptapEditor, blockId);
5343
+ if (tablePos < 0) return;
5344
+ const base = measureTableForScale(view, tablePos);
5345
+ if (!base) return;
5346
+ const minScale = Math.max(
5347
+ TABLE_SCALE_MIN_COL_WIDTH / Math.max(1, Math.min(...base.colWidths)),
5348
+ ROW_RESIZE_MIN_HEIGHT / Math.max(1, Math.min(...base.rowHeights))
5349
+ );
5350
+ const startX = e.clientX;
5351
+ const startY = e.clientY;
5352
+ let current = base;
5353
+ editor.tableHandles?.freezeHandles();
5354
+ const onMove = (me) => {
5355
+ if (me.buttons === 0) return onUp();
5356
+ const newW = base.origW + (me.clientX - startX);
5357
+ const newH = base.origH + (me.clientY - startY);
5358
+ const scale = Math.min(
5359
+ TABLE_SCALE_MAX,
5360
+ Math.max(minScale, Math.max(newW / base.origW, newH / base.origH))
5361
+ );
5362
+ current = { ...base, scale };
5363
+ setTableScalePreview(view, current);
5364
+ };
5365
+ const onUp = () => {
5366
+ window.removeEventListener("pointermove", onMove);
5367
+ window.removeEventListener("pointerup", onUp);
5368
+ setTableScalePreview(view, null);
5369
+ commitTableScale(view, current);
5370
+ editor.tableHandles?.unfreezeHandles();
5371
+ };
5372
+ window.addEventListener("pointermove", onMove);
5373
+ window.addEventListener("pointerup", onUp);
5374
+ },
5375
+ [editor, coreState]
5376
+ );
5079
5377
  return /* @__PURE__ */ jsxs18(Fragment7, { children: [
5080
5378
  /* @__PURE__ */ jsx26("div", { ref: setMenuContainerRef }),
5081
5379
  th && focused && menuContainerRef && /* @__PURE__ */ jsxs18(FloatingPortal, { root: focused.widgetContainer, children: [
@@ -5191,6 +5489,16 @@ function LumirTableHandlesController() {
5191
5489
  }
5192
5490
  )
5193
5491
  }
5492
+ ),
5493
+ tableCorner.isMounted && /* @__PURE__ */ jsx26(
5494
+ "div",
5495
+ {
5496
+ ref: tableCorner.ref,
5497
+ style: tableCorner.style,
5498
+ className: "lumir-tbl-scale-handle",
5499
+ title: "\uD45C \uD06C\uAE30 \uC870\uC808 (\uC885\uD6A1\uBE44 \uACE0\uC815)",
5500
+ onPointerDown: onScaleStart
5501
+ }
5194
5502
  )
5195
5503
  ] })
5196
5504
  ] });
@@ -5447,7 +5755,7 @@ function liftFontSize(blocks) {
5447
5755
  }
5448
5756
 
5449
5757
  // src/utils/table-delete.ts
5450
- import { CellSelection, TableMap as TableMap2, deleteRow } from "prosemirror-tables";
5758
+ import { CellSelection as CellSelection2, TableMap as TableMap4, deleteRow } from "prosemirror-tables";
5451
5759
  function measureRowHeights(view, tablePos) {
5452
5760
  try {
5453
5761
  const at = view?.domAtPos?.(tablePos + 1);
@@ -5466,7 +5774,7 @@ function measureRowHeights(view, tablePos) {
5466
5774
  function buildDeleteColumnTr(state, tablePos, col, rowPx) {
5467
5775
  const table = state.doc.nodeAt(tablePos);
5468
5776
  if (!table || table.type.name !== "table") return null;
5469
- const map = TableMap2.get(table);
5777
+ const map = TableMap4.get(table);
5470
5778
  const W = map.width;
5471
5779
  const H = map.height;
5472
5780
  if (col < 0 || col >= W || W <= 1) return null;
@@ -5572,7 +5880,7 @@ function removeFocusedRowOrColumn(editor, index, direction) {
5572
5880
  const rowStart = state.doc.resolve(tableInside.posAtIndex(index) + 1);
5573
5881
  const cellPos = state.doc.resolve(rowStart.posAtIndex(0));
5574
5882
  const selState = state.apply(
5575
- state.tr.setSelection(new CellSelection(cellPos))
5883
+ state.tr.setSelection(new CellSelection2(cellPos))
5576
5884
  );
5577
5885
  return deleteRow(selState, (tr) => tiptap.view.dispatch(tr));
5578
5886
  } catch {
@@ -5724,6 +6032,20 @@ function normalizeAlign(ta) {
5724
6032
  if (v === "justify") return "justify";
5725
6033
  return "";
5726
6034
  }
6035
+ function mapVerticalAlign(v) {
6036
+ const s = (v || "").trim().toLowerCase();
6037
+ if (s === "middle" || s === "center") return "middle";
6038
+ if (s === "bottom") return "bottom";
6039
+ return null;
6040
+ }
6041
+ function fontSizeToPx(raw) {
6042
+ if (!raw) return null;
6043
+ const m = String(raw).trim().match(/^([\d.]+)\s*(px|pt)?$/i);
6044
+ if (!m) return null;
6045
+ const v = parseFloat(m[1]);
6046
+ if (!Number.isFinite(v) || v <= 0) return null;
6047
+ return (m[2] || "px").toLowerCase() === "pt" ? v * (96 / 72) : v;
6048
+ }
5727
6049
  function applyCellFormatting(el, fmt) {
5728
6050
  if (fmt.bgRgb && !fmt.bgTransparent) {
5729
6051
  const v = nearestBackgroundColorValue(fmt.bgRgb);
@@ -5743,6 +6065,13 @@ function applyCellFormatting(el, fmt) {
5743
6065
  if (fmt.bold) inner = `<strong>${inner}</strong>`;
5744
6066
  el.innerHTML = inner;
5745
6067
  }
6068
+ if (fmt.verticalAlign) {
6069
+ el.setAttribute("data-vertical-alignment", fmt.verticalAlign);
6070
+ }
6071
+ if (fmt.fontSizePx && Math.abs(fmt.fontSizePx - 14) > 1 && el.innerHTML.trim()) {
6072
+ const v = `${Math.round(fmt.fontSizePx)}px`;
6073
+ el.innerHTML = `<span data-style-type="fontSize" data-value="${v}" style="font-size:${v}">` + el.innerHTML + `</span>`;
6074
+ }
5746
6075
  }
5747
6076
  function readComputedFormat(el) {
5748
6077
  const cs = getComputedStyle(el);
@@ -5756,7 +6085,13 @@ function readComputedFormat(el) {
5756
6085
  align: normalizeAlign(cs.textAlign),
5757
6086
  bold: fw === "bold" || fw === "bolder" || !isNaN(fwNum) && fwNum >= 600,
5758
6087
  italic: (cs.fontStyle || "").toLowerCase().includes("italic"),
5759
- underline: decoration.toLowerCase().includes("underline")
6088
+ underline: decoration.toLowerCase().includes("underline"),
6089
+ fontSizePx: fontSizeToPx(cs.fontSize),
6090
+ // ⚠️ computed vertical-align은 td 기본값이 "middle"이라 셀마다 잘못 붙는다.
6091
+ // 명시적 inline style / valign 속성만 읽는다(기본값 노이즈 방지).
6092
+ verticalAlign: mapVerticalAlign(
6093
+ el.style?.verticalAlign || el.getAttribute("valign")
6094
+ )
5760
6095
  };
5761
6096
  }
5762
6097
  function readInlineFormat(el) {
@@ -5772,7 +6107,11 @@ function readInlineFormat(el) {
5772
6107
  align: normalizeAlign(sm["text-align"] || el.getAttribute("align")),
5773
6108
  bold: fw === "bold" || fw === "bolder" || parseInt(fw, 10) >= 600,
5774
6109
  italic: (sm["font-style"] || "").toLowerCase().includes("italic"),
5775
- underline: decoration.toLowerCase().includes("underline")
6110
+ underline: decoration.toLowerCase().includes("underline"),
6111
+ fontSizePx: fontSizeToPx(sm["font-size"]),
6112
+ verticalAlign: mapVerticalAlign(
6113
+ sm["vertical-align"] || el.getAttribute("valign")
6114
+ )
5776
6115
  };
5777
6116
  }
5778
6117
  function normalizeExcelTableHtml(html) {
@@ -5786,7 +6125,7 @@ function normalizeExcelTableHtml(html) {
5786
6125
  try {
5787
6126
  host = document.createElement("div");
5788
6127
  host.setAttribute("aria-hidden", "true");
5789
- host.style.cssText = "position:absolute;left:-99999px;top:0;width:0;height:0;overflow:hidden;opacity:0;pointer-events:none";
6128
+ host.style.cssText = "position:absolute;left:-99999px;top:0;width:0;height:0;overflow:hidden;opacity:0;pointer-events:none;font-size:14px";
5790
6129
  const shadow = host.attachShadow({ mode: "open" });
5791
6130
  const styles = Array.from(doc.querySelectorAll("style")).map((s) => s.outerHTML).join("");
5792
6131
  shadow.innerHTML = styles + doc.body.innerHTML;
@@ -5809,6 +6148,100 @@ function normalizeExcelTableHtml(html) {
5809
6148
  }
5810
6149
  }
5811
6150
 
6151
+ // src/utils/table-paste-fit.ts
6152
+ var MIN_COL_PX = 24;
6153
+ function toPx(raw, maxWidth) {
6154
+ if (!raw) return null;
6155
+ const m = String(raw).trim().match(/^([\d.]+)\s*(pt|px|%)?$/i);
6156
+ if (!m) return null;
6157
+ const v = parseFloat(m[1]);
6158
+ if (!Number.isFinite(v) || v <= 0) return null;
6159
+ const unit = (m[2] || "px").toLowerCase();
6160
+ if (unit === "pt") return v * (96 / 72);
6161
+ if (unit === "%") return v / 100 * maxWidth;
6162
+ return v;
6163
+ }
6164
+ function elWidthPx(el, maxWidth) {
6165
+ const styleW = el.style?.width;
6166
+ return toPx(styleW, maxWidth) ?? toPx(el.getAttribute("width"), maxWidth);
6167
+ }
6168
+ function readColumnWidths(table, maxWidth) {
6169
+ const colEls = table.querySelector("colgroup")?.querySelectorAll("col");
6170
+ if (colEls && colEls.length > 0) {
6171
+ const widths2 = [];
6172
+ let ok2 = true;
6173
+ colEls.forEach((c) => {
6174
+ const span = parseInt(c.getAttribute("span") || "1", 10) || 1;
6175
+ const w = elWidthPx(c, maxWidth);
6176
+ if (w == null) ok2 = false;
6177
+ for (let i = 0; i < span; i++) widths2.push(w ?? 0);
6178
+ });
6179
+ if (ok2 && widths2.length > 0) return widths2;
6180
+ }
6181
+ const firstRow = table.querySelector("tr");
6182
+ if (!firstRow) return null;
6183
+ const widths = [];
6184
+ let ok = true;
6185
+ Array.from(firstRow.children).forEach((cell) => {
6186
+ if (cell.tagName !== "TD" && cell.tagName !== "TH") return;
6187
+ const span = parseInt(cell.getAttribute("colspan") || "1", 10) || 1;
6188
+ const w = elWidthPx(cell, maxWidth);
6189
+ if (w == null) ok = false;
6190
+ const per = (w ?? 0) / span;
6191
+ for (let i = 0; i < span; i++) widths.push(per);
6192
+ });
6193
+ return ok && widths.length > 0 ? widths : null;
6194
+ }
6195
+ function fitWidths(widths, maxWidth) {
6196
+ const total = widths.reduce((a, b) => a + b, 0);
6197
+ if (total <= 0) return widths;
6198
+ const scale = total > maxWidth ? maxWidth / total : 1;
6199
+ return widths.map((w) => Math.max(MIN_COL_PX, Math.round(w * scale)));
6200
+ }
6201
+ function computeFittedColumnWidthsPerTable(html, maxWidth) {
6202
+ if (!html || typeof DOMParser === "undefined" || !(maxWidth > 0)) return [];
6203
+ let doc;
6204
+ try {
6205
+ doc = new DOMParser().parseFromString(html, "text/html");
6206
+ } catch {
6207
+ return [];
6208
+ }
6209
+ return Array.from(doc.querySelectorAll("table")).map((t) => {
6210
+ const widths = readColumnWidths(t, maxWidth);
6211
+ return widths ? fitWidths(widths, maxWidth) : null;
6212
+ });
6213
+ }
6214
+ function collectTableBlocks(blocks) {
6215
+ const out = [];
6216
+ const walk = (bs) => {
6217
+ for (const b of bs) {
6218
+ if (b?.type === "table") out.push(b);
6219
+ if (b?.children?.length) walk(b.children);
6220
+ }
6221
+ };
6222
+ walk(blocks || []);
6223
+ return out;
6224
+ }
6225
+ function applyFittedWidthsToNewTables(editor, beforeIds, perTable) {
6226
+ if (!editor || perTable.length === 0) return;
6227
+ const newTables = collectTableBlocks(editor.document).filter(
6228
+ (b) => !beforeIds.has(b.id)
6229
+ );
6230
+ newTables.forEach((tb, i) => {
6231
+ const widths = perTable[i];
6232
+ const current = tb?.content?.columnWidths;
6233
+ if (widths && Array.isArray(current) && current.length === widths.length) {
6234
+ try {
6235
+ editor.updateBlock(tb, {
6236
+ type: "table",
6237
+ content: { ...tb.content, columnWidths: widths }
6238
+ });
6239
+ } catch {
6240
+ }
6241
+ }
6242
+ });
6243
+ }
6244
+
5812
6245
  // src/components/LumirEditor.tsx
5813
6246
  import { Fragment as Fragment8, jsx as jsx27, jsxs as jsxs19 } from "react/jsx-runtime";
5814
6247
  var DEBUG_LOG = (loc, msg, data) => {
@@ -6121,6 +6554,7 @@ function LumirEditor({
6121
6554
  className = "",
6122
6555
  placeholder,
6123
6556
  sideMenuAddButton = false,
6557
+ columnDivider = false,
6124
6558
  floatingMenu = false,
6125
6559
  floatingMenuPosition = "sticky",
6126
6560
  // callbacks / refs
@@ -6234,7 +6668,9 @@ function LumirEditor({
6234
6668
  // tableHandles prop으로 게이트(기존 grip 컨트롤러와 동일 게이트).
6235
6669
  RowHeightExtension.configure({ resizable: tableHandles }),
6236
6670
  // 표 블록 정렬(좌/가운데/우) attr.
6237
- TableAlignmentExtension
6671
+ TableAlignmentExtension,
6672
+ // 셀 포커스 시 Ctrl/Cmd+A → 표 전체 선택.
6673
+ TableSelectAllExtension
6238
6674
  ]
6239
6675
  },
6240
6676
  placeholders: placeholder ? { default: placeholder, emptyDocument: placeholder } : void 0,
@@ -6340,7 +6776,14 @@ function LumirEditor({
6340
6776
  hasFiles: !!event?.clipboardData?.files?.length
6341
6777
  });
6342
6778
  event.preventDefault();
6779
+ const pmDom = editor2.prosemirrorView?.dom;
6780
+ const maxWidth = pmDom?.clientWidth ? pmDom.clientWidth - 8 : 0;
6781
+ const fittedWidths = computeFittedColumnWidthsPerTable(pastedHtml, maxWidth);
6782
+ const beforeTableIds = new Set(
6783
+ collectTableBlocks(editor2.document).map((b) => b.id)
6784
+ );
6343
6785
  editor2.pasteHTML(normalizeExcelTableHtml(pastedHtml));
6786
+ applyFittedWidthsToNewTables(editor2, beforeTableIds, fittedWidths);
6344
6787
  return true;
6345
6788
  }
6346
6789
  const fileList = event?.clipboardData?.files ?? null;
@@ -6635,7 +7078,11 @@ function LumirEditor({
6635
7078
  return /* @__PURE__ */ jsxs19(
6636
7079
  "div",
6637
7080
  {
6638
- className: cn("lumirEditor", className),
7081
+ className: cn(
7082
+ "lumirEditor",
7083
+ columnDivider && "lumir-column-divider",
7084
+ className
7085
+ ),
6639
7086
  style: { position: "relative", display: "flex", flexDirection: "column" },
6640
7087
  children: [
6641
7088
  floatingMenu && editor && /* @__PURE__ */ jsxs19(Fragment8, { children: [
@@ -6832,33 +7279,52 @@ function LumirEditor({
6832
7279
  ),
6833
7280
  subtext: "HTML \uD30C\uC77C\uC744 \uBBF8\uB9AC\uBCF4\uAE30\uB85C \uC0BD\uC785"
6834
7281
  };
7282
+ const columnIcon = (withDivider) => /* @__PURE__ */ jsxs19(
7283
+ "svg",
7284
+ {
7285
+ width: "18",
7286
+ height: "18",
7287
+ viewBox: "0 0 24 24",
7288
+ fill: "none",
7289
+ stroke: "currentColor",
7290
+ strokeWidth: "2",
7291
+ strokeLinecap: "round",
7292
+ strokeLinejoin: "round",
7293
+ children: [
7294
+ /* @__PURE__ */ jsx27("rect", { x: "3", y: "4", width: "7", height: "16", rx: "1" }),
7295
+ /* @__PURE__ */ jsx27("rect", { x: "14", y: "4", width: "7", height: "16", rx: "1" }),
7296
+ withDivider && /* @__PURE__ */ jsx27("line", { x1: "12", y1: "3", x2: "12", y2: "21", strokeDasharray: "2 2" })
7297
+ ]
7298
+ }
7299
+ );
6835
7300
  const columnItem = {
6836
7301
  title: "2\uB2E8 \uCEEC\uB7FC",
6837
- onItemClick: () => {
6838
- insertTwoColumns(editor);
6839
- },
7302
+ onItemClick: () => insertTwoColumns(editor, false),
6840
7303
  aliases: ["columns", "column", "2col", "\uB2E8", "\uCEEC\uB7FC", "\uB2E4\uB2E8", "\uBD84\uD560"],
6841
7304
  group: "Basic blocks",
6842
- icon: /* @__PURE__ */ jsxs19(
6843
- "svg",
6844
- {
6845
- width: "18",
6846
- height: "18",
6847
- viewBox: "0 0 24 24",
6848
- fill: "none",
6849
- stroke: "currentColor",
6850
- strokeWidth: "2",
6851
- strokeLinecap: "round",
6852
- strokeLinejoin: "round",
6853
- children: [
6854
- /* @__PURE__ */ jsx27("rect", { x: "3", y: "4", width: "7", height: "16", rx: "1" }),
6855
- /* @__PURE__ */ jsx27("rect", { x: "14", y: "4", width: "7", height: "16", rx: "1" })
6856
- ]
6857
- }
6858
- ),
7305
+ icon: columnIcon(false),
6859
7306
  subtext: "\uBE14\uB85D\uC744 \uC88C\uC6B0 2\uB2E8\uC73C\uB85C \uBC30\uCE58"
6860
7307
  };
6861
- const allItems = [...filtered, htmlPreviewItem, columnItem];
7308
+ const columnDividerItem = {
7309
+ title: "2\uB2E8 \uCEEC\uB7FC (\uAD6C\uBD84\uC120)",
7310
+ onItemClick: () => insertTwoColumns(editor, true),
7311
+ aliases: [
7312
+ "columns divider",
7313
+ "\uAD6C\uBD84\uC120",
7314
+ "\uCEEC\uB7FC \uAD6C\uBD84\uC120",
7315
+ "2\uB2E8 \uAD6C\uBD84\uC120",
7316
+ "divider"
7317
+ ],
7318
+ group: "Basic blocks",
7319
+ icon: columnIcon(true),
7320
+ subtext: "\uAC00\uC6B4\uB370 \uC138\uB85C \uAD6C\uBD84\uC120\uC774 \uC788\uB294 2\uB2E8 \uCEEC\uB7FC"
7321
+ };
7322
+ const allItems = [
7323
+ ...filtered,
7324
+ htmlPreviewItem,
7325
+ columnItem,
7326
+ columnDividerItem
7327
+ ];
6862
7328
  if (linkPreview?.apiEndpoint) {
6863
7329
  allItems.push({
6864
7330
  title: "Link Preview",