@growgroup/visual-editor 0.4.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -0
- package/dist/editor.css +1 -1
- package/package.json +7 -2
- package/src/editor/FrontendVisualEditor.tsx +244 -17
- package/src/editor/collab/CollabPresence.tsx +172 -0
- package/src/editor/collab/binding.ts +269 -0
- package/src/editor/collab/color.ts +28 -0
- package/src/editor/collab/connection.ts +110 -0
- package/src/editor/collab/layer.ts +168 -0
- package/src/editor/collab/merge.ts +112 -0
- package/src/editor/collab/morph.ts +113 -0
- package/src/editor/collab/room.ts +82 -0
- package/src/editor/collab/runtime.ts +442 -0
- package/src/editor/collab/signals.ts +11 -0
- package/src/editor/collab/store.ts +192 -0
- package/src/editor/collab/useCollab.ts +266 -0
- package/src/editor/components/AiPromptPopover.tsx +3 -2
- package/src/editor/components/EditorCanvas.tsx +27 -0
- package/src/editor/components/EditorContextMenu.tsx +80 -0
- package/src/editor/components/EditorPropertyPanel.tsx +248 -80
- package/src/editor/components/EditorSidebar.tsx +2 -2
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +41 -19
- package/src/editor/components/ppt/PptChrome.tsx +46 -4
- package/src/editor/components/ppt/PptComments.tsx +3 -8
- package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +94 -50
- package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +12 -6
- package/src/editor/components/shell/EditorTopBar.tsx +13 -1
- package/src/editor/components/shell/PagesPanel.tsx +3 -0
- package/src/editor/constants.ts +24 -0
- package/src/editor/contexts/EditorAppearanceContext.tsx +5 -5
- package/src/editor/contexts/EditorHistoryContext.tsx +2 -0
- package/src/editor/contexts/MultiPageCanvasContext.tsx +120 -15
- package/src/editor/hooks/index.ts +3 -0
- package/src/editor/hooks/useDragResize.ts +30 -5
- package/src/editor/hooks/useElementActions.ts +41 -9
- package/src/editor/hooks/useElementSelection.ts +4 -2
- package/src/editor/hooks/useGoogleFonts.ts +6 -8
- package/src/editor/hooks/useIframeSetup.ts +10 -1
- package/src/editor/hooks/useInlineTextSelection.ts +16 -0
- package/src/editor/hooks/useKeepArtboardInPlace.ts +87 -0
- package/src/editor/hooks/useLinkNavigation.ts +160 -0
- package/src/editor/hooks/useResizablePanel.ts +3 -2
- package/src/editor/parts.ts +2 -0
- package/src/editor/types.ts +4 -0
- package/src/editor/utils/debug.ts +2 -5
- package/src/editor/utils/dom-utils.ts +45 -0
- package/src/editor/utils/drop-target.ts +1 -1
- package/src/editor/utils/flex-reorder.ts +1 -1
- package/src/editor/utils/free-layout-tree.ts +562 -0
- package/src/editor/utils/free-layout.ts +794 -0
- package/src/editor/utils/geometry.ts +41 -3
- package/src/editor/utils/html-utils.ts +4 -1
- package/src/editor/utils/inline-text-style.ts +426 -0
- package/src/editor/utils/link-navigation.ts +474 -0
- package/src/editor/utils/restore-flow.ts +1 -1
- package/src/editor/utils/smart-guides.ts +1 -1
- package/src/editor/utils/storage.ts +47 -0
- package/src/editor/utils/style-utils.ts +24 -0
- package/src/editor/utils/tailwind-utils.ts +12 -0
- package/src/editor/utils/text-line-break.ts +121 -0
- package/src/index.ts +15 -0
- package/src/io.ts +29 -0
- package/src/styles/skin.css +17 -0
- package/src/version.ts +1 -1
|
@@ -67,7 +67,8 @@ import {
|
|
|
67
67
|
import { Checkbox } from "../../components/ui/checkbox";
|
|
68
68
|
import { buildFilterString, buildTransformString } from "../utils/style-utils";
|
|
69
69
|
import { useEditorContext } from "../EditorContext";
|
|
70
|
-
import { useElementActions, useEditorColors } from "../hooks";
|
|
70
|
+
import { useElementActions, useEditorColors, useInlineTextSelection } from "../hooks";
|
|
71
|
+
import { MIXED } from "../utils/inline-text-style";
|
|
71
72
|
import { FONT_WEIGHTS } from "../constants";
|
|
72
73
|
import type { PanelSections, SelectedElementInfo } from "../types";
|
|
73
74
|
import {
|
|
@@ -104,6 +105,8 @@ import {
|
|
|
104
105
|
pxValue,
|
|
105
106
|
MIN_ELEMENT_SIZE,
|
|
106
107
|
} from "../utils/geometry";
|
|
108
|
+
import { setElementAbsolute, setElementAuto, keepsInlineGeometry } from "../utils/free-layout";
|
|
109
|
+
import { extractElementInfo } from "../utils/style-utils";
|
|
107
110
|
import { convertInlineStylesToTailwind } from "../utils/tailwind-utils";
|
|
108
111
|
import {
|
|
109
112
|
isAspectRatioLocked,
|
|
@@ -418,6 +421,21 @@ const isPlainPxValue = (raw: string | number | undefined | null): boolean => {
|
|
|
418
421
|
return /^-?\d+(\.\d+)?px$/.test(raw.trim());
|
|
419
422
|
};
|
|
420
423
|
|
|
424
|
+
/** サイズ欄の最小・最大。raw は SelectedElementInfo の読み戻し先 */
|
|
425
|
+
// 2 列の格子に行優先で並べるので、左の列(W の下)が幅、右の列(H の下)が高さになる順にする
|
|
426
|
+
const SIZE_LIMIT_FIELDS = [
|
|
427
|
+
{ id: "min-w", prop: "minWidth", raw: "rawMinWidth", label: "最小", title: "最小幅(min-width)", dimension: "width" },
|
|
428
|
+
{ id: "min-h", prop: "minHeight", raw: "rawMinHeight", label: "最小", title: "最小の高さ(min-height)", dimension: "height" },
|
|
429
|
+
{ id: "max-w", prop: "maxWidth", raw: "rawMaxWidth", label: "最大", title: "最大幅(max-width)", dimension: "width" },
|
|
430
|
+
{ id: "max-h", prop: "maxHeight", raw: "rawMaxHeight", label: "最大", title: "最大の高さ(max-height)", dimension: "height" },
|
|
431
|
+
] as const;
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* サイズ欄のラベルの列(W / H / 最小 / 最大)。幅をそろえて、どの行も箱の左端と右端が縦に並ぶようにする。
|
|
435
|
+
* 幅は「最小」の 2 文字(パネルでは 12px)が収まる 24px
|
|
436
|
+
*/
|
|
437
|
+
const SIZE_LABEL_CLASS = "text-[10px] text-gray-500 w-6 block whitespace-nowrap";
|
|
438
|
+
|
|
421
439
|
/**
|
|
422
440
|
* 表示値の決定
|
|
423
441
|
* raw が var(--x) / % / vw など「px 以外の指定」なら、実測 px で上書きせず raw をそのまま返す。
|
|
@@ -463,7 +481,7 @@ function translateGroup(doc: Document, ids: string[], dx: number, dy: number): b
|
|
|
463
481
|
if (dy !== 0) {
|
|
464
482
|
el.style.top = pxValue((parseFloat(cs?.top ?? "") || 0) + dy);
|
|
465
483
|
}
|
|
466
|
-
convertInlineStylesToTailwind(el, ["left", "top"]);
|
|
484
|
+
if (!keepsInlineGeometry(el)) convertInlineStylesToTailwind(el, ["left", "top"]);
|
|
467
485
|
changed = true;
|
|
468
486
|
});
|
|
469
487
|
|
|
@@ -517,7 +535,7 @@ function scaleGroup(
|
|
|
517
535
|
el.style.height = `${newHeight}px`;
|
|
518
536
|
}
|
|
519
537
|
|
|
520
|
-
convertInlineStylesToTailwind(el, ["left", "top", "width", "height"]);
|
|
538
|
+
if (!keepsInlineGeometry(el)) convertInlineStylesToTailwind(el, ["left", "top", "width", "height"]);
|
|
521
539
|
changed = true;
|
|
522
540
|
});
|
|
523
541
|
|
|
@@ -821,6 +839,7 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
821
839
|
} = useEditorContext();
|
|
822
840
|
const {
|
|
823
841
|
updateElementStyle,
|
|
842
|
+
updateTextStyle,
|
|
824
843
|
updateLinkAttribute,
|
|
825
844
|
updateElementAttribute,
|
|
826
845
|
deleteElement,
|
|
@@ -828,6 +847,14 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
828
847
|
alignElements,
|
|
829
848
|
} = useElementActions();
|
|
830
849
|
const { colors: editorColors } = useEditorColors();
|
|
850
|
+
// テキストの一部を選んでいるとき、文字の欄(書体・太さ・大きさ・字間・色・斜体・下線)は
|
|
851
|
+
// その範囲の値を出し、変えるとその範囲だけに当てる(updateTextStyle)。揃っていなければ「混在」
|
|
852
|
+
const inlineText = useInlineTextSelection();
|
|
853
|
+
const partialText =
|
|
854
|
+
inlineText && selectedElement && inlineText.hostId === selectedElement.id ? inlineText : null;
|
|
855
|
+
const textFontWeight = partialText ? partialText.fontWeight : selectedElement?.fontWeight;
|
|
856
|
+
const textFontStyle = partialText ? partialText.fontStyle : selectedElement?.fontStyle;
|
|
857
|
+
const textDecorationLine = partialText ? partialText.textDecoration : selectedElement?.textDecoration;
|
|
831
858
|
|
|
832
859
|
// Component instance management
|
|
833
860
|
const {
|
|
@@ -860,6 +887,30 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
860
887
|
const slot = root === el ? null : (el.closest("[data-slot]") as HTMLElement | null);
|
|
861
888
|
return { root, isRoot: root === el, slotName: slot && root.contains(slot) && slot !== root ? slot.getAttribute("data-slot") : null };
|
|
862
889
|
}, [selectedElement, getIframeDoc]);
|
|
890
|
+
/**
|
|
891
|
+
* 配置の切替(Figma の「絶対位置」トグル)。webpage だけに出す。
|
|
892
|
+
*
|
|
893
|
+
* slide は開いた時点で全部が絶対配置なので、このトグルには意味がない。
|
|
894
|
+
* webpage は流し込みが既定で、ここが「この要素だけ流れから外す」入口になる。
|
|
895
|
+
*/
|
|
896
|
+
const setPositionMode = useCallback(
|
|
897
|
+
(mode: "auto" | "absolute") => {
|
|
898
|
+
const doc = getIframeDoc();
|
|
899
|
+
const el = selectedElement?.id
|
|
900
|
+
? doc?.querySelector<HTMLElement>(`[data-element-id="${selectedElement.id}"]`)
|
|
901
|
+
: null;
|
|
902
|
+
if (!doc || !el) return;
|
|
903
|
+
const changed = mode === "absolute" ? setElementAbsolute(el, doc) : setElementAuto(el, doc);
|
|
904
|
+
if (!changed) return;
|
|
905
|
+
notifyIframeChange();
|
|
906
|
+
// 枠とパネルの数値をその場で追従させる(X/Y が使えるようになった/なくなったの反映)
|
|
907
|
+
refreshSelectionOverlay(doc);
|
|
908
|
+
const info = extractElementInfo(el, doc);
|
|
909
|
+
if (info) setSelectedElement(info);
|
|
910
|
+
},
|
|
911
|
+
[getIframeDoc, selectedElement?.id, notifyIframeChange, setSelectedElement],
|
|
912
|
+
);
|
|
913
|
+
|
|
863
914
|
const selectedPart = useMemo(() => (selectedPartRoot ? partInfoOf(selectedPartRoot.root) : null), [selectedPartRoot]);
|
|
864
915
|
const selectedPartLabel = useMemo(() => {
|
|
865
916
|
if (!selectedPart) return "";
|
|
@@ -943,8 +994,18 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
943
994
|
storageKey: 'editor-property-panel-width',
|
|
944
995
|
});
|
|
945
996
|
|
|
997
|
+
// サイズ欄の「最小・最大」を開いているか(値が入っている要素では常に出す。これは空の要素で開いたとき)。
|
|
998
|
+
// 別の要素を選んだら閉じる
|
|
999
|
+
const [sizeLimitsOpen, setSizeLimitsOpen] = useState(false);
|
|
1000
|
+
useEffect(() => {
|
|
1001
|
+
setSizeLimitsOpen(false);
|
|
1002
|
+
}, [selectedElement?.id]);
|
|
1003
|
+
|
|
946
1004
|
const [openSections, setOpenSections] = useState<PanelSections>({
|
|
947
|
-
|
|
1005
|
+
// 以前は流し込みのページで畳んでいた(X/Y が効かず詳細操作だったため)。
|
|
1006
|
+
// 自由配置の「配置: 自動 / 絶対」トグルがこの中に入り、流し込みを抜ける
|
|
1007
|
+
// 入口になったので、webpage でも開いておく(畳んだままだと見つけられない)
|
|
1008
|
+
position: true,
|
|
948
1009
|
layout: true, // レイアウトセクション
|
|
949
1010
|
appearance: true, // 外見セクション
|
|
950
1011
|
image: false, // 塗りに統合されたため非表示
|
|
@@ -1387,7 +1448,11 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
1387
1448
|
|
|
1388
1449
|
{/* 位置 X/Y
|
|
1389
1450
|
LiveGeom で包むことで、ここだけがドラッグ中のフレーム更新を受ける。
|
|
1390
|
-
raw が var()/% 等の場合は実測 px で上書きしない(liveOrRaw 参照)。
|
|
1451
|
+
raw が var()/% 等の場合は実測 px で上書きしない(liveOrRaw 参照)。
|
|
1452
|
+
|
|
1453
|
+
webpage では上に「配置: 自動 / 絶対」を出す。流し込みの要素に
|
|
1454
|
+
left/top を書いても見た目は動かないので、絶対でないときは
|
|
1455
|
+
X/Y を触れなくして「効かない入力」を無くす。 */}
|
|
1391
1456
|
<div>
|
|
1392
1457
|
<Label className="text-[10px] text-gray-500 mb-1 block">
|
|
1393
1458
|
位置
|
|
@@ -1397,39 +1462,77 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
1397
1462
|
const live = geometry?.members.find(
|
|
1398
1463
|
(m) => m.id === selectedElement.id,
|
|
1399
1464
|
);
|
|
1465
|
+
// 流し込みの要素かどうかは computed position が正。
|
|
1466
|
+
// 計測がまだなら従来どおり編集できる状態にしておく
|
|
1467
|
+
const isAbsolute = live ? live.canMove : true;
|
|
1468
|
+
const lockedHint =
|
|
1469
|
+
"絶対配置ではないため、X / Y を直接指定できません(上の「配置」を絶対にすると指定できます)";
|
|
1400
1470
|
return (
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1471
|
+
<>
|
|
1472
|
+
{editorMode === "webpage" && (
|
|
1473
|
+
<div
|
|
1474
|
+
className="mb-2 flex items-center gap-2"
|
|
1475
|
+
data-gg-position-mode={isAbsolute ? "absolute" : "auto"}
|
|
1476
|
+
>
|
|
1477
|
+
<span className="text-[10px] text-gray-500 shrink-0">配置</span>
|
|
1478
|
+
<div className="flex rounded overflow-hidden border border-[#4a4a4a]">
|
|
1479
|
+
<button
|
|
1480
|
+
type="button"
|
|
1481
|
+
onClick={() => setPositionMode("auto")}
|
|
1482
|
+
className={`px-2 h-6 text-[10px] ${!isAbsolute ? "bg-[#0d99ff] text-white" : "bg-[#383838] text-gray-400 hover:bg-[#4a4a4a]"}`}
|
|
1483
|
+
title="流し込み(親の並びに従う)"
|
|
1484
|
+
>
|
|
1485
|
+
自動
|
|
1486
|
+
</button>
|
|
1487
|
+
<button
|
|
1488
|
+
type="button"
|
|
1489
|
+
onClick={() => setPositionMode("absolute")}
|
|
1490
|
+
className={`px-2 h-6 text-[10px] ${isAbsolute ? "bg-[#0d99ff] text-white" : "bg-[#383838] text-gray-400 hover:bg-[#4a4a4a]"}`}
|
|
1491
|
+
title="絶対配置(今の位置のまま流れから外し、X / Y で動かせるようにする)"
|
|
1492
|
+
>
|
|
1493
|
+
絶対
|
|
1494
|
+
</button>
|
|
1495
|
+
</div>
|
|
1496
|
+
</div>
|
|
1497
|
+
)}
|
|
1498
|
+
<div
|
|
1499
|
+
className="grid grid-cols-2 gap-2"
|
|
1500
|
+
title={editorMode === "webpage" && !isAbsolute ? lockedHint : undefined}
|
|
1501
|
+
>
|
|
1502
|
+
<VariableAwareUnitInput
|
|
1503
|
+
value={liveOrRaw(
|
|
1504
|
+
selectedElement.rawLeft,
|
|
1505
|
+
live?.styleLeft,
|
|
1506
|
+
selectedElement.x,
|
|
1507
|
+
)}
|
|
1508
|
+
onChange={(val) => updateElementStyle({ left: val })}
|
|
1509
|
+
units={POSITION_UNITS}
|
|
1510
|
+
defaultUnit="px"
|
|
1511
|
+
category="spacing"
|
|
1512
|
+
label="X"
|
|
1513
|
+
compact
|
|
1514
|
+
hideVariableLink
|
|
1515
|
+
conversionContext={conversionContext}
|
|
1516
|
+
disabled={editorMode === "webpage" && !isAbsolute}
|
|
1517
|
+
/>
|
|
1518
|
+
<VariableAwareUnitInput
|
|
1519
|
+
value={liveOrRaw(
|
|
1520
|
+
selectedElement.rawTop,
|
|
1521
|
+
live?.styleTop,
|
|
1522
|
+
selectedElement.y,
|
|
1523
|
+
)}
|
|
1524
|
+
onChange={(val) => updateElementStyle({ top: val })}
|
|
1525
|
+
units={POSITION_UNITS}
|
|
1526
|
+
defaultUnit="px"
|
|
1527
|
+
category="spacing"
|
|
1528
|
+
label="Y"
|
|
1529
|
+
compact
|
|
1530
|
+
hideVariableLink
|
|
1531
|
+
conversionContext={conversionContext}
|
|
1532
|
+
disabled={editorMode === "webpage" && !isAbsolute}
|
|
1533
|
+
/>
|
|
1534
|
+
</div>
|
|
1535
|
+
</>
|
|
1433
1536
|
);
|
|
1434
1537
|
}}
|
|
1435
1538
|
</LiveGeom>
|
|
@@ -1526,7 +1629,7 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
1526
1629
|
/>
|
|
1527
1630
|
|
|
1528
1631
|
{/* サイズ W/H */}
|
|
1529
|
-
<div>
|
|
1632
|
+
<div className="relative" data-size-section>
|
|
1530
1633
|
<Label className="text-[10px] text-gray-500 mb-1 block">
|
|
1531
1634
|
サイズ
|
|
1532
1635
|
</Label>
|
|
@@ -1612,11 +1715,7 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
1612
1715
|
});
|
|
1613
1716
|
}
|
|
1614
1717
|
}}
|
|
1615
|
-
label={
|
|
1616
|
-
<span className="text-[10px] text-gray-500 w-3 text-center block">
|
|
1617
|
-
W
|
|
1618
|
-
</span>
|
|
1619
|
-
}
|
|
1718
|
+
label={<span className={SIZE_LABEL_CLASS}>W</span>}
|
|
1620
1719
|
dimension="width"
|
|
1621
1720
|
canFill={isParentFlexContainer}
|
|
1622
1721
|
canHug={true}
|
|
@@ -1706,11 +1805,7 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
1706
1805
|
});
|
|
1707
1806
|
}
|
|
1708
1807
|
}}
|
|
1709
|
-
label={
|
|
1710
|
-
<span className="text-[10px] text-gray-500 w-3 text-center block">
|
|
1711
|
-
H
|
|
1712
|
-
</span>
|
|
1713
|
-
}
|
|
1808
|
+
label={<span className={SIZE_LABEL_CLASS}>H</span>}
|
|
1714
1809
|
dimension="height"
|
|
1715
1810
|
canFill={isParentFlexContainer}
|
|
1716
1811
|
canHug={true}
|
|
@@ -1722,6 +1817,58 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
1722
1817
|
</LiveGeom>
|
|
1723
1818
|
</div>
|
|
1724
1819
|
</div>
|
|
1820
|
+
|
|
1821
|
+
{/* 最小・最大の幅と高さ(min-width / max-width / min-height / max-height)。
|
|
1822
|
+
既定では隠し、「+ 最小・最大」で出す。値が入っている要素では最初から出す。
|
|
1823
|
+
適用は W / H と同じ updateElementStyle(複数選択なら全要素に同じ値)。空にすると指定を消す */}
|
|
1824
|
+
{(() => {
|
|
1825
|
+
// 最小の 0(Tailwind の min-w-0。構成ラフに多い)は「制約なし」と見分けがつかないので、それだけでは開かない
|
|
1826
|
+
const hasLimit = SIZE_LIMIT_FIELDS.some((f) => {
|
|
1827
|
+
const v = selectedElement[f.raw];
|
|
1828
|
+
return !!v && !(f.prop.startsWith("min") && /^0(px)?$/.test(v));
|
|
1829
|
+
});
|
|
1830
|
+
const visible = hasLimit || sizeLimitsOpen;
|
|
1831
|
+
return (
|
|
1832
|
+
<>
|
|
1833
|
+
{!hasLimit && (
|
|
1834
|
+
<button
|
|
1835
|
+
type="button"
|
|
1836
|
+
data-size-limits-toggle
|
|
1837
|
+
aria-expanded={visible}
|
|
1838
|
+
onClick={() => setSizeLimitsOpen((v) => !v)}
|
|
1839
|
+
className="absolute right-0 top-0 text-[10px] text-gray-400 hover:text-white"
|
|
1840
|
+
title="最小・最大の幅と高さ"
|
|
1841
|
+
>
|
|
1842
|
+
{visible ? "− 最小・最大" : "+ 最小・最大"}
|
|
1843
|
+
</button>
|
|
1844
|
+
)}
|
|
1845
|
+
{visible && (
|
|
1846
|
+
<div className="grid grid-cols-2 gap-x-2 gap-y-1.5 mt-1.5" data-size-limits>
|
|
1847
|
+
{SIZE_LIMIT_FIELDS.map((f) => (
|
|
1848
|
+
<VariableAwareSizeInput
|
|
1849
|
+
key={f.id}
|
|
1850
|
+
fieldId={f.id}
|
|
1851
|
+
variant="limit"
|
|
1852
|
+
value={selectedElement[f.raw] ?? ""}
|
|
1853
|
+
mode="fixed"
|
|
1854
|
+
onChangeMode={() => {}}
|
|
1855
|
+
onChangeValue={(val) => updateElementStyle({ [f.prop]: val })}
|
|
1856
|
+
label={
|
|
1857
|
+
<span className={SIZE_LABEL_CLASS} title={f.title}>
|
|
1858
|
+
{f.label}
|
|
1859
|
+
</span>
|
|
1860
|
+
}
|
|
1861
|
+
placeholder="—"
|
|
1862
|
+
dimension={f.dimension}
|
|
1863
|
+
min={0}
|
|
1864
|
+
conversionContext={conversionContext}
|
|
1865
|
+
/>
|
|
1866
|
+
))}
|
|
1867
|
+
</div>
|
|
1868
|
+
)}
|
|
1869
|
+
</>
|
|
1870
|
+
);
|
|
1871
|
+
})()}
|
|
1725
1872
|
{/* [修復] 「サイズ」ブロックの閉じタグ。
|
|
1726
1873
|
縦横比ロックの追加作業が中断され、この1つが欠けていた */}
|
|
1727
1874
|
</div>
|
|
@@ -2107,30 +2254,39 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
2107
2254
|
/>
|
|
2108
2255
|
</CollapsibleTrigger>
|
|
2109
2256
|
<CollapsibleContent className="space-y-2 pb-3">
|
|
2257
|
+
{partialText && (
|
|
2258
|
+
<p data-typo-scope="selection" className="text-[10px] text-[#0d99ff]">
|
|
2259
|
+
選んだ文字だけに当てます(行間・配置は要素全体)
|
|
2260
|
+
</p>
|
|
2261
|
+
)}
|
|
2110
2262
|
<div>
|
|
2111
2263
|
<Label className="text-[10px] text-gray-500">フォント</Label>
|
|
2112
2264
|
<GoogleFontPicker
|
|
2113
|
-
value={
|
|
2265
|
+
value={
|
|
2266
|
+
partialText
|
|
2267
|
+
? partialText.fontFamily === MIXED ? "" : partialText.fontFamily
|
|
2268
|
+
: selectedElement.fontFamily
|
|
2269
|
+
}
|
|
2114
2270
|
onChange={(value) =>
|
|
2115
|
-
|
|
2271
|
+
updateTextStyle({ fontFamily: value })
|
|
2116
2272
|
}
|
|
2117
2273
|
iframeDoc={iframeRef.current?.contentDocument || null}
|
|
2118
2274
|
className="w-full"
|
|
2119
2275
|
/>
|
|
2120
2276
|
</div>
|
|
2121
2277
|
<div className="grid grid-cols-2 gap-2">
|
|
2122
|
-
<div>
|
|
2278
|
+
<div data-typo-field="font-weight">
|
|
2123
2279
|
<Label className="text-[10px] text-gray-500">
|
|
2124
2280
|
ウェイト
|
|
2125
2281
|
</Label>
|
|
2126
2282
|
<Select
|
|
2127
|
-
value={
|
|
2283
|
+
value={textFontWeight === MIXED ? "" : textFontWeight}
|
|
2128
2284
|
onValueChange={(value) =>
|
|
2129
|
-
|
|
2285
|
+
updateTextStyle({ fontWeight: value })
|
|
2130
2286
|
}
|
|
2131
2287
|
>
|
|
2132
2288
|
<SelectTrigger className="h-7 text-xs bg-[#383838] border-[#444444] text-white">
|
|
2133
|
-
<SelectValue />
|
|
2289
|
+
<SelectValue placeholder={textFontWeight === MIXED ? "混在" : undefined} />
|
|
2134
2290
|
</SelectTrigger>
|
|
2135
2291
|
<SelectContent>
|
|
2136
2292
|
{FONT_WEIGHTS.map((w) => (
|
|
@@ -2141,13 +2297,18 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
2141
2297
|
</SelectContent>
|
|
2142
2298
|
</Select>
|
|
2143
2299
|
</div>
|
|
2144
|
-
<div>
|
|
2300
|
+
<div data-typo-field="font-size">
|
|
2145
2301
|
<span className="text-[10px] text-gray-500 mb-1 block">
|
|
2146
2302
|
サイズ
|
|
2147
2303
|
</span>
|
|
2148
2304
|
<VariableAwareUnitInput
|
|
2149
|
-
value={
|
|
2150
|
-
|
|
2305
|
+
value={
|
|
2306
|
+
partialText
|
|
2307
|
+
? partialText.fontSize === MIXED ? "" : `${Math.round(parseFloat(partialText.fontSize) * 100) / 100}px`
|
|
2308
|
+
: selectedElement.rawFontSize || `${Math.round(selectedElement.fontSize)}px`
|
|
2309
|
+
}
|
|
2310
|
+
mixed={partialText?.fontSize === MIXED}
|
|
2311
|
+
onChange={(val) => updateTextStyle({ fontSize: val })}
|
|
2151
2312
|
units={FONT_SIZE_UNITS}
|
|
2152
2313
|
defaultUnit="px"
|
|
2153
2314
|
category="typography"
|
|
@@ -2191,14 +2352,14 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
2191
2352
|
variant="ghost"
|
|
2192
2353
|
size="icon"
|
|
2193
2354
|
onClick={() =>
|
|
2194
|
-
|
|
2355
|
+
updateTextStyle({
|
|
2195
2356
|
fontWeight:
|
|
2196
|
-
|
|
2357
|
+
textFontWeight === "700"
|
|
2197
2358
|
? "400"
|
|
2198
2359
|
: "700",
|
|
2199
2360
|
})
|
|
2200
2361
|
}
|
|
2201
|
-
className={`h-7 w-7 text-gray-400 ${
|
|
2362
|
+
className={`h-7 w-7 text-gray-400 ${textFontWeight === "700" ? "bg-[#0d99ff] text-white" : "hover:bg-[#4a4a4a] hover:text-white"}`}
|
|
2202
2363
|
>
|
|
2203
2364
|
<Bold className="w-3.5 h-3.5" />
|
|
2204
2365
|
</Button>
|
|
@@ -2206,14 +2367,14 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
2206
2367
|
variant="ghost"
|
|
2207
2368
|
size="icon"
|
|
2208
2369
|
onClick={() =>
|
|
2209
|
-
|
|
2370
|
+
updateTextStyle({
|
|
2210
2371
|
fontStyle:
|
|
2211
|
-
|
|
2372
|
+
textFontStyle === "italic"
|
|
2212
2373
|
? "normal"
|
|
2213
2374
|
: "italic",
|
|
2214
2375
|
})
|
|
2215
2376
|
}
|
|
2216
|
-
className={`h-7 w-7 text-gray-400 ${
|
|
2377
|
+
className={`h-7 w-7 text-gray-400 ${textFontStyle === "italic" ? "bg-[#0d99ff] text-white" : "hover:bg-[#4a4a4a] hover:text-white"}`}
|
|
2217
2378
|
>
|
|
2218
2379
|
<Italic className="w-3.5 h-3.5" />
|
|
2219
2380
|
</Button>
|
|
@@ -2221,22 +2382,22 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
2221
2382
|
variant="ghost"
|
|
2222
2383
|
size="icon"
|
|
2223
2384
|
onClick={() =>
|
|
2224
|
-
|
|
2385
|
+
updateTextStyle({
|
|
2225
2386
|
textDecoration:
|
|
2226
|
-
|
|
2387
|
+
textDecorationLine?.includes(
|
|
2227
2388
|
"underline",
|
|
2228
2389
|
)
|
|
2229
2390
|
? "none"
|
|
2230
2391
|
: "underline",
|
|
2231
2392
|
})
|
|
2232
2393
|
}
|
|
2233
|
-
className={`h-7 w-7 text-gray-400 ${
|
|
2394
|
+
className={`h-7 w-7 text-gray-400 ${textDecorationLine?.includes("underline") ? "bg-[#0d99ff] text-white" : "hover:bg-[#4a4a4a] hover:text-white"}`}
|
|
2234
2395
|
>
|
|
2235
2396
|
<Underline className="w-3.5 h-3.5" />
|
|
2236
2397
|
</Button>
|
|
2237
2398
|
</div>
|
|
2238
2399
|
<div className="grid grid-cols-2 gap-2 mt-2">
|
|
2239
|
-
<div>
|
|
2400
|
+
<div data-typo-field="line-height">
|
|
2240
2401
|
<span className="text-[10px] text-gray-500 mb-1 block">
|
|
2241
2402
|
行間
|
|
2242
2403
|
</span>
|
|
@@ -2258,17 +2419,22 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
2258
2419
|
conversionContext={conversionContext}
|
|
2259
2420
|
/>
|
|
2260
2421
|
</div>
|
|
2261
|
-
<div>
|
|
2422
|
+
<div data-typo-field="letter-spacing">
|
|
2262
2423
|
<span className="text-[10px] text-gray-500 mb-1 block">
|
|
2263
2424
|
文字間
|
|
2264
2425
|
</span>
|
|
2265
2426
|
<VariableAwareUnitInput
|
|
2266
|
-
value={
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2427
|
+
value={
|
|
2428
|
+
partialText
|
|
2429
|
+
? partialText.letterSpacing === MIXED ? "" : partialText.letterSpacing === "normal" ? "0em" : partialText.letterSpacing
|
|
2430
|
+
: selectedElement.rawLetterSpacing || (() => {
|
|
2431
|
+
const ls = selectedElement.letterSpacing;
|
|
2432
|
+
if (!ls || ls === "normal") return "0em";
|
|
2433
|
+
return ls;
|
|
2434
|
+
})()
|
|
2435
|
+
}
|
|
2436
|
+
mixed={partialText?.letterSpacing === MIXED}
|
|
2437
|
+
onChange={(val) => updateTextStyle({ letterSpacing: val })}
|
|
2272
2438
|
units={LETTER_SPACING_UNITS}
|
|
2273
2439
|
defaultUnit="em"
|
|
2274
2440
|
category="typography"
|
|
@@ -2280,13 +2446,14 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
2280
2446
|
</div>
|
|
2281
2447
|
</div>
|
|
2282
2448
|
</div>
|
|
2449
|
+
<div data-typo-field="color">
|
|
2283
2450
|
<FigmaColorPicker
|
|
2284
|
-
label="文字色"
|
|
2451
|
+
label={partialText?.color === MIXED ? "文字色(混在)" : "文字色"}
|
|
2285
2452
|
value={parseCssToFillConfig(
|
|
2286
|
-
selectedElement.color,
|
|
2453
|
+
partialText && partialText.color !== MIXED ? partialText.color : selectedElement.color,
|
|
2287
2454
|
undefined,
|
|
2288
2455
|
undefined,
|
|
2289
|
-
selectedElement.rawColor,
|
|
2456
|
+
partialText ? undefined : selectedElement.rawColor,
|
|
2290
2457
|
)}
|
|
2291
2458
|
onChange={(config) => {
|
|
2292
2459
|
if (config.type === "solid" && config.color) {
|
|
@@ -2295,22 +2462,23 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
2295
2462
|
const r = parseInt(config.color.slice(1, 3), 16);
|
|
2296
2463
|
const g = parseInt(config.color.slice(3, 5), 16);
|
|
2297
2464
|
const b = parseInt(config.color.slice(5, 7), 16);
|
|
2298
|
-
|
|
2465
|
+
updateTextStyle({
|
|
2299
2466
|
color: `rgba(${r}, ${g}, ${b}, ${opacity / 100})`,
|
|
2300
2467
|
});
|
|
2301
2468
|
} else {
|
|
2302
|
-
|
|
2469
|
+
updateTextStyle({ color: config.color });
|
|
2303
2470
|
}
|
|
2304
2471
|
}
|
|
2305
2472
|
}}
|
|
2306
2473
|
onVariableSelect={(varRef) => {
|
|
2307
|
-
|
|
2474
|
+
updateTextStyle({ color: varRef });
|
|
2308
2475
|
}}
|
|
2309
2476
|
presetColors={colorPresets}
|
|
2310
2477
|
showImageTab={false}
|
|
2311
2478
|
showVariablesTab={true}
|
|
2312
2479
|
onClose={restoreFocus}
|
|
2313
2480
|
/>
|
|
2481
|
+
</div>
|
|
2314
2482
|
</CollapsibleContent>
|
|
2315
2483
|
</Collapsible>
|
|
2316
2484
|
)}
|
|
@@ -32,7 +32,7 @@ export function EditorSidebar() {
|
|
|
32
32
|
iframeRef,
|
|
33
33
|
} = useEditorContext();
|
|
34
34
|
|
|
35
|
-
const { updateElementStyle } = useElementActions();
|
|
35
|
+
const { updateElementStyle, updateTextStyle } = useElementActions();
|
|
36
36
|
|
|
37
37
|
// レイヤーノードの展開/折りたたみ
|
|
38
38
|
const toggleNode = (nodeId: string) => {
|
|
@@ -177,7 +177,7 @@ export function EditorSidebar() {
|
|
|
177
177
|
onOpenChange={(open) =>
|
|
178
178
|
setOpenSections((prev) => ({ ...prev, typography: open }))
|
|
179
179
|
}
|
|
180
|
-
onStyleChange={
|
|
180
|
+
onStyleChange={updateTextStyle}
|
|
181
181
|
iframeDoc={iframeRef.current?.contentDocument || null}
|
|
182
182
|
/>
|
|
183
183
|
|