@lumir-company/editor 0.4.22 → 0.4.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/index.js +133 -28
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +123 -18
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1479,6 +1479,11 @@ const url = await uploader(imageFile);
|
|
|
1479
1479
|
|
|
1480
1480
|
## 변경 로그
|
|
1481
1481
|
|
|
1482
|
+
### v0.4.23 (2026-06-22)
|
|
1483
|
+
|
|
1484
|
+
- **다중 블록(여러 문단/리스트) 선택 시 글자 크기 스테퍼 누적 안 되던 버그 수정**: BlockNote `getActiveStyles()`는 선택 끝(`$to`)의 마크만 읽어 다중 블록 선택에서 빈 값을 돌려줬고, 그 결과 스테퍼 표기가 14에 고정돼 +/− 연속 클릭이 매번 같은 값을 재적용했음. 선택 범위 전체를 스캔하는 `readSelectionFontSize` + 낙관적 갱신으로 해결(연속 증감 누적, 표기 정상 갱신)
|
|
1485
|
+
- **툴바 드롭다운 조작 시 선택 하이라이트가 사라지던 문제 보완**: 텍스트를 범위 선택한 뒤 글자 크기 등 툴바 드롭다운을 열면 에디터가 blur되어 브라우저 네이티브 선택 하이라이트가 사라졌음(적용 자체는 정상). blur 상태에서도 선택 범위에 인라인 데코레이션을 입히는 `InactiveSelectionExtension` 추가로 선택 영역이 계속 보이도록 함(색상 CSS 변수 `--lumir-inactive-selection`)
|
|
1486
|
+
|
|
1482
1487
|
### v0.4.22 (2026-06-22)
|
|
1483
1488
|
|
|
1484
1489
|
- **글자 크기 1px 단위 조절** *(신규)*
|
package/dist/index.js
CHANGED
|
@@ -55,7 +55,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
55
55
|
var import_react36 = require("react");
|
|
56
56
|
var import_react37 = require("@blocknote/react");
|
|
57
57
|
var import_mantine = require("@blocknote/mantine");
|
|
58
|
-
var
|
|
58
|
+
var import_core12 = require("@blocknote/core");
|
|
59
59
|
var import_locales = require("@blocknote/core/locales");
|
|
60
60
|
|
|
61
61
|
// src/utils/cn.ts
|
|
@@ -1610,6 +1610,40 @@ function clampFontSizePx(px) {
|
|
|
1610
1610
|
function toFontSizeValue(px) {
|
|
1611
1611
|
return `${clampFontSizePx(px)}px`;
|
|
1612
1612
|
}
|
|
1613
|
+
function readSelectionFontSize(editor) {
|
|
1614
|
+
const ed = editor;
|
|
1615
|
+
const fallback = () => {
|
|
1616
|
+
try {
|
|
1617
|
+
return ed?.getActiveStyles?.().fontSize || "";
|
|
1618
|
+
} catch {
|
|
1619
|
+
return "";
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
try {
|
|
1623
|
+
const tt = ed._tiptapEditor;
|
|
1624
|
+
const state = tt?.state;
|
|
1625
|
+
if (!state) return fallback();
|
|
1626
|
+
const sel = state.selection;
|
|
1627
|
+
if (sel.empty) {
|
|
1628
|
+
const marks = state.storedMarks || sel.$to.marks();
|
|
1629
|
+
const m = marks?.find?.((mk) => mk.type?.name === "fontSize");
|
|
1630
|
+
return m?.attrs?.stringValue || "";
|
|
1631
|
+
}
|
|
1632
|
+
let value = null;
|
|
1633
|
+
let mixed = false;
|
|
1634
|
+
state.doc.nodesBetween(sel.from, sel.to, (node) => {
|
|
1635
|
+
if (mixed || !node.isText) return !mixed;
|
|
1636
|
+
const m = node.marks?.find?.((mk) => mk.type?.name === "fontSize");
|
|
1637
|
+
const v = m?.attrs?.stringValue || "";
|
|
1638
|
+
if (value === null) value = v;
|
|
1639
|
+
else if (value !== v) mixed = true;
|
|
1640
|
+
return !mixed;
|
|
1641
|
+
});
|
|
1642
|
+
return mixed ? "" : value || "";
|
|
1643
|
+
} catch {
|
|
1644
|
+
return fallback();
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1613
1647
|
|
|
1614
1648
|
// src/blocks/HtmlPreview.tsx
|
|
1615
1649
|
var import_react6 = require("react");
|
|
@@ -2609,14 +2643,16 @@ var toLabel = (size) => size.replace(/px$/, "");
|
|
|
2609
2643
|
var FontSizeButton = ({ editor }) => {
|
|
2610
2644
|
const [isOpen, setIsOpen] = (0, import_react13.useState)(false);
|
|
2611
2645
|
const dropdownRef = (0, import_react13.useRef)(null);
|
|
2612
|
-
const
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2646
|
+
const live = readSelectionFontSize(editor);
|
|
2647
|
+
const [optimistic, setOptimistic] = (0, import_react13.useState)(null);
|
|
2648
|
+
const lastLiveRef = (0, import_react13.useRef)(live);
|
|
2649
|
+
(0, import_react13.useEffect)(() => {
|
|
2650
|
+
if (live !== lastLiveRef.current) {
|
|
2651
|
+
lastLiveRef.current = live;
|
|
2652
|
+
setOptimistic(null);
|
|
2617
2653
|
}
|
|
2618
|
-
};
|
|
2619
|
-
const currentSize =
|
|
2654
|
+
}, [live]);
|
|
2655
|
+
const currentSize = optimistic ?? live;
|
|
2620
2656
|
const currentPx = parseFontSizePx(currentSize);
|
|
2621
2657
|
const [inputValue, setInputValue] = (0, import_react13.useState)(String(currentPx));
|
|
2622
2658
|
(0, import_react13.useEffect)(() => {
|
|
@@ -2637,8 +2673,10 @@ var FontSizeButton = ({ editor }) => {
|
|
|
2637
2673
|
if (!editor) return;
|
|
2638
2674
|
if (size === "") {
|
|
2639
2675
|
editor.removeStyles?.({ fontSize: "" });
|
|
2676
|
+
setOptimistic(null);
|
|
2640
2677
|
} else {
|
|
2641
2678
|
editor.addStyles?.({ fontSize: size });
|
|
2679
|
+
setOptimistic(size);
|
|
2642
2680
|
}
|
|
2643
2681
|
setIsOpen(false);
|
|
2644
2682
|
setTimeout(() => editor.focus?.());
|
|
@@ -2651,9 +2689,9 @@ var FontSizeButton = ({ editor }) => {
|
|
|
2651
2689
|
const stepBy = (0, import_react13.useCallback)(
|
|
2652
2690
|
(delta) => {
|
|
2653
2691
|
try {
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2692
|
+
const value = toFontSizeValue(currentPx + delta);
|
|
2693
|
+
editor?.addStyles?.({ fontSize: value });
|
|
2694
|
+
setOptimistic(value);
|
|
2657
2695
|
} catch (err) {
|
|
2658
2696
|
console.error("Font size step failed:", err);
|
|
2659
2697
|
}
|
|
@@ -2664,7 +2702,9 @@ var FontSizeButton = ({ editor }) => {
|
|
|
2664
2702
|
const n = parseInt(inputValue, 10);
|
|
2665
2703
|
if (Number.isFinite(n)) {
|
|
2666
2704
|
try {
|
|
2667
|
-
|
|
2705
|
+
const value = toFontSizeValue(n);
|
|
2706
|
+
editor?.addStyles?.({ fontSize: value });
|
|
2707
|
+
setOptimistic(value);
|
|
2668
2708
|
} catch (err) {
|
|
2669
2709
|
console.error("Font size apply failed:", err);
|
|
2670
2710
|
}
|
|
@@ -4265,8 +4305,67 @@ var TableSelectAllExtension = import_core8.Extension.create({
|
|
|
4265
4305
|
}
|
|
4266
4306
|
});
|
|
4267
4307
|
|
|
4268
|
-
// src/
|
|
4308
|
+
// src/extensions/InactiveSelectionExtension.ts
|
|
4309
|
+
var import_core9 = require("@tiptap/core");
|
|
4269
4310
|
var import_prosemirror_state7 = require("prosemirror-state");
|
|
4311
|
+
var import_prosemirror_view5 = require("prosemirror-view");
|
|
4312
|
+
var inactiveSelectionKey = new import_prosemirror_state7.PluginKey("lumirInactiveSelection");
|
|
4313
|
+
var InactiveSelectionExtension = import_core9.Extension.create({
|
|
4314
|
+
name: "lumirInactiveSelection",
|
|
4315
|
+
addProseMirrorPlugins() {
|
|
4316
|
+
return [
|
|
4317
|
+
new import_prosemirror_state7.Plugin({
|
|
4318
|
+
key: inactiveSelectionKey,
|
|
4319
|
+
// 플러그인 state = 에디터 포커스 여부
|
|
4320
|
+
state: {
|
|
4321
|
+
init: () => false,
|
|
4322
|
+
apply: (tr, value) => {
|
|
4323
|
+
const meta = tr.getMeta(inactiveSelectionKey);
|
|
4324
|
+
return typeof meta === "boolean" ? meta : value;
|
|
4325
|
+
}
|
|
4326
|
+
},
|
|
4327
|
+
props: {
|
|
4328
|
+
decorations(state) {
|
|
4329
|
+
const hasFocus = inactiveSelectionKey.getState(state);
|
|
4330
|
+
if (hasFocus) {
|
|
4331
|
+
return null;
|
|
4332
|
+
}
|
|
4333
|
+
const { selection } = state;
|
|
4334
|
+
if (selection.empty || !(selection instanceof import_prosemirror_state7.TextSelection)) {
|
|
4335
|
+
return null;
|
|
4336
|
+
}
|
|
4337
|
+
return import_prosemirror_view5.DecorationSet.create(state.doc, [
|
|
4338
|
+
import_prosemirror_view5.Decoration.inline(selection.from, selection.to, {
|
|
4339
|
+
class: "bn-inactive-selection"
|
|
4340
|
+
})
|
|
4341
|
+
]);
|
|
4342
|
+
}
|
|
4343
|
+
},
|
|
4344
|
+
view(view) {
|
|
4345
|
+
const sync = () => {
|
|
4346
|
+
const has = view.hasFocus();
|
|
4347
|
+
if (inactiveSelectionKey.getState(view.state) !== has) {
|
|
4348
|
+
view.dispatch(view.state.tr.setMeta(inactiveSelectionKey, has));
|
|
4349
|
+
}
|
|
4350
|
+
};
|
|
4351
|
+
view.dom.addEventListener("focus", sync);
|
|
4352
|
+
view.dom.addEventListener("blur", sync);
|
|
4353
|
+
const raf = requestAnimationFrame(sync);
|
|
4354
|
+
return {
|
|
4355
|
+
destroy() {
|
|
4356
|
+
cancelAnimationFrame(raf);
|
|
4357
|
+
view.dom.removeEventListener("focus", sync);
|
|
4358
|
+
view.dom.removeEventListener("blur", sync);
|
|
4359
|
+
}
|
|
4360
|
+
};
|
|
4361
|
+
}
|
|
4362
|
+
})
|
|
4363
|
+
];
|
|
4364
|
+
}
|
|
4365
|
+
});
|
|
4366
|
+
|
|
4367
|
+
// src/blocks/columns/insertColumns.ts
|
|
4368
|
+
var import_prosemirror_state8 = require("prosemirror-state");
|
|
4270
4369
|
function insertTwoColumns(editor, showDivider = false) {
|
|
4271
4370
|
const tiptap = editor?._tiptapEditor;
|
|
4272
4371
|
if (!tiptap) {
|
|
@@ -4298,7 +4397,7 @@ function insertTwoColumns(editor, showDivider = false) {
|
|
|
4298
4397
|
try {
|
|
4299
4398
|
let tr = state.tr.insert(insertPos, list);
|
|
4300
4399
|
try {
|
|
4301
|
-
tr = tr.setSelection(
|
|
4400
|
+
tr = tr.setSelection(import_prosemirror_state8.TextSelection.create(tr.doc, insertPos + 4));
|
|
4302
4401
|
} catch {
|
|
4303
4402
|
}
|
|
4304
4403
|
tiptap.view.dispatch(tr.scrollIntoView());
|
|
@@ -4312,7 +4411,7 @@ function insertTwoColumns(editor, showDivider = false) {
|
|
|
4312
4411
|
var import_react29 = require("@blocknote/react");
|
|
4313
4412
|
|
|
4314
4413
|
// src/components/TextAlignButtonWithVA.tsx
|
|
4315
|
-
var
|
|
4414
|
+
var import_core10 = require("@blocknote/core");
|
|
4316
4415
|
var import_react19 = require("react");
|
|
4317
4416
|
var import_react20 = require("@blocknote/react");
|
|
4318
4417
|
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
@@ -4334,7 +4433,7 @@ var TextAlignButtonWithVA = (props) => {
|
|
|
4334
4433
|
const selectedBlocks = (0, import_react20.useSelectedBlocks)(editor);
|
|
4335
4434
|
const textAlignment = (0, import_react19.useMemo)(() => {
|
|
4336
4435
|
const block = selectedBlocks[0];
|
|
4337
|
-
if ((0,
|
|
4436
|
+
if ((0, import_core10.checkBlockHasDefaultProp)("textAlignment", block, editor)) {
|
|
4338
4437
|
return block.props.textAlignment;
|
|
4339
4438
|
}
|
|
4340
4439
|
if (block.type === "table") {
|
|
@@ -4343,7 +4442,7 @@ var TextAlignButtonWithVA = (props) => {
|
|
|
4343
4442
|
return;
|
|
4344
4443
|
}
|
|
4345
4444
|
const allCellsInTable = cellSelection.cells.map(
|
|
4346
|
-
({ row, col }) => (0,
|
|
4445
|
+
({ row, col }) => (0, import_core10.mapTableCell)(
|
|
4347
4446
|
block.content.rows[row].cells[col]
|
|
4348
4447
|
).props.textAlignment
|
|
4349
4448
|
);
|
|
@@ -4375,7 +4474,7 @@ var TextAlignButtonWithVA = (props) => {
|
|
|
4375
4474
|
}
|
|
4376
4475
|
}
|
|
4377
4476
|
tiptap.view?.dispatch(tr);
|
|
4378
|
-
} else if ((0,
|
|
4477
|
+
} else if ((0, import_core10.checkBlockTypeHasDefaultProp)("textAlignment", block.type, editor)) {
|
|
4379
4478
|
editor.updateBlock(block, {
|
|
4380
4479
|
props: { textAlignment: newAlignment }
|
|
4381
4480
|
});
|
|
@@ -4580,11 +4679,11 @@ function FontSizeButton2() {
|
|
|
4580
4679
|
const fontSizeInSchema = styleSchema.fontSize?.type === "fontSize" && styleSchema.fontSize?.propSchema === "string";
|
|
4581
4680
|
const selectedBlocks = (0, import_react25.useSelectedBlocks)(editor);
|
|
4582
4681
|
const [currentSize, setCurrentSize] = (0, import_react26.useState)(
|
|
4583
|
-
fontSizeInSchema ?
|
|
4682
|
+
fontSizeInSchema ? readSelectionFontSize(editor) : ""
|
|
4584
4683
|
);
|
|
4585
4684
|
(0, import_react25.useEditorContentOrSelectionChange)(() => {
|
|
4586
4685
|
if (fontSizeInSchema) {
|
|
4587
|
-
setCurrentSize(
|
|
4686
|
+
setCurrentSize(readSelectionFontSize(editor));
|
|
4588
4687
|
}
|
|
4589
4688
|
}, editor);
|
|
4590
4689
|
const currentPx = parseFontSizePx(currentSize);
|
|
@@ -4602,7 +4701,9 @@ function FontSizeButton2() {
|
|
|
4602
4701
|
);
|
|
4603
4702
|
const stepBy = (0, import_react26.useCallback)(
|
|
4604
4703
|
(delta) => {
|
|
4605
|
-
|
|
4704
|
+
const value = toFontSizeValue(currentPx + delta);
|
|
4705
|
+
ed.addStyles({ fontSize: value });
|
|
4706
|
+
setCurrentSize(value);
|
|
4606
4707
|
},
|
|
4607
4708
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4608
4709
|
[currentPx]
|
|
@@ -4610,7 +4711,9 @@ function FontSizeButton2() {
|
|
|
4610
4711
|
const applyInput = (0, import_react26.useCallback)(() => {
|
|
4611
4712
|
const n = parseInt(inputValue, 10);
|
|
4612
4713
|
if (Number.isFinite(n)) {
|
|
4613
|
-
|
|
4714
|
+
const value = toFontSizeValue(n);
|
|
4715
|
+
ed.addStyles({ fontSize: value });
|
|
4716
|
+
setCurrentSize(value);
|
|
4614
4717
|
} else {
|
|
4615
4718
|
setInputValue(String(currentPx));
|
|
4616
4719
|
}
|
|
@@ -4724,7 +4827,7 @@ function FontSizeButton2() {
|
|
|
4724
4827
|
}
|
|
4725
4828
|
|
|
4726
4829
|
// src/components/color/LumirColorControls.tsx
|
|
4727
|
-
var
|
|
4830
|
+
var import_core11 = require("@blocknote/core");
|
|
4728
4831
|
var import_react27 = require("@blocknote/react");
|
|
4729
4832
|
var import_react28 = require("react");
|
|
4730
4833
|
var import_jsx_runtime22 = require("react/jsx-runtime");
|
|
@@ -4972,7 +5075,7 @@ function LumirCellColorPickerButton(props) {
|
|
|
4972
5075
|
const updateColor = (color, type) => {
|
|
4973
5076
|
const newTable = props.block.content.rows.map((row) => ({
|
|
4974
5077
|
...row,
|
|
4975
|
-
cells: row.cells.map((cell) => (0,
|
|
5078
|
+
cells: row.cells.map((cell) => (0, import_core11.mapTableCell)(cell))
|
|
4976
5079
|
}));
|
|
4977
5080
|
if (type === "text") {
|
|
4978
5081
|
newTable[props.rowIndex].cells[props.colIndex].props.textColor = color;
|
|
@@ -5003,11 +5106,11 @@ function LumirCellColorPickerButton(props) {
|
|
|
5003
5106
|
textTitle: "\uC140 \uAE00\uC790\uC0C9",
|
|
5004
5107
|
backgroundTitle: "\uC140 \uBC30\uACBD",
|
|
5005
5108
|
text: editor.settings.tables.cellTextColor ? {
|
|
5006
|
-
color: (0,
|
|
5109
|
+
color: (0, import_core11.isTableCell)(currentCell) ? currentCell.props.textColor : "default",
|
|
5007
5110
|
setColor: (color) => updateColor(color, "text")
|
|
5008
5111
|
} : void 0,
|
|
5009
5112
|
background: editor.settings.tables.cellBackgroundColor ? {
|
|
5010
|
-
color: (0,
|
|
5113
|
+
color: (0, import_core11.isTableCell)(currentCell) ? currentCell.props.backgroundColor : "default",
|
|
5011
5114
|
setColor: (color) => updateColor(color, "background")
|
|
5012
5115
|
} : void 0
|
|
5013
5116
|
}
|
|
@@ -6811,7 +6914,9 @@ function LumirEditor({
|
|
|
6811
6914
|
// 표 블록 정렬(좌/가운데/우) attr.
|
|
6812
6915
|
TableAlignmentExtension,
|
|
6813
6916
|
// 셀 포커스 시 Ctrl/Cmd+A → 표 전체 선택.
|
|
6814
|
-
TableSelectAllExtension
|
|
6917
|
+
TableSelectAllExtension,
|
|
6918
|
+
// blur 상태(툴바 드롭다운 조작 등)에서도 텍스트 선택 하이라이트 유지.
|
|
6919
|
+
InactiveSelectionExtension
|
|
6815
6920
|
]
|
|
6816
6921
|
},
|
|
6817
6922
|
placeholders: placeholder ? { default: placeholder, emptyDocument: placeholder } : void 0,
|
|
@@ -7470,7 +7575,7 @@ function LumirEditor({
|
|
|
7470
7575
|
allItems.push({
|
|
7471
7576
|
title: "Link Preview",
|
|
7472
7577
|
onItemClick: () => {
|
|
7473
|
-
(0,
|
|
7578
|
+
(0, import_core12.insertOrUpdateBlock)(editor, {
|
|
7474
7579
|
type: "linkPreview",
|
|
7475
7580
|
props: { url: "" }
|
|
7476
7581
|
});
|