@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,311 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* プロパティパネル用のCSS変数ピッカー
|
|
5
|
+
* Figmaスタイルでプロパティ値にCSS変数をリンク
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useMemo, useCallback } from 'react';
|
|
9
|
+
import {
|
|
10
|
+
Popover,
|
|
11
|
+
PopoverContent,
|
|
12
|
+
PopoverTrigger,
|
|
13
|
+
} from '../../../components/ui/popover';
|
|
14
|
+
import { Button } from '../../../components/ui/button';
|
|
15
|
+
import { Input } from '../../../components/ui/input';
|
|
16
|
+
import { ScrollArea } from '../../../components/ui/scroll-area';
|
|
17
|
+
import {
|
|
18
|
+
Settings2,
|
|
19
|
+
Search,
|
|
20
|
+
X,
|
|
21
|
+
Palette,
|
|
22
|
+
Ruler,
|
|
23
|
+
Type,
|
|
24
|
+
Square,
|
|
25
|
+
Layers,
|
|
26
|
+
Settings,
|
|
27
|
+
type LucideIcon,
|
|
28
|
+
} from 'lucide-react';
|
|
29
|
+
import { cn } from '../../../lib/utils';
|
|
30
|
+
import { useEditorVariables } from '../../EditorContext';
|
|
31
|
+
import type { CSSVariableDefinition, CSSVariableCategory } from '../../../types/css-variables';
|
|
32
|
+
import { isVariableReference, extractVariableName } from '../../../types/css-variables';
|
|
33
|
+
|
|
34
|
+
// ============================================
|
|
35
|
+
// アイコンマッピング
|
|
36
|
+
// ============================================
|
|
37
|
+
|
|
38
|
+
const CATEGORY_ICONS: Record<string, LucideIcon> = {
|
|
39
|
+
color: Palette,
|
|
40
|
+
spacing: Ruler,
|
|
41
|
+
typography: Type,
|
|
42
|
+
border: Square,
|
|
43
|
+
shadow: Layers,
|
|
44
|
+
other: Settings,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ============================================
|
|
48
|
+
// 型定義
|
|
49
|
+
// ============================================
|
|
50
|
+
|
|
51
|
+
export interface PropertyVariablePickerProps {
|
|
52
|
+
/** フィルタするカテゴリ(nullで全て表示) */
|
|
53
|
+
category?: CSSVariableCategory | null;
|
|
54
|
+
/** 現在の値(var(--xxx)形式の場合、リンク済みと判定) */
|
|
55
|
+
currentValue?: string;
|
|
56
|
+
/** 変数選択時のコールバック */
|
|
57
|
+
onSelectVariable: (variable: CSSVariableDefinition) => void;
|
|
58
|
+
/** リンク解除時のコールバック(解除後の値を渡す) */
|
|
59
|
+
onDetach?: (resolvedValue: string) => void;
|
|
60
|
+
/** ボタンのクラス */
|
|
61
|
+
className?: string;
|
|
62
|
+
/** 無効状態 */
|
|
63
|
+
disabled?: boolean;
|
|
64
|
+
/** サイズ */
|
|
65
|
+
size?: 'sm' | 'default';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================
|
|
69
|
+
// メインコンポーネント
|
|
70
|
+
// ============================================
|
|
71
|
+
|
|
72
|
+
export function PropertyVariablePicker({
|
|
73
|
+
category,
|
|
74
|
+
currentValue,
|
|
75
|
+
onSelectVariable,
|
|
76
|
+
onDetach,
|
|
77
|
+
className,
|
|
78
|
+
disabled = false,
|
|
79
|
+
size = 'sm',
|
|
80
|
+
}: PropertyVariablePickerProps) {
|
|
81
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
82
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
83
|
+
|
|
84
|
+
// CSS変数を取得
|
|
85
|
+
const { variables } = useEditorVariables();
|
|
86
|
+
|
|
87
|
+
// 現在の値がCSS変数参照かどうか
|
|
88
|
+
const isLinked = useMemo(() => {
|
|
89
|
+
return currentValue ? isVariableReference(currentValue) : false;
|
|
90
|
+
}, [currentValue]);
|
|
91
|
+
|
|
92
|
+
// リンクされている変数を取得
|
|
93
|
+
const linkedVariable = useMemo(() => {
|
|
94
|
+
if (!isLinked || !currentValue) return null;
|
|
95
|
+
const varName = extractVariableName(currentValue);
|
|
96
|
+
return variables.find(v => v.cssName === varName);
|
|
97
|
+
}, [isLinked, currentValue, variables]);
|
|
98
|
+
|
|
99
|
+
// フィルタリングされた変数
|
|
100
|
+
const filteredVariables = useMemo(() => {
|
|
101
|
+
let filtered = variables;
|
|
102
|
+
|
|
103
|
+
// カテゴリでフィルタ
|
|
104
|
+
if (category) {
|
|
105
|
+
filtered = filtered.filter(v => v.category === category);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 検索でフィルタ
|
|
109
|
+
if (searchQuery) {
|
|
110
|
+
const query = searchQuery.toLowerCase();
|
|
111
|
+
filtered = filtered.filter(v =>
|
|
112
|
+
v.name.toLowerCase().includes(query) ||
|
|
113
|
+
v.cssName.toLowerCase().includes(query)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return filtered;
|
|
118
|
+
}, [variables, category, searchQuery]);
|
|
119
|
+
|
|
120
|
+
// カテゴリごとにグループ化
|
|
121
|
+
const groupedVariables = useMemo(() => {
|
|
122
|
+
const groups: Record<CSSVariableCategory, CSSVariableDefinition[]> = {
|
|
123
|
+
color: [],
|
|
124
|
+
spacing: [],
|
|
125
|
+
typography: [],
|
|
126
|
+
border: [],
|
|
127
|
+
shadow: [],
|
|
128
|
+
other: [],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
filteredVariables.forEach(v => {
|
|
132
|
+
groups[v.category].push(v);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return groups;
|
|
136
|
+
}, [filteredVariables]);
|
|
137
|
+
|
|
138
|
+
// 変数を選択
|
|
139
|
+
const handleSelect = useCallback((variable: CSSVariableDefinition) => {
|
|
140
|
+
onSelectVariable(variable);
|
|
141
|
+
setIsOpen(false);
|
|
142
|
+
setSearchQuery('');
|
|
143
|
+
}, [onSelectVariable]);
|
|
144
|
+
|
|
145
|
+
// リンク解除
|
|
146
|
+
const handleDetach = useCallback(() => {
|
|
147
|
+
if (onDetach) {
|
|
148
|
+
if (linkedVariable) {
|
|
149
|
+
onDetach(linkedVariable.value);
|
|
150
|
+
} else {
|
|
151
|
+
// 変数が見つからない場合はデフォルト値を使用
|
|
152
|
+
// カテゴリに応じたデフォルト値を設定
|
|
153
|
+
const defaultValue = category === 'color' ? '#000000' : '0';
|
|
154
|
+
onDetach(defaultValue);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
setIsOpen(false);
|
|
158
|
+
}, [linkedVariable, onDetach, category]);
|
|
159
|
+
|
|
160
|
+
const buttonSize = size === 'sm' ? 'h-6 w-6' : 'h-7 w-7';
|
|
161
|
+
const iconSize = size === 'sm' ? 'h-3 w-3' : 'h-3.5 w-3.5';
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
|
165
|
+
<PopoverTrigger asChild>
|
|
166
|
+
<Button
|
|
167
|
+
variant="ghost"
|
|
168
|
+
size="icon"
|
|
169
|
+
disabled={disabled}
|
|
170
|
+
className={cn(
|
|
171
|
+
buttonSize,
|
|
172
|
+
'flex-shrink-0',
|
|
173
|
+
isLinked
|
|
174
|
+
? 'text-purple-400 hover:text-purple-300 bg-purple-500/10 hover:bg-purple-500/20'
|
|
175
|
+
: 'text-gray-500 hover:text-gray-300',
|
|
176
|
+
className
|
|
177
|
+
)}
|
|
178
|
+
title={isLinked ? `変数: ${linkedVariable?.name}` : 'CSS変数を使用'}
|
|
179
|
+
>
|
|
180
|
+
<Settings2 className={iconSize} />
|
|
181
|
+
</Button>
|
|
182
|
+
</PopoverTrigger>
|
|
183
|
+
|
|
184
|
+
<PopoverContent
|
|
185
|
+
className="w-64 p-0 bg-[#2c2c2c] border-[#444444]"
|
|
186
|
+
align="end"
|
|
187
|
+
side="left"
|
|
188
|
+
>
|
|
189
|
+
{/* ヘッダー */}
|
|
190
|
+
<div className="flex items-center gap-2 p-2 border-b border-[#444444]">
|
|
191
|
+
<div className="relative flex-1">
|
|
192
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-500" />
|
|
193
|
+
<Input
|
|
194
|
+
type="text"
|
|
195
|
+
placeholder="変数を検索..."
|
|
196
|
+
value={searchQuery}
|
|
197
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
198
|
+
className="h-7 pl-7 text-xs bg-[#383838] border-[#444444] text-white placeholder:text-gray-500"
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
{isLinked && onDetach && (
|
|
202
|
+
<Button
|
|
203
|
+
variant="ghost"
|
|
204
|
+
size="icon"
|
|
205
|
+
className="h-7 w-7 text-gray-400 hover:text-white"
|
|
206
|
+
onClick={(e) => {
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
e.stopPropagation();
|
|
209
|
+
handleDetach();
|
|
210
|
+
}}
|
|
211
|
+
onMouseDown={(e) => {
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
e.stopPropagation();
|
|
214
|
+
}}
|
|
215
|
+
onPointerDown={(e) => {
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
e.stopPropagation();
|
|
218
|
+
}}
|
|
219
|
+
title="変数を解除"
|
|
220
|
+
>
|
|
221
|
+
<X className="h-3.5 w-3.5" />
|
|
222
|
+
</Button>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
{/* リンク済みの場合の情報表示 */}
|
|
227
|
+
{isLinked && linkedVariable && (
|
|
228
|
+
<div className="flex items-center gap-2 px-3 py-2 bg-purple-500/10 border-b border-[#444444]">
|
|
229
|
+
{linkedVariable.category === 'color' && (
|
|
230
|
+
<div
|
|
231
|
+
className="w-4 h-4 rounded border border-[#444444]"
|
|
232
|
+
style={{ backgroundColor: linkedVariable.value }}
|
|
233
|
+
/>
|
|
234
|
+
)}
|
|
235
|
+
<div className="flex-1 min-w-0">
|
|
236
|
+
<div className="text-xs text-white truncate">{linkedVariable.name}</div>
|
|
237
|
+
<div className="text-[10px] text-gray-500 font-mono truncate">{linkedVariable.value}</div>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
{/* 変数リスト */}
|
|
243
|
+
<ScrollArea className="max-h-64">
|
|
244
|
+
{filteredVariables.length === 0 ? (
|
|
245
|
+
<div className="p-4 text-center text-xs text-gray-500">
|
|
246
|
+
{searchQuery ? '検索結果がありません' : 'CSS変数がありません'}
|
|
247
|
+
</div>
|
|
248
|
+
) : (
|
|
249
|
+
<div className="p-1">
|
|
250
|
+
{(category ? [category] : (['color', 'spacing', 'typography', 'border', 'shadow', 'other'] as CSSVariableCategory[])).map((cat) => {
|
|
251
|
+
const catVariables = groupedVariables[cat];
|
|
252
|
+
if (catVariables.length === 0) return null;
|
|
253
|
+
|
|
254
|
+
const Icon = CATEGORY_ICONS[cat];
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<div key={cat} className="mb-1">
|
|
258
|
+
{/* カテゴリヘッダー(カテゴリ指定がない場合のみ表示) */}
|
|
259
|
+
{!category && (
|
|
260
|
+
<div className="flex items-center gap-1.5 px-2 py-1 text-[10px] text-gray-500 uppercase">
|
|
261
|
+
<Icon className="w-3 h-3" />
|
|
262
|
+
<span>{cat}</span>
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
{/* 変数リスト */}
|
|
267
|
+
{catVariables.map((variable) => {
|
|
268
|
+
const isSelected = linkedVariable?.id === variable.id;
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<button
|
|
272
|
+
key={variable.id}
|
|
273
|
+
onClick={() => handleSelect(variable)}
|
|
274
|
+
className={cn(
|
|
275
|
+
'w-full flex items-center gap-2 px-2 py-1.5 rounded text-left',
|
|
276
|
+
'hover:bg-[#444444] transition-colors',
|
|
277
|
+
isSelected && 'bg-purple-500/20'
|
|
278
|
+
)}
|
|
279
|
+
>
|
|
280
|
+
{/* カラープレビュー */}
|
|
281
|
+
{variable.category === 'color' && (
|
|
282
|
+
<div
|
|
283
|
+
className="w-4 h-4 rounded border border-[#444444] flex-shrink-0"
|
|
284
|
+
style={{ backgroundColor: variable.value }}
|
|
285
|
+
/>
|
|
286
|
+
)}
|
|
287
|
+
|
|
288
|
+
{/* 変数情報 */}
|
|
289
|
+
<div className="flex-1 min-w-0">
|
|
290
|
+
<div className="text-xs text-white truncate">{variable.name}</div>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
{/* 値 */}
|
|
294
|
+
<div className="text-[10px] text-gray-500 font-mono truncate max-w-[80px]">
|
|
295
|
+
{variable.value}
|
|
296
|
+
</div>
|
|
297
|
+
</button>
|
|
298
|
+
);
|
|
299
|
+
})}
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
})}
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
</ScrollArea>
|
|
306
|
+
</PopoverContent>
|
|
307
|
+
</Popover>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export default PropertyVariablePicker;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { Input } from '../../../components/ui/input';
|
|
5
|
+
import { Label } from '../../../components/ui/label';
|
|
6
|
+
import {
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue,
|
|
12
|
+
} from '../../../components/ui/select';
|
|
13
|
+
import { MousePointer2, Scaling, Check } from 'lucide-react';
|
|
14
|
+
import { useEditorContext } from '../../EditorContext';
|
|
15
|
+
import { useElementActions } from '../../hooks';
|
|
16
|
+
import { buildTransformString } from '../../utils/style-utils';
|
|
17
|
+
|
|
18
|
+
export function ScalePanel() {
|
|
19
|
+
const { selectedElement, setActiveTool } = useEditorContext();
|
|
20
|
+
const { updateElementStyle } = useElementActions();
|
|
21
|
+
|
|
22
|
+
// 現在のスケール値を取得(デフォルト1)
|
|
23
|
+
const currentScale = useMemo(() => {
|
|
24
|
+
if (!selectedElement) return { x: 1, y: 1 };
|
|
25
|
+
return {
|
|
26
|
+
x: selectedElement.scaleX || 1,
|
|
27
|
+
y: selectedElement.scaleY || 1,
|
|
28
|
+
};
|
|
29
|
+
}, [selectedElement]);
|
|
30
|
+
|
|
31
|
+
// 見た目のサイズを計算
|
|
32
|
+
const visualSize = useMemo(() => {
|
|
33
|
+
if (!selectedElement) return { w: 0, h: 0 };
|
|
34
|
+
return {
|
|
35
|
+
w: Math.round(selectedElement.width * currentScale.x),
|
|
36
|
+
h: Math.round(selectedElement.height * currentScale.y),
|
|
37
|
+
};
|
|
38
|
+
}, [selectedElement, currentScale]);
|
|
39
|
+
|
|
40
|
+
// アンカーポイントの変換(CSS <-> UI)
|
|
41
|
+
const currentAnchor = useMemo(() => {
|
|
42
|
+
if (!selectedElement?.transformOrigin) return 'center center';
|
|
43
|
+
// 数値の場合はキーワードに変換(簡易対応)
|
|
44
|
+
const origin = selectedElement.transformOrigin;
|
|
45
|
+
if (origin.includes('50% 50%')) return 'center center';
|
|
46
|
+
return origin;
|
|
47
|
+
}, [selectedElement]);
|
|
48
|
+
|
|
49
|
+
const handleScaleChange = (newScale: number) => {
|
|
50
|
+
if (!selectedElement) return;
|
|
51
|
+
updateElementStyle({
|
|
52
|
+
transform: buildTransformString({
|
|
53
|
+
rotation: selectedElement.rotation,
|
|
54
|
+
scaleX: newScale,
|
|
55
|
+
scaleY: newScale, // アスペクト比維持のため両方変更
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleSizeChange = (dimension: 'w' | 'h', value: number) => {
|
|
61
|
+
if (!selectedElement) return;
|
|
62
|
+
|
|
63
|
+
// スケール前の元のサイズ
|
|
64
|
+
const originalSize = dimension === 'w' ? selectedElement.width : selectedElement.height;
|
|
65
|
+
if (originalSize === 0) return;
|
|
66
|
+
|
|
67
|
+
// 新しいスケールを計算 (NewVisualSize / OriginalSize)
|
|
68
|
+
const newScale = value / originalSize;
|
|
69
|
+
|
|
70
|
+
updateElementStyle({
|
|
71
|
+
transform: buildTransformString({
|
|
72
|
+
rotation: selectedElement.rotation,
|
|
73
|
+
scaleX: newScale,
|
|
74
|
+
scaleY: newScale, // アスペクト比維持
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const anchorPoints = [
|
|
80
|
+
{ id: 'left top', label: '左上' },
|
|
81
|
+
{ id: 'center top', label: '中上' },
|
|
82
|
+
{ id: 'right top', label: '右上' },
|
|
83
|
+
{ id: 'left center', label: '左中' },
|
|
84
|
+
{ id: 'center center', label: '中心' },
|
|
85
|
+
{ id: 'right center', label: '右中' },
|
|
86
|
+
{ id: 'left bottom', label: '左下' },
|
|
87
|
+
{ id: 'center bottom', label: '中下' },
|
|
88
|
+
{ id: 'right bottom', label: '右下' },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
if (!selectedElement) {
|
|
92
|
+
return (
|
|
93
|
+
<div className="w-72 bg-[#2c2c2c] border-l border-[#444444] flex flex-col overflow-hidden">
|
|
94
|
+
<div className="flex-1 flex items-center justify-center p-4">
|
|
95
|
+
<div className="text-center text-gray-500 text-xs">
|
|
96
|
+
<MousePointer2 className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
|
97
|
+
<p>要素を選択してください</p>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="w-72 bg-[#2c2c2c] border-l border-[#444444] flex flex-col overflow-hidden">
|
|
106
|
+
<div className="p-3 space-y-4">
|
|
107
|
+
<div className="flex items-center justify-between mb-4">
|
|
108
|
+
<div className="flex items-center gap-2">
|
|
109
|
+
<Scaling className="w-4 h-4 text-[#4fb8ff]" />
|
|
110
|
+
<h3 className="text-sm font-medium text-white">拡大縮小</h3>
|
|
111
|
+
</div>
|
|
112
|
+
<button
|
|
113
|
+
onClick={() => setActiveTool('select')}
|
|
114
|
+
className="p-1 hover:bg-[#444444] rounded text-gray-400 hover:text-white transition-colors"
|
|
115
|
+
title="完了 (選択ツールに戻る)"
|
|
116
|
+
>
|
|
117
|
+
<Check className="w-4 h-4" />
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* 幅・高さ */}
|
|
122
|
+
<div className="grid grid-cols-2 gap-2">
|
|
123
|
+
<div>
|
|
124
|
+
<Label className="text-[10px] text-gray-500">幅 (W)</Label>
|
|
125
|
+
<div className="relative">
|
|
126
|
+
<span className="absolute left-2 top-1.5 text-xs text-gray-500 font-mono">W</span>
|
|
127
|
+
<Input
|
|
128
|
+
type="number"
|
|
129
|
+
value={visualSize.w}
|
|
130
|
+
onChange={(e) => handleSizeChange('w', parseFloat(e.target.value) || 0)}
|
|
131
|
+
className="h-8 pl-6 text-xs bg-[#383838] border-[#444444] text-white font-mono"
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div>
|
|
136
|
+
<Label className="text-[10px] text-gray-500">高さ (H)</Label>
|
|
137
|
+
<div className="relative">
|
|
138
|
+
<span className="absolute left-2 top-1.5 text-xs text-gray-500 font-mono">H</span>
|
|
139
|
+
<Input
|
|
140
|
+
type="number"
|
|
141
|
+
value={visualSize.h}
|
|
142
|
+
onChange={(e) => handleSizeChange('h', parseFloat(e.target.value) || 0)}
|
|
143
|
+
className="h-8 pl-6 text-xs bg-[#383838] border-[#444444] text-white font-mono"
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div className="grid grid-cols-2 gap-4">
|
|
150
|
+
{/* 拡大率 */}
|
|
151
|
+
<div>
|
|
152
|
+
<Label className="text-[10px] text-gray-500 mb-1 block">拡大縮小</Label>
|
|
153
|
+
<div className="flex gap-1">
|
|
154
|
+
<div className="relative flex-1">
|
|
155
|
+
<Scaling className="absolute left-2 top-2 w-3.5 h-3.5 text-gray-500" />
|
|
156
|
+
<Input
|
|
157
|
+
type="text"
|
|
158
|
+
value={`${Math.round(currentScale.x * 100) / 100}x`}
|
|
159
|
+
onChange={(e) => {
|
|
160
|
+
const val = parseFloat(e.target.value.replace('x', ''));
|
|
161
|
+
if (!isNaN(val)) handleScaleChange(val);
|
|
162
|
+
}}
|
|
163
|
+
className="h-8 pl-7 text-xs bg-[#383838] border-[#444444] text-[#4fb8ff] font-bold border-[#0d99ff]/50"
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
<Select
|
|
167
|
+
value={String(currentScale.x)}
|
|
168
|
+
onValueChange={(val) => handleScaleChange(parseFloat(val))}
|
|
169
|
+
>
|
|
170
|
+
<SelectTrigger className="w-8 h-8 p-0 bg-[#383838] border-[#444444]">
|
|
171
|
+
<span className="sr-only">倍率を選択</span>
|
|
172
|
+
</SelectTrigger>
|
|
173
|
+
<SelectContent>
|
|
174
|
+
<SelectItem value="0.5">0.5x</SelectItem>
|
|
175
|
+
<SelectItem value="1">1x</SelectItem>
|
|
176
|
+
<SelectItem value="1.5">1.5x</SelectItem>
|
|
177
|
+
<SelectItem value="2">2x</SelectItem>
|
|
178
|
+
<SelectItem value="4">4x</SelectItem>
|
|
179
|
+
</SelectContent>
|
|
180
|
+
</Select>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{/* アンカーポイント */}
|
|
185
|
+
<div>
|
|
186
|
+
<Label className="text-[10px] text-gray-500 mb-1 block">アンカーポイント</Label>
|
|
187
|
+
<div className="grid grid-cols-3 gap-1 w-[72px] h-[72px] p-1 bg-[#2e2e2e] rounded-md border border-[#444444]">
|
|
188
|
+
{anchorPoints.map((point) => (
|
|
189
|
+
<button
|
|
190
|
+
key={point.id}
|
|
191
|
+
onClick={() => updateElementStyle({ transformOrigin: point.id })}
|
|
192
|
+
className={`w-4 h-4 rounded-sm flex items-center justify-center transition-colors ${
|
|
193
|
+
(selectedElement?.transformOrigin || 'center center') === point.id
|
|
194
|
+
? 'bg-[#0d99ff]'
|
|
195
|
+
: 'bg-[#4a4a4a] hover:bg-[#5d5d5d]'
|
|
196
|
+
}`}
|
|
197
|
+
title={point.label}
|
|
198
|
+
>
|
|
199
|
+
<div className={`w-0.5 h-0.5 rounded-full ${
|
|
200
|
+
(selectedElement?.transformOrigin || 'center center') === point.id
|
|
201
|
+
? 'bg-white'
|
|
202
|
+
: 'bg-gray-400'
|
|
203
|
+
}`} />
|
|
204
|
+
</button>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|