@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.
Files changed (219) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +52 -0
  3. package/README.md +143 -0
  4. package/dist/editor.css +2 -0
  5. package/package.json +94 -0
  6. package/src/components/DeckSlideRender.tsx +21 -0
  7. package/src/components/SaveNote.tsx +21 -0
  8. package/src/components/auth/AuthProvider.tsx +44 -0
  9. package/src/components/ui/accordion.tsx +57 -0
  10. package/src/components/ui/alert-dialog.tsx +141 -0
  11. package/src/components/ui/badge.tsx +36 -0
  12. package/src/components/ui/button.tsx +56 -0
  13. package/src/components/ui/card.tsx +76 -0
  14. package/src/components/ui/checkbox.tsx +30 -0
  15. package/src/components/ui/collapsible.tsx +11 -0
  16. package/src/components/ui/context-menu.tsx +200 -0
  17. package/src/components/ui/dialog.tsx +122 -0
  18. package/src/components/ui/dropdown-menu.tsx +201 -0
  19. package/src/components/ui/input.tsx +22 -0
  20. package/src/components/ui/label.tsx +26 -0
  21. package/src/components/ui/popover.tsx +33 -0
  22. package/src/components/ui/scroll-area.tsx +48 -0
  23. package/src/components/ui/scrubbable-label.tsx +95 -0
  24. package/src/components/ui/select.tsx +159 -0
  25. package/src/components/ui/separator.tsx +31 -0
  26. package/src/components/ui/slider.tsx +28 -0
  27. package/src/components/ui/switch.tsx +29 -0
  28. package/src/components/ui/tabs.tsx +55 -0
  29. package/src/components/ui/textarea.tsx +22 -0
  30. package/src/components/ui/tooltip.tsx +32 -0
  31. package/src/components/viewer/useDeck.ts +58 -0
  32. package/src/editor/EditorContext.tsx +632 -0
  33. package/src/editor/EditorToolbar.tsx +446 -0
  34. package/src/editor/FrontendVisualEditor.tsx +2595 -0
  35. package/src/editor/autosave.ts +34 -0
  36. package/src/editor/components/AiPromptPopover.tsx +378 -0
  37. package/src/editor/components/BreakpointGuides.tsx +323 -0
  38. package/src/editor/components/BreakpointSelector.tsx +180 -0
  39. package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
  40. package/src/editor/components/CssEditorDialog.tsx +168 -0
  41. package/src/editor/components/EditorCanvas.tsx +987 -0
  42. package/src/editor/components/EditorContextMenu.tsx +515 -0
  43. package/src/editor/components/EditorFooter.tsx +81 -0
  44. package/src/editor/components/EditorHeader.tsx +524 -0
  45. package/src/editor/components/EditorLayerPanel.tsx +1197 -0
  46. package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
  47. package/src/editor/components/EditorSidebar.tsx +226 -0
  48. package/src/editor/components/FigmaColorPicker.tsx +1577 -0
  49. package/src/editor/components/HtmlEditorDialog.tsx +71 -0
  50. package/src/editor/components/HtmlImportDialog.tsx +346 -0
  51. package/src/editor/components/JsEditorDialog.tsx +175 -0
  52. package/src/editor/components/LayerTreeItem.tsx +494 -0
  53. package/src/editor/components/MasterComponentEditor.tsx +846 -0
  54. package/src/editor/components/MediaLibraryDialog.tsx +447 -0
  55. package/src/editor/components/PageSettingsDialog.tsx +776 -0
  56. package/src/editor/components/VariablePicker.tsx +355 -0
  57. package/src/editor/components/VariablesDialog.tsx +564 -0
  58. package/src/editor/components/VariantEditor.tsx +687 -0
  59. package/src/editor/components/VariantMatrix.tsx +507 -0
  60. package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
  61. package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
  62. package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
  63. package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
  64. package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
  65. package/src/editor/components/component-panel/VariantList.tsx +260 -0
  66. package/src/editor/components/component-panel/index.ts +12 -0
  67. package/src/editor/components/index.ts +48 -0
  68. package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
  69. package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
  70. package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
  71. package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
  72. package/src/editor/components/multi-page/PageLabel.tsx +34 -0
  73. package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
  74. package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
  75. package/src/editor/components/multi-page/index.ts +7 -0
  76. package/src/editor/components/ppt/PptChrome.tsx +2508 -0
  77. package/src/editor/components/ppt/PptComments.tsx +589 -0
  78. package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
  79. package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
  80. package/src/editor/components/ppt/PptNotes.tsx +246 -0
  81. package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
  82. package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
  83. package/src/editor/components/property-panel/BorderSection.tsx +104 -0
  84. package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
  85. package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
  86. package/src/editor/components/property-panel/EffectSection.tsx +157 -0
  87. package/src/editor/components/property-panel/FillSection.tsx +69 -0
  88. package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
  89. package/src/editor/components/property-panel/ImageSection.tsx +206 -0
  90. package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
  91. package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
  92. package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
  93. package/src/editor/components/property-panel/LinkSection.tsx +103 -0
  94. package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
  95. package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
  96. package/src/editor/components/property-panel/TypographySection.tsx +230 -0
  97. package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
  98. package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
  99. package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
  100. package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
  101. package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
  102. package/src/editor/components/property-panel/index.ts +24 -0
  103. package/src/editor/components/property-panel/unit-utils.ts +261 -0
  104. package/src/editor/components/shell/EditorTopBar.tsx +392 -0
  105. package/src/editor/components/shell/LeftPanel.tsx +47 -0
  106. package/src/editor/components/shell/PagesPanel.tsx +118 -0
  107. package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
  108. package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
  109. package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
  110. package/src/editor/components/variables-panel/index.ts +7 -0
  111. package/src/editor/constants.ts +468 -0
  112. package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
  113. package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
  114. package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
  115. package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
  116. package/src/editor/contexts/EditorRefsContext.tsx +204 -0
  117. package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
  118. package/src/editor/contexts/EditorToolContext.tsx +58 -0
  119. package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
  120. package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
  121. package/src/editor/contexts/EditorViewContext.tsx +101 -0
  122. package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
  123. package/src/editor/contexts/index.ts +17 -0
  124. package/src/editor/editor-skin.css +122 -0
  125. package/src/editor/hooks/index.ts +38 -0
  126. package/src/editor/hooks/useAiReplace.ts +397 -0
  127. package/src/editor/hooks/useAltMeasure.ts +67 -0
  128. package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
  129. package/src/editor/hooks/useCanvasControls.ts +1222 -0
  130. package/src/editor/hooks/useComponentEditMode.ts +658 -0
  131. package/src/editor/hooks/useComponentInstances.ts +515 -0
  132. package/src/editor/hooks/useContextMenuHandler.ts +157 -0
  133. package/src/editor/hooks/useCoordinateTransform.ts +207 -0
  134. package/src/editor/hooks/useDragResize.ts +1317 -0
  135. package/src/editor/hooks/useDrawingMode.ts +472 -0
  136. package/src/editor/hooks/useEditorColors.ts +144 -0
  137. package/src/editor/hooks/useEditorMessages.ts +108 -0
  138. package/src/editor/hooks/useElementActions.ts +1754 -0
  139. package/src/editor/hooks/useElementSelection.ts +1573 -0
  140. package/src/editor/hooks/useFocusManagement.ts +249 -0
  141. package/src/editor/hooks/useGoogleFonts.ts +305 -0
  142. package/src/editor/hooks/useIframeInitializer.ts +584 -0
  143. package/src/editor/hooks/useIframeSetup.ts +360 -0
  144. package/src/editor/hooks/useImageUpload.ts +270 -0
  145. package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
  146. package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
  147. package/src/editor/hooks/useMarqueeSelection.ts +388 -0
  148. package/src/editor/hooks/useMediaLibrary.ts +129 -0
  149. package/src/editor/hooks/usePageSettingsManager.ts +739 -0
  150. package/src/editor/hooks/useResizablePanel.ts +163 -0
  151. package/src/editor/hooks/useRichPaste.ts +255 -0
  152. package/src/editor/hooks/useTouchGestures.ts +197 -0
  153. package/src/editor/index.ts +27 -0
  154. package/src/editor/types/page-settings.ts +513 -0
  155. package/src/editor/types.ts +382 -0
  156. package/src/editor/utils/align-elements.ts +169 -0
  157. package/src/editor/utils/api-json.ts +21 -0
  158. package/src/editor/utils/aspect-lock.ts +50 -0
  159. package/src/editor/utils/component-renderer.ts +702 -0
  160. package/src/editor/utils/component-sync.ts +845 -0
  161. package/src/editor/utils/crop-mode.ts +265 -0
  162. package/src/editor/utils/dom-utils.ts +2238 -0
  163. package/src/editor/utils/element-effects.ts +218 -0
  164. package/src/editor/utils/eyedropper.ts +35 -0
  165. package/src/editor/utils/figma-export.ts +560 -0
  166. package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
  167. package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
  168. package/src/editor/utils/figma-paste.ts +2262 -0
  169. package/src/editor/utils/flex-reorder.ts +231 -0
  170. package/src/editor/utils/flex-utils.ts +1230 -0
  171. package/src/editor/utils/geometry.ts +232 -0
  172. package/src/editor/utils/grid-layout.ts +210 -0
  173. package/src/editor/utils/html-utils.ts +470 -0
  174. package/src/editor/utils/index.ts +11 -0
  175. package/src/editor/utils/ink-style.ts +57 -0
  176. package/src/editor/utils/inline-format.ts +236 -0
  177. package/src/editor/utils/measure-distance.ts +120 -0
  178. package/src/editor/utils/override-apply.ts +526 -0
  179. package/src/editor/utils/override-detection.ts +613 -0
  180. package/src/editor/utils/paste-processors.ts +609 -0
  181. package/src/editor/utils/paste-sanitizer.ts +244 -0
  182. package/src/editor/utils/restore-flow.ts +188 -0
  183. package/src/editor/utils/shape-library.ts +171 -0
  184. package/src/editor/utils/slide-root.ts +29 -0
  185. package/src/editor/utils/smart-guides.ts +218 -0
  186. package/src/editor/utils/style-utils.ts +685 -0
  187. package/src/editor/utils/table-edit.ts +151 -0
  188. package/src/editor/utils/tailwind-mappings.ts +778 -0
  189. package/src/editor/utils/tailwind-utils.ts +804 -0
  190. package/src/editor/utils/text-highlight.ts +216 -0
  191. package/src/editor/utils/theme-to-variables.ts +341 -0
  192. package/src/editor/utils/variant-resolver.ts +456 -0
  193. package/src/editor/utils/viewport-utils.ts +295 -0
  194. package/src/hooks/useEditorHistory.ts +195 -0
  195. package/src/hooks/useEditorShortcuts.ts +260 -0
  196. package/src/index.ts +40 -0
  197. package/src/io.ts +134 -0
  198. package/src/lib/agent/slide-agent/types.ts +547 -0
  199. package/src/lib/agent/website-agent/types.ts +805 -0
  200. package/src/lib/api/auth-fetch.ts +58 -0
  201. package/src/lib/deck.ts +69 -0
  202. package/src/lib/export.ts +47 -0
  203. package/src/lib/firebase/config.ts +29 -0
  204. package/src/lib/firebase/css-variables.ts +378 -0
  205. package/src/lib/firebase/editor-components.ts +677 -0
  206. package/src/lib/firebase/storage.ts +469 -0
  207. package/src/lib/utils.ts +6 -0
  208. package/src/styles/editor.css +52 -0
  209. package/src/styles/skin.css +122 -0
  210. package/src/types/css-variables.ts +645 -0
  211. package/src/types/editor-components.ts +944 -0
  212. package/src/types/editor.ts +420 -0
  213. package/src/types/page-master.ts +197 -0
  214. package/src/types/page.ts +419 -0
  215. package/src/types/slide.ts +402 -0
  216. package/src/types/website-theme.ts +788 -0
  217. package/src/types/website.ts +374 -0
  218. package/src/vendor/firebase-firestore.ts +13 -0
  219. 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
+ }