@growgroup/visual-editor 0.1.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/LICENSE +21 -0
- package/NOTICE +52 -0
- package/README.md +143 -0
- package/dist/editor.css +2 -0
- package/package.json +94 -0
- package/src/components/DeckSlideRender.tsx +21 -0
- package/src/components/SaveNote.tsx +21 -0
- package/src/components/auth/AuthProvider.tsx +44 -0
- package/src/components/ui/accordion.tsx +57 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +76 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +11 -0
- package/src/components/ui/context-menu.tsx +200 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/scrubbable-label.tsx +95 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/components/viewer/useDeck.ts +58 -0
- package/src/editor/EditorContext.tsx +632 -0
- package/src/editor/EditorToolbar.tsx +446 -0
- package/src/editor/FrontendVisualEditor.tsx +2595 -0
- package/src/editor/autosave.ts +34 -0
- package/src/editor/components/AiPromptPopover.tsx +378 -0
- package/src/editor/components/BreakpointGuides.tsx +323 -0
- package/src/editor/components/BreakpointSelector.tsx +180 -0
- package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
- package/src/editor/components/CssEditorDialog.tsx +168 -0
- package/src/editor/components/EditorCanvas.tsx +987 -0
- package/src/editor/components/EditorContextMenu.tsx +515 -0
- package/src/editor/components/EditorFooter.tsx +81 -0
- package/src/editor/components/EditorHeader.tsx +524 -0
- package/src/editor/components/EditorLayerPanel.tsx +1197 -0
- package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
- package/src/editor/components/EditorSidebar.tsx +226 -0
- package/src/editor/components/FigmaColorPicker.tsx +1577 -0
- package/src/editor/components/HtmlEditorDialog.tsx +71 -0
- package/src/editor/components/HtmlImportDialog.tsx +346 -0
- package/src/editor/components/JsEditorDialog.tsx +175 -0
- package/src/editor/components/LayerTreeItem.tsx +494 -0
- package/src/editor/components/MasterComponentEditor.tsx +846 -0
- package/src/editor/components/MediaLibraryDialog.tsx +447 -0
- package/src/editor/components/PageSettingsDialog.tsx +776 -0
- package/src/editor/components/VariablePicker.tsx +355 -0
- package/src/editor/components/VariablesDialog.tsx +564 -0
- package/src/editor/components/VariantEditor.tsx +687 -0
- package/src/editor/components/VariantMatrix.tsx +507 -0
- package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
- package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
- package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
- package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
- package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
- package/src/editor/components/component-panel/VariantList.tsx +260 -0
- package/src/editor/components/component-panel/index.ts +12 -0
- package/src/editor/components/index.ts +48 -0
- package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
- package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
- package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
- package/src/editor/components/multi-page/PageLabel.tsx +34 -0
- package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
- package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
- package/src/editor/components/multi-page/index.ts +7 -0
- package/src/editor/components/ppt/PptChrome.tsx +2508 -0
- package/src/editor/components/ppt/PptComments.tsx +589 -0
- package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
- package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
- package/src/editor/components/ppt/PptNotes.tsx +246 -0
- package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
- package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
- package/src/editor/components/property-panel/BorderSection.tsx +104 -0
- package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
- package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
- package/src/editor/components/property-panel/EffectSection.tsx +157 -0
- package/src/editor/components/property-panel/FillSection.tsx +69 -0
- package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
- package/src/editor/components/property-panel/ImageSection.tsx +206 -0
- package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
- package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
- package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
- package/src/editor/components/property-panel/LinkSection.tsx +103 -0
- package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
- package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
- package/src/editor/components/property-panel/TypographySection.tsx +230 -0
- package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
- package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
- package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
- package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
- package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
- package/src/editor/components/property-panel/index.ts +24 -0
- package/src/editor/components/property-panel/unit-utils.ts +261 -0
- package/src/editor/components/shell/EditorTopBar.tsx +392 -0
- package/src/editor/components/shell/LeftPanel.tsx +47 -0
- package/src/editor/components/shell/PagesPanel.tsx +118 -0
- package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
- package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
- package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
- package/src/editor/components/variables-panel/index.ts +7 -0
- package/src/editor/constants.ts +468 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
- package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
- package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
- package/src/editor/contexts/EditorRefsContext.tsx +204 -0
- package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
- package/src/editor/contexts/EditorToolContext.tsx +58 -0
- package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
- package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
- package/src/editor/contexts/EditorViewContext.tsx +101 -0
- package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
- package/src/editor/contexts/index.ts +17 -0
- package/src/editor/editor-skin.css +122 -0
- package/src/editor/hooks/index.ts +38 -0
- package/src/editor/hooks/useAiReplace.ts +397 -0
- package/src/editor/hooks/useAltMeasure.ts +67 -0
- package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
- package/src/editor/hooks/useCanvasControls.ts +1222 -0
- package/src/editor/hooks/useComponentEditMode.ts +658 -0
- package/src/editor/hooks/useComponentInstances.ts +515 -0
- package/src/editor/hooks/useContextMenuHandler.ts +157 -0
- package/src/editor/hooks/useCoordinateTransform.ts +207 -0
- package/src/editor/hooks/useDragResize.ts +1317 -0
- package/src/editor/hooks/useDrawingMode.ts +472 -0
- package/src/editor/hooks/useEditorColors.ts +144 -0
- package/src/editor/hooks/useEditorMessages.ts +108 -0
- package/src/editor/hooks/useElementActions.ts +1754 -0
- package/src/editor/hooks/useElementSelection.ts +1573 -0
- package/src/editor/hooks/useFocusManagement.ts +249 -0
- package/src/editor/hooks/useGoogleFonts.ts +305 -0
- package/src/editor/hooks/useIframeInitializer.ts +584 -0
- package/src/editor/hooks/useIframeSetup.ts +360 -0
- package/src/editor/hooks/useImageUpload.ts +270 -0
- package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
- package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
- package/src/editor/hooks/useMarqueeSelection.ts +388 -0
- package/src/editor/hooks/useMediaLibrary.ts +129 -0
- package/src/editor/hooks/usePageSettingsManager.ts +739 -0
- package/src/editor/hooks/useResizablePanel.ts +163 -0
- package/src/editor/hooks/useRichPaste.ts +255 -0
- package/src/editor/hooks/useTouchGestures.ts +197 -0
- package/src/editor/index.ts +27 -0
- package/src/editor/types/page-settings.ts +513 -0
- package/src/editor/types.ts +382 -0
- package/src/editor/utils/align-elements.ts +169 -0
- package/src/editor/utils/api-json.ts +21 -0
- package/src/editor/utils/aspect-lock.ts +50 -0
- package/src/editor/utils/component-renderer.ts +702 -0
- package/src/editor/utils/component-sync.ts +845 -0
- package/src/editor/utils/crop-mode.ts +265 -0
- package/src/editor/utils/dom-utils.ts +2238 -0
- package/src/editor/utils/element-effects.ts +218 -0
- package/src/editor/utils/eyedropper.ts +35 -0
- package/src/editor/utils/figma-export.ts +560 -0
- package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
- package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
- package/src/editor/utils/figma-paste.ts +2262 -0
- package/src/editor/utils/flex-reorder.ts +231 -0
- package/src/editor/utils/flex-utils.ts +1230 -0
- package/src/editor/utils/geometry.ts +232 -0
- package/src/editor/utils/grid-layout.ts +210 -0
- package/src/editor/utils/html-utils.ts +470 -0
- package/src/editor/utils/index.ts +11 -0
- package/src/editor/utils/ink-style.ts +57 -0
- package/src/editor/utils/inline-format.ts +236 -0
- package/src/editor/utils/measure-distance.ts +120 -0
- package/src/editor/utils/override-apply.ts +526 -0
- package/src/editor/utils/override-detection.ts +613 -0
- package/src/editor/utils/paste-processors.ts +609 -0
- package/src/editor/utils/paste-sanitizer.ts +244 -0
- package/src/editor/utils/restore-flow.ts +188 -0
- package/src/editor/utils/shape-library.ts +171 -0
- package/src/editor/utils/slide-root.ts +29 -0
- package/src/editor/utils/smart-guides.ts +218 -0
- package/src/editor/utils/style-utils.ts +685 -0
- package/src/editor/utils/table-edit.ts +151 -0
- package/src/editor/utils/tailwind-mappings.ts +778 -0
- package/src/editor/utils/tailwind-utils.ts +804 -0
- package/src/editor/utils/text-highlight.ts +216 -0
- package/src/editor/utils/theme-to-variables.ts +341 -0
- package/src/editor/utils/variant-resolver.ts +456 -0
- package/src/editor/utils/viewport-utils.ts +295 -0
- package/src/hooks/useEditorHistory.ts +195 -0
- package/src/hooks/useEditorShortcuts.ts +260 -0
- package/src/index.ts +40 -0
- package/src/io.ts +134 -0
- package/src/lib/agent/slide-agent/types.ts +547 -0
- package/src/lib/agent/website-agent/types.ts +805 -0
- package/src/lib/api/auth-fetch.ts +58 -0
- package/src/lib/deck.ts +69 -0
- package/src/lib/export.ts +47 -0
- package/src/lib/firebase/config.ts +29 -0
- package/src/lib/firebase/css-variables.ts +378 -0
- package/src/lib/firebase/editor-components.ts +677 -0
- package/src/lib/firebase/storage.ts +469 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/editor.css +52 -0
- package/src/styles/skin.css +122 -0
- package/src/types/css-variables.ts +645 -0
- package/src/types/editor-components.ts +944 -0
- package/src/types/editor.ts +420 -0
- package/src/types/page-master.ts +197 -0
- package/src/types/page.ts +419 -0
- package/src/types/slide.ts +402 -0
- package/src/types/website-theme.ts +788 -0
- package/src/types/website.ts +374 -0
- package/src/vendor/firebase-firestore.ts +13 -0
- package/src/vendor/firebase-functions.ts +17 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 蛍光ペン(文字の背景色)と文字色を「箱」ではなく**文字**に効かせる。
|
|
3
|
+
*
|
|
4
|
+
* PowerPointの蛍光ペンは段落やプレースホルダー全体ではなく、文字の背後だけを塗る。
|
|
5
|
+
* 要素に background-color を直接置くと <p> や <div> の箱全体が塗られてしまうので、
|
|
6
|
+
* - テキストを範囲選択している → その範囲だけを <span> で包む
|
|
7
|
+
* - 選択が無い(要素選択だけ) → 要素の中の**テキスト部分**を <span> で包む
|
|
8
|
+
* という2段構えにする。生成されるのはただのDOMなので、保存すると書き戻し
|
|
9
|
+
* エンジンがそのまま原本TSXへ運ぶ。
|
|
10
|
+
*
|
|
11
|
+
* 文字色は箱に置いても見た目が変わらない(継承されるだけ)ため、範囲選択が
|
|
12
|
+
* ある時だけ span 化し、それ以外は要素のスタイルに置く。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const HL_ATTR = 'data-gg-hl';
|
|
16
|
+
|
|
17
|
+
const isElement = (n: Node): n is HTMLElement => n.nodeType === 1;
|
|
18
|
+
const isText = (n: Node): n is Text => n.nodeType === 3;
|
|
19
|
+
|
|
20
|
+
/** 蛍光ペンで作った(あるいは同等の)スパンか */
|
|
21
|
+
function isHighlightSpan(el: Element | null): el is HTMLElement {
|
|
22
|
+
if (!el || el.tagName !== 'SPAN') return false;
|
|
23
|
+
const he = el as HTMLElement;
|
|
24
|
+
if (he.hasAttribute(HL_ATTR)) return true;
|
|
25
|
+
// 保存→再読込後は data-gg-* が落ちるので、背景色だけを持つspanも対象にする
|
|
26
|
+
const bg = he.style.backgroundColor;
|
|
27
|
+
return !!bg && bg !== 'transparent' && he.style.length <= 2;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** spanを外して中身だけ残す */
|
|
31
|
+
function unwrap(el: HTMLElement): void {
|
|
32
|
+
const parent = el.parentNode;
|
|
33
|
+
if (!parent) return;
|
|
34
|
+
while (el.firstChild) parent.insertBefore(el.firstChild, el);
|
|
35
|
+
parent.removeChild(el);
|
|
36
|
+
parent.normalize();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 範囲に関わる蛍光スパンを外す(入れ子を作らないため適用前に必ず通す) */
|
|
40
|
+
function clearHighlights(root: HTMLElement | Document, range?: Range): void {
|
|
41
|
+
const scope = 'querySelectorAll' in root ? root : (root as Document);
|
|
42
|
+
const spans = [...scope.querySelectorAll('span')].filter(isHighlightSpan);
|
|
43
|
+
for (const s of spans) {
|
|
44
|
+
if (!range || range.intersectsNode(s)) unwrap(s);
|
|
45
|
+
}
|
|
46
|
+
// 範囲を「包んでいる」スパン(範囲の内側には現れない)も外す
|
|
47
|
+
if (range) {
|
|
48
|
+
let node: Node | null = range.commonAncestorContainer;
|
|
49
|
+
while (node && isText(node)) node = node.parentNode;
|
|
50
|
+
let cur = node as Element | null;
|
|
51
|
+
while (cur) {
|
|
52
|
+
if (isHighlightSpan(cur)) {
|
|
53
|
+
unwrap(cur);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
cur = cur.parentElement;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** テキスト編集中/アートボード内の有効な範囲選択を返す */
|
|
62
|
+
function activeRange(doc: Document): Range | null {
|
|
63
|
+
const sel = doc.getSelection();
|
|
64
|
+
if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return null;
|
|
65
|
+
const range = sel.getRangeAt(0);
|
|
66
|
+
if (!range.toString().trim()) return null;
|
|
67
|
+
const artboard = doc.getElementById('artboard');
|
|
68
|
+
if (!artboard || !artboard.contains(range.commonAncestorContainer)) return null;
|
|
69
|
+
return range;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function makeSpan(doc: Document, color: string): HTMLElement {
|
|
73
|
+
const span = doc.createElement('span');
|
|
74
|
+
span.setAttribute(HL_ATTR, '1');
|
|
75
|
+
span.style.backgroundColor = color;
|
|
76
|
+
return span;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 直前に蛍光ペンを当てたスパン。
|
|
81
|
+
* 適用後は選択を解除する(青い選択帯が蛍光色の上に残るのを避ける)ので、
|
|
82
|
+
* 「続けて別の色を選ぶ」操作のために対象を覚えておく。
|
|
83
|
+
*/
|
|
84
|
+
let lastSpan: HTMLElement | null = null;
|
|
85
|
+
|
|
86
|
+
/** 範囲をspanで包む(要素境界をまたぐ選択にも耐える) */
|
|
87
|
+
function wrapRange(doc: Document, range: Range, color: string): void {
|
|
88
|
+
const span = makeSpan(doc, color);
|
|
89
|
+
try {
|
|
90
|
+
range.surroundContents(span);
|
|
91
|
+
} catch {
|
|
92
|
+
span.appendChild(range.extractContents());
|
|
93
|
+
range.insertNode(span);
|
|
94
|
+
}
|
|
95
|
+
lastSpan = span;
|
|
96
|
+
// 選択は解除する。残すとリボンへフォーカスが移った後も選択帯が描かれ、
|
|
97
|
+
// 蛍光ペンの色が見えなくなる
|
|
98
|
+
doc.getSelection()?.removeAllRanges();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 要素の中の「文字が並んでいる部分」だけを span で包む。
|
|
103
|
+
* テキストノードとインライン要素の連続を1つの run とみなすので、
|
|
104
|
+
* 見出しの中の <span>強調</span> のようなインライン構造も一続きで塗れる。
|
|
105
|
+
*/
|
|
106
|
+
function wrapTextRuns(doc: Document, el: HTMLElement, color: string): boolean {
|
|
107
|
+
const win = doc.defaultView;
|
|
108
|
+
if (!win) return false;
|
|
109
|
+
|
|
110
|
+
// 直接テキストを持つ要素を集める(自分自身も含む)
|
|
111
|
+
const hosts: HTMLElement[] = [];
|
|
112
|
+
const visit = (node: HTMLElement) => {
|
|
113
|
+
if ([...node.childNodes].some((n) => isText(n) && n.textContent?.trim())) hosts.push(node);
|
|
114
|
+
for (const child of [...node.children]) visit(child as HTMLElement);
|
|
115
|
+
};
|
|
116
|
+
visit(el);
|
|
117
|
+
if (!hosts.length) return false;
|
|
118
|
+
|
|
119
|
+
let changed = false;
|
|
120
|
+
for (const host of hosts) {
|
|
121
|
+
// 連続する「テキスト or インライン要素」を run にまとめる
|
|
122
|
+
const runs: Node[][] = [];
|
|
123
|
+
let cur: Node[] = [];
|
|
124
|
+
for (const n of [...host.childNodes]) {
|
|
125
|
+
const inline =
|
|
126
|
+
isText(n) || (isElement(n) && win.getComputedStyle(n).display.startsWith('inline'));
|
|
127
|
+
if (inline) cur.push(n);
|
|
128
|
+
else if (cur.length) {
|
|
129
|
+
runs.push(cur);
|
|
130
|
+
cur = [];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (cur.length) runs.push(cur);
|
|
134
|
+
|
|
135
|
+
for (const run of runs) {
|
|
136
|
+
if (!run.some((n) => n.textContent?.trim())) continue;
|
|
137
|
+
// すでに蛍光スパンだけの run なら色を差し替える
|
|
138
|
+
if (run.length === 1 && isElement(run[0]) && isHighlightSpan(run[0])) {
|
|
139
|
+
run[0].style.backgroundColor = color;
|
|
140
|
+
changed = true;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const span = makeSpan(doc, color);
|
|
144
|
+
host.insertBefore(span, run[0]);
|
|
145
|
+
for (const n of run) span.appendChild(n);
|
|
146
|
+
changed = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return changed;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 蛍光ペンを適用する。color=null で解除。
|
|
154
|
+
* @returns DOMを変更したか
|
|
155
|
+
*/
|
|
156
|
+
export function applyTextHighlight(
|
|
157
|
+
doc: Document,
|
|
158
|
+
els: HTMLElement[],
|
|
159
|
+
color: string | null,
|
|
160
|
+
): boolean {
|
|
161
|
+
const range = activeRange(doc);
|
|
162
|
+
|
|
163
|
+
if (range) {
|
|
164
|
+
clearHighlights(doc, range);
|
|
165
|
+
if (color) wrapRange(doc, range, color);
|
|
166
|
+
else lastSpan = null;
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 直前に塗った範囲がまだ生きていれば、そこを塗り替える/消す
|
|
171
|
+
if (lastSpan?.isConnected && (!els.length || els.some((el) => el.contains(lastSpan!)))) {
|
|
172
|
+
if (color) lastSpan.style.backgroundColor = color;
|
|
173
|
+
else {
|
|
174
|
+
unwrap(lastSpan);
|
|
175
|
+
lastSpan = null;
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!els.length) return false;
|
|
181
|
+
let changed = false;
|
|
182
|
+
for (const el of els) {
|
|
183
|
+
// 要素自身に付いていた箱の塗りは外す(旧仕様で塗られていた分の後始末)
|
|
184
|
+
if (el.style.backgroundColor) {
|
|
185
|
+
el.style.backgroundColor = '';
|
|
186
|
+
changed = true;
|
|
187
|
+
}
|
|
188
|
+
clearHighlights(el);
|
|
189
|
+
lastSpan = null;
|
|
190
|
+
if (color) changed = wrapTextRuns(doc, el, color) || changed;
|
|
191
|
+
else changed = true;
|
|
192
|
+
}
|
|
193
|
+
return changed;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 文字色。範囲選択があればその範囲だけ、無ければ要素のスタイルに置く。
|
|
198
|
+
*/
|
|
199
|
+
export function applyTextColor(doc: Document, els: HTMLElement[], color: string): boolean {
|
|
200
|
+
const range = activeRange(doc);
|
|
201
|
+
if (range) {
|
|
202
|
+
const span = doc.createElement('span');
|
|
203
|
+
span.style.color = color;
|
|
204
|
+
try {
|
|
205
|
+
range.surroundContents(span);
|
|
206
|
+
} catch {
|
|
207
|
+
span.appendChild(range.extractContents());
|
|
208
|
+
range.insertNode(span);
|
|
209
|
+
}
|
|
210
|
+
doc.getSelection()?.removeAllRanges();
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
if (!els.length) return false;
|
|
214
|
+
for (const el of els) el.style.color = color;
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebsiteTheme → CSS変数への変換ユーティリティ
|
|
3
|
+
* テーマ設定からエディタ用のCSS変数を生成
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { WebsiteTheme, WebsiteThemeColors, WebsiteTypography } from '../../types/website-theme';
|
|
7
|
+
import type { CSSVariableDefinition, CSSVariableCategory } from '../../types/css-variables';
|
|
8
|
+
import { generateVariableId } from '../../types/css-variables';
|
|
9
|
+
|
|
10
|
+
// ============================================
|
|
11
|
+
// 変数生成ヘルパー
|
|
12
|
+
// ============================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 単一のCSS変数定義を生成
|
|
16
|
+
*/
|
|
17
|
+
function createVariable(
|
|
18
|
+
name: string,
|
|
19
|
+
cssName: string,
|
|
20
|
+
value: string,
|
|
21
|
+
category: CSSVariableCategory,
|
|
22
|
+
description?: string
|
|
23
|
+
): CSSVariableDefinition {
|
|
24
|
+
const now = new Date();
|
|
25
|
+
return {
|
|
26
|
+
id: generateVariableId(),
|
|
27
|
+
name,
|
|
28
|
+
cssName,
|
|
29
|
+
value,
|
|
30
|
+
category,
|
|
31
|
+
description,
|
|
32
|
+
createdAt: now,
|
|
33
|
+
updatedAt: now,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================
|
|
38
|
+
// カラー変数の変換
|
|
39
|
+
// ============================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* テーマカラーからCSS変数を生成
|
|
43
|
+
*/
|
|
44
|
+
export function colorsToVariables(colors: WebsiteThemeColors): CSSVariableDefinition[] {
|
|
45
|
+
const variables: CSSVariableDefinition[] = [];
|
|
46
|
+
|
|
47
|
+
// メインカラー
|
|
48
|
+
variables.push(createVariable('background', '--color-background', colors.background, 'color', '背景色'));
|
|
49
|
+
variables.push(createVariable('text', '--color-text', colors.text, 'color', 'テキスト色'));
|
|
50
|
+
variables.push(createVariable('primary', '--color-primary', colors.primary, 'color', 'プライマリカラー'));
|
|
51
|
+
|
|
52
|
+
// オプショナルカラー
|
|
53
|
+
if (colors.textMuted) {
|
|
54
|
+
variables.push(createVariable('text-muted', '--color-text-muted', colors.textMuted, 'color', '薄いテキスト色'));
|
|
55
|
+
}
|
|
56
|
+
if (colors.secondary) {
|
|
57
|
+
variables.push(createVariable('secondary', '--color-secondary', colors.secondary, 'color', 'セカンダリカラー'));
|
|
58
|
+
}
|
|
59
|
+
if (colors.accent) {
|
|
60
|
+
variables.push(createVariable('accent', '--color-accent', colors.accent, 'color', 'アクセントカラー'));
|
|
61
|
+
}
|
|
62
|
+
if (colors.surface) {
|
|
63
|
+
variables.push(createVariable('surface', '--color-surface', colors.surface, 'color', 'サーフェス色'));
|
|
64
|
+
}
|
|
65
|
+
if (colors.surfaceAlt) {
|
|
66
|
+
variables.push(createVariable('surface-alt', '--color-surface-alt', colors.surfaceAlt, 'color', '代替サーフェス色'));
|
|
67
|
+
}
|
|
68
|
+
if (colors.border) {
|
|
69
|
+
variables.push(createVariable('border', '--color-border', colors.border, 'color', 'ボーダー色'));
|
|
70
|
+
}
|
|
71
|
+
if (colors.borderStrong) {
|
|
72
|
+
variables.push(createVariable('border-strong', '--color-border-strong', colors.borderStrong, 'color', '強調ボーダー色'));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ステータスカラー
|
|
76
|
+
if (colors.success) {
|
|
77
|
+
variables.push(createVariable('success', '--color-success', colors.success, 'color', '成功色'));
|
|
78
|
+
}
|
|
79
|
+
if (colors.warning) {
|
|
80
|
+
variables.push(createVariable('warning', '--color-warning', colors.warning, 'color', '警告色'));
|
|
81
|
+
}
|
|
82
|
+
if (colors.error) {
|
|
83
|
+
variables.push(createVariable('error', '--color-error', colors.error, 'color', 'エラー色'));
|
|
84
|
+
}
|
|
85
|
+
if (colors.info) {
|
|
86
|
+
variables.push(createVariable('info', '--color-info', colors.info, 'color', '情報色'));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return variables;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================
|
|
93
|
+
// タイポグラフィ変数の変換
|
|
94
|
+
// ============================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* タイポグラフィ設定からCSS変数を生成
|
|
98
|
+
*/
|
|
99
|
+
export function typographyToVariables(typography: WebsiteTypography): CSSVariableDefinition[] {
|
|
100
|
+
const variables: CSSVariableDefinition[] = [];
|
|
101
|
+
|
|
102
|
+
// フォントファミリー
|
|
103
|
+
variables.push(createVariable('family', '--font-family', typography.fontFamily, 'typography', '本文フォント'));
|
|
104
|
+
|
|
105
|
+
if (typography.headingFontFamily) {
|
|
106
|
+
variables.push(createVariable('family-heading', '--font-family-heading', typography.headingFontFamily, 'typography', '見出しフォント'));
|
|
107
|
+
}
|
|
108
|
+
if (typography.monoFontFamily) {
|
|
109
|
+
variables.push(createVariable('family-mono', '--font-family-mono', typography.monoFontFamily, 'typography', '等幅フォント'));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 基準フォントサイズ
|
|
113
|
+
if (typography.baseFontSize) {
|
|
114
|
+
variables.push(createVariable('size-base', '--font-size-base', typography.baseFontSize, 'typography', '基準フォントサイズ'));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 見出しスタイル
|
|
118
|
+
if (typography.headings) {
|
|
119
|
+
const headings = typography.headings;
|
|
120
|
+
|
|
121
|
+
// H1
|
|
122
|
+
if (headings.h1) {
|
|
123
|
+
variables.push(createVariable('size-h1', '--font-size-h1', headings.h1.fontSize, 'typography', 'H1フォントサイズ'));
|
|
124
|
+
}
|
|
125
|
+
// H2
|
|
126
|
+
if (headings.h2) {
|
|
127
|
+
variables.push(createVariable('size-h2', '--font-size-h2', headings.h2.fontSize, 'typography', 'H2フォントサイズ'));
|
|
128
|
+
}
|
|
129
|
+
// H3
|
|
130
|
+
if (headings.h3) {
|
|
131
|
+
variables.push(createVariable('size-h3', '--font-size-h3', headings.h3.fontSize, 'typography', 'H3フォントサイズ'));
|
|
132
|
+
}
|
|
133
|
+
// H4
|
|
134
|
+
if (headings.h4) {
|
|
135
|
+
variables.push(createVariable('size-h4', '--font-size-h4', headings.h4.fontSize, 'typography', 'H4フォントサイズ'));
|
|
136
|
+
}
|
|
137
|
+
// H5
|
|
138
|
+
if (headings.h5) {
|
|
139
|
+
variables.push(createVariable('size-h5', '--font-size-h5', headings.h5.fontSize, 'typography', 'H5フォントサイズ'));
|
|
140
|
+
}
|
|
141
|
+
// H6
|
|
142
|
+
if (headings.h6) {
|
|
143
|
+
variables.push(createVariable('size-h6', '--font-size-h6', headings.h6.fontSize, 'typography', 'H6フォントサイズ'));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 本文スタイル
|
|
148
|
+
if (typography.body) {
|
|
149
|
+
const body = typography.body;
|
|
150
|
+
|
|
151
|
+
if (body.base) {
|
|
152
|
+
variables.push(createVariable('size-body', '--font-size-body', body.base.fontSize, 'typography', '本文フォントサイズ'));
|
|
153
|
+
variables.push(createVariable('line-height-body', '--line-height-body', body.base.lineHeight, 'typography', '本文行間'));
|
|
154
|
+
}
|
|
155
|
+
if (body.large) {
|
|
156
|
+
variables.push(createVariable('size-body-lg', '--font-size-body-lg', body.large.fontSize, 'typography', '大本文フォントサイズ'));
|
|
157
|
+
}
|
|
158
|
+
if (body.small) {
|
|
159
|
+
variables.push(createVariable('size-body-sm', '--font-size-body-sm', body.small.fontSize, 'typography', '小本文フォントサイズ'));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return variables;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ============================================
|
|
167
|
+
// スペーシング変数の生成
|
|
168
|
+
// ============================================
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* スタイルオプションからスペーシング変数を生成
|
|
172
|
+
*/
|
|
173
|
+
export function generateSpacingVariables(spacing: 'compact' | 'normal' | 'relaxed' | 'spacious' = 'normal'): CSSVariableDefinition[] {
|
|
174
|
+
const multipliers: Record<string, number> = {
|
|
175
|
+
compact: 0.75,
|
|
176
|
+
normal: 1,
|
|
177
|
+
relaxed: 1.25,
|
|
178
|
+
spacious: 1.5,
|
|
179
|
+
};
|
|
180
|
+
const multiplier = multipliers[spacing] ?? 1;
|
|
181
|
+
|
|
182
|
+
const baseValues = [4, 8, 16, 24, 32, 48, 64];
|
|
183
|
+
const names = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl'];
|
|
184
|
+
|
|
185
|
+
return baseValues.map((value, index) => {
|
|
186
|
+
const adjustedValue = Math.round(value * multiplier);
|
|
187
|
+
return createVariable(
|
|
188
|
+
names[index],
|
|
189
|
+
`--spacing-${names[index]}`,
|
|
190
|
+
`${adjustedValue}px`,
|
|
191
|
+
'spacing',
|
|
192
|
+
`${names[index].toUpperCase()}スペース`
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ============================================
|
|
198
|
+
// ボーダー変数の生成
|
|
199
|
+
// ============================================
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* ボーダースタイルからCSS変数を生成
|
|
203
|
+
*/
|
|
204
|
+
export function generateBorderVariables(borderRadius: 'none' | 'small' | 'medium' | 'large' | 'full' = 'medium'): CSSVariableDefinition[] {
|
|
205
|
+
const radiusMap: Record<string, { sm: string; md: string; lg: string; full: string }> = {
|
|
206
|
+
none: { sm: '0', md: '0', lg: '0', full: '0' },
|
|
207
|
+
small: { sm: '2px', md: '4px', lg: '6px', full: '9999px' },
|
|
208
|
+
medium: { sm: '4px', md: '8px', lg: '12px', full: '9999px' },
|
|
209
|
+
large: { sm: '8px', md: '16px', lg: '24px', full: '9999px' },
|
|
210
|
+
full: { sm: '9999px', md: '9999px', lg: '9999px', full: '9999px' },
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const values = radiusMap[borderRadius] ?? radiusMap.medium;
|
|
214
|
+
|
|
215
|
+
return [
|
|
216
|
+
createVariable('radius-sm', '--border-radius-sm', values.sm, 'border', '小角丸'),
|
|
217
|
+
createVariable('radius-md', '--border-radius-md', values.md, 'border', '中角丸'),
|
|
218
|
+
createVariable('radius-lg', '--border-radius-lg', values.lg, 'border', '大角丸'),
|
|
219
|
+
createVariable('radius-full', '--border-radius-full', values.full, 'border', '完全丸'),
|
|
220
|
+
];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ============================================
|
|
224
|
+
// シャドウ変数の生成
|
|
225
|
+
// ============================================
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* シャドウスタイルからCSS変数を生成
|
|
229
|
+
*/
|
|
230
|
+
export function generateShadowVariables(shadow: 'none' | 'subtle' | 'medium' | 'strong' = 'medium'): CSSVariableDefinition[] {
|
|
231
|
+
const shadowMap: Record<string, { sm: string; md: string; lg: string }> = {
|
|
232
|
+
none: {
|
|
233
|
+
sm: 'none',
|
|
234
|
+
md: 'none',
|
|
235
|
+
lg: 'none',
|
|
236
|
+
},
|
|
237
|
+
subtle: {
|
|
238
|
+
sm: '0 1px 2px rgba(0,0,0,0.03)',
|
|
239
|
+
md: '0 2px 4px rgba(0,0,0,0.05)',
|
|
240
|
+
lg: '0 4px 6px rgba(0,0,0,0.07)',
|
|
241
|
+
},
|
|
242
|
+
medium: {
|
|
243
|
+
sm: '0 1px 2px rgba(0,0,0,0.05)',
|
|
244
|
+
md: '0 4px 6px rgba(0,0,0,0.1)',
|
|
245
|
+
lg: '0 10px 15px rgba(0,0,0,0.15)',
|
|
246
|
+
},
|
|
247
|
+
strong: {
|
|
248
|
+
sm: '0 2px 4px rgba(0,0,0,0.1)',
|
|
249
|
+
md: '0 8px 12px rgba(0,0,0,0.15)',
|
|
250
|
+
lg: '0 15px 25px rgba(0,0,0,0.2)',
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const values = shadowMap[shadow] ?? shadowMap.medium;
|
|
255
|
+
|
|
256
|
+
return [
|
|
257
|
+
createVariable('sm', '--shadow-sm', values.sm, 'shadow', '小シャドウ'),
|
|
258
|
+
createVariable('md', '--shadow-md', values.md, 'shadow', '中シャドウ'),
|
|
259
|
+
createVariable('lg', '--shadow-lg', values.lg, 'shadow', '大シャドウ'),
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ============================================
|
|
264
|
+
// テーマ全体の変換
|
|
265
|
+
// ============================================
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* WebsiteTheme全体からCSS変数を生成
|
|
269
|
+
*/
|
|
270
|
+
export function themeToVariables(theme: WebsiteTheme): CSSVariableDefinition[] {
|
|
271
|
+
const variables: CSSVariableDefinition[] = [];
|
|
272
|
+
|
|
273
|
+
// カラー
|
|
274
|
+
variables.push(...colorsToVariables(theme.colors));
|
|
275
|
+
|
|
276
|
+
// タイポグラフィ
|
|
277
|
+
if (theme.typography) {
|
|
278
|
+
variables.push(...typographyToVariables(theme.typography));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// スペーシング
|
|
282
|
+
if (theme.styles?.spacing) {
|
|
283
|
+
variables.push(...generateSpacingVariables(theme.styles.spacing));
|
|
284
|
+
} else {
|
|
285
|
+
variables.push(...generateSpacingVariables('normal'));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ボーダー
|
|
289
|
+
if (theme.styles?.borderRadius) {
|
|
290
|
+
variables.push(...generateBorderVariables(theme.styles.borderRadius));
|
|
291
|
+
} else {
|
|
292
|
+
variables.push(...generateBorderVariables('medium'));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// シャドウ
|
|
296
|
+
if (theme.styles?.shadow) {
|
|
297
|
+
variables.push(...generateShadowVariables(theme.styles.shadow));
|
|
298
|
+
} else {
|
|
299
|
+
variables.push(...generateShadowVariables('medium'));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return variables;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ============================================
|
|
306
|
+
// 差分検出
|
|
307
|
+
// ============================================
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 現在の変数とテーマから生成した変数の差分を検出
|
|
311
|
+
*/
|
|
312
|
+
export function detectVariableDifferences(
|
|
313
|
+
current: CSSVariableDefinition[],
|
|
314
|
+
fromTheme: CSSVariableDefinition[]
|
|
315
|
+
): {
|
|
316
|
+
added: CSSVariableDefinition[];
|
|
317
|
+
updated: CSSVariableDefinition[];
|
|
318
|
+
unchanged: CSSVariableDefinition[];
|
|
319
|
+
} {
|
|
320
|
+
const currentMap = new Map(current.map(v => [v.cssName, v]));
|
|
321
|
+
const added: CSSVariableDefinition[] = [];
|
|
322
|
+
const updated: CSSVariableDefinition[] = [];
|
|
323
|
+
const unchanged: CSSVariableDefinition[] = [];
|
|
324
|
+
|
|
325
|
+
for (const themeVar of fromTheme) {
|
|
326
|
+
const existing = currentMap.get(themeVar.cssName);
|
|
327
|
+
if (!existing) {
|
|
328
|
+
added.push(themeVar);
|
|
329
|
+
} else if (existing.value !== themeVar.value) {
|
|
330
|
+
updated.push({
|
|
331
|
+
...existing,
|
|
332
|
+
value: themeVar.value,
|
|
333
|
+
updatedAt: new Date(),
|
|
334
|
+
});
|
|
335
|
+
} else {
|
|
336
|
+
unchanged.push(existing);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { added, updated, unchanged };
|
|
341
|
+
}
|