@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,588 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * VariableAwareSizeInput
5
+ *
6
+ * Figmaスタイルの変数対応サイズ入力コンポーネント(ダークテーマ)
7
+ * サイズモード(fixed/fill/hug)、単位選択、変数選択を統合
8
+ */
9
+
10
+ import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react';
11
+ import { ChevronDown, Search, X, Ruler, Plus, Link2 } from 'lucide-react';
12
+ import { cn } from '../../../lib/utils';
13
+ import {
14
+ Popover,
15
+ PopoverContent,
16
+ PopoverTrigger,
17
+ } from '../../../components/ui/popover';
18
+ import { Input } from '../../../components/ui/input';
19
+ import { Button } from '../../../components/ui/button';
20
+ import { ScrollArea } from '../../../components/ui/scroll-area';
21
+ import { useEditorVariables } from '../../EditorContext';
22
+ import type { CSSVariableDefinition } from '../../../types/css-variables';
23
+ import { isVariableReference, extractVariableName, generateVarReference } from '../../../types/css-variables';
24
+ import { parseValueWithUnit, formatValueWithUnit, convertUnit, type UnitConversionContext } from './unit-utils';
25
+
26
+ export type SizeMode = 'fixed' | 'fill' | 'hug';
27
+
28
+ // Width用の単位
29
+ const WIDTH_UNITS = ['px', '%', 'vw', 'em', 'rem'] as const;
30
+ // Height用の単位
31
+ const HEIGHT_UNITS = ['px', '%', 'vh', 'em', 'rem'] as const;
32
+
33
+ interface VariableAwareSizeInputProps {
34
+ value: number | string;
35
+ mode: SizeMode;
36
+ onChangeValue: (value: string) => void;
37
+ onChangeMode: (mode: SizeMode) => void;
38
+ label?: React.ReactNode;
39
+ disabled?: boolean;
40
+ min?: number;
41
+ max?: number;
42
+ canFill?: boolean;
43
+ canHug?: boolean;
44
+ /** 'width' or 'height' - 単位リストを決定 */
45
+ dimension?: 'width' | 'height';
46
+ /** 単位変換コンテキスト */
47
+ conversionContext?: UnitConversionContext;
48
+ }
49
+
50
+ export function VariableAwareSizeInput({
51
+ value,
52
+ mode,
53
+ onChangeValue,
54
+ onChangeMode,
55
+ label,
56
+ disabled = false,
57
+ min = 0,
58
+ max,
59
+ canFill = false,
60
+ canHug = false,
61
+ dimension = 'width',
62
+ conversionContext,
63
+ }: VariableAwareSizeInputProps) {
64
+ const [isOpen, setIsOpen] = useState(false);
65
+ const [searchQuery, setSearchQuery] = useState('');
66
+ const [localValue, setLocalValue] = useState('');
67
+ const [isCreating, setIsCreating] = useState(false);
68
+ const [newVarName, setNewVarName] = useState('');
69
+ const [newVarValue, setNewVarValue] = useState('');
70
+ const [isDragging, setIsDragging] = useState(false);
71
+ // 単位を独立して追跡(valueが数値に変わっても単位を保持)
72
+ const [trackedUnit, setTrackedUnit] = useState<string | null>(null);
73
+ const createInputRef = useRef<HTMLInputElement>(null);
74
+ const inputRef = useRef<HTMLInputElement>(null);
75
+ const dragStartRef = useRef<{ x: number; startValue: number } | null>(null);
76
+ // ドラッグ開始のための移動閾値
77
+ const DRAG_THRESHOLD = 3;
78
+
79
+ // 単位リストを決定
80
+ const unitList = dimension === 'height' ? HEIGHT_UNITS : WIDTH_UNITS;
81
+
82
+ // CSS変数を取得
83
+ const { variables, addVariable } = useEditorVariables();
84
+
85
+ // 現在の値がCSS変数参照かどうか
86
+ const isLinked = useMemo(() => {
87
+ return typeof value === 'string' && isVariableReference(value);
88
+ }, [value]);
89
+
90
+ // リンクされている変数を取得
91
+ const linkedVariable = useMemo(() => {
92
+ if (!isLinked || typeof value !== 'string') return null;
93
+ const varName = extractVariableName(value);
94
+ return variables.find(v => v.cssName === varName);
95
+ }, [isLinked, value, variables]);
96
+
97
+ // 値と単位をパース
98
+ const parsed = useMemo(() => {
99
+ if (isLinked) {
100
+ return { numericValue: 0, unit: 'px', isSpecial: true };
101
+ }
102
+ if (typeof value === 'number') {
103
+ // 数値の場合、追跡中の単位があればそれを使用
104
+ return { numericValue: value, unit: trackedUnit || 'px', isSpecial: false };
105
+ }
106
+ return parseValueWithUnit(value, 'px');
107
+ }, [value, isLinked, trackedUnit]);
108
+
109
+ // 文字列値から単位を追跡
110
+ useEffect(() => {
111
+ if (typeof value === 'string' && !isLinked) {
112
+ const parsedValue = parseValueWithUnit(value, 'px');
113
+ if (!parsedValue.isSpecial && parsedValue.unit !== trackedUnit) {
114
+ setTrackedUnit(parsedValue.unit);
115
+ }
116
+ }
117
+ }, [value, isLinked, trackedUnit]);
118
+
119
+ const currentUnit = parsed.unit;
120
+
121
+ // スペーシング変数のみフィルター
122
+ const spacingVariables = useMemo(() => {
123
+ let filtered = variables.filter(v => v.category === 'spacing');
124
+
125
+ if (searchQuery) {
126
+ const query = searchQuery.toLowerCase();
127
+ filtered = filtered.filter(v =>
128
+ v.name.toLowerCase().includes(query) ||
129
+ v.cssName.toLowerCase().includes(query)
130
+ );
131
+ }
132
+
133
+ return filtered;
134
+ }, [variables, searchQuery]);
135
+
136
+ // 表示値
137
+ const displayValue = useMemo(() => {
138
+ if (isLinked && linkedVariable) {
139
+ return linkedVariable.name;
140
+ }
141
+ return parsed.numericValue;
142
+ }, [isLinked, linkedVariable, parsed]);
143
+
144
+ // 変数を選択
145
+ const handleSelectVariable = useCallback((variable: CSSVariableDefinition) => {
146
+ const varReference = generateVarReference(variable);
147
+ onChangeValue(varReference);
148
+ setIsOpen(false);
149
+ setSearchQuery('');
150
+ }, [onChangeValue]);
151
+
152
+ // リンク解除
153
+ const handleDetach = useCallback((e: React.MouseEvent) => {
154
+ e.stopPropagation();
155
+ if (linkedVariable) {
156
+ onChangeValue(linkedVariable.value);
157
+ } else {
158
+ onChangeValue('auto');
159
+ }
160
+ }, [linkedVariable, onChangeValue]);
161
+
162
+ // モード変更
163
+ const handleModeChange = useCallback((newMode: SizeMode) => {
164
+ onChangeMode(newMode);
165
+ setIsOpen(false);
166
+ }, [onChangeMode]);
167
+
168
+ // 入力値の変更
169
+ const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
170
+ const newValue = e.target.value;
171
+ setLocalValue(newValue);
172
+
173
+ const num = parseFloat(newValue);
174
+ if (!isNaN(num)) {
175
+ let clampedValue = num;
176
+ if (min !== undefined) clampedValue = Math.max(min, clampedValue);
177
+ if (max !== undefined) clampedValue = Math.min(max, clampedValue);
178
+ onChangeValue(formatValueWithUnit(clampedValue, currentUnit));
179
+ }
180
+ }, [onChangeValue, min, max, currentUnit]);
181
+
182
+ // 単位変更(値を変換)
183
+ const handleUnitChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
184
+ const newUnit = e.target.value;
185
+ const context: UnitConversionContext = {
186
+ ...conversionContext,
187
+ property: dimension === 'height' ? 'height' : 'width',
188
+ };
189
+ const convertedValue = convertUnit(parsed.numericValue, currentUnit, newUnit, context);
190
+ // 新しい単位を追跡
191
+ setTrackedUnit(newUnit);
192
+ onChangeValue(formatValueWithUnit(convertedValue, newUnit));
193
+ }, [onChangeValue, parsed.numericValue, currentUnit, conversionContext, dimension]);
194
+
195
+ // 入力のフォーカス時
196
+ const handleInputFocus = useCallback(() => {
197
+ if (!isLinked) {
198
+ setLocalValue(String(Math.round(parsed.numericValue)));
199
+ }
200
+ }, [isLinked, parsed.numericValue]);
201
+
202
+ // 現在の値から変数を作成
203
+ const handleCreateVariable = useCallback(() => {
204
+ if (!newVarName.trim() || !addVariable) return;
205
+
206
+ let finalValue = newVarValue.trim();
207
+ if (!finalValue) {
208
+ finalValue = formatValueWithUnit(parsed.numericValue, currentUnit);
209
+ }
210
+
211
+ const newVar = addVariable(newVarName.trim(), finalValue, 'spacing');
212
+ const varRef = generateVarReference(newVar);
213
+ onChangeValue(varRef);
214
+
215
+ setNewVarName('');
216
+ setNewVarValue('');
217
+ setIsCreating(false);
218
+ setIsOpen(false);
219
+ }, [newVarName, newVarValue, parsed, currentUnit, addVariable, onChangeValue]);
220
+
221
+ // 変数作成UIを開く
222
+ const handleStartCreating = useCallback(() => {
223
+ setIsCreating(true);
224
+ setSearchQuery('');
225
+ setNewVarValue(formatValueWithUnit(parsed.numericValue, currentUnit));
226
+ setTimeout(() => createInputRef.current?.focus(), 0);
227
+ }, [parsed, currentUnit]);
228
+
229
+ // 変数作成をキャンセル
230
+ const handleCancelCreating = useCallback(() => {
231
+ setIsCreating(false);
232
+ setNewVarName('');
233
+ setNewVarValue('');
234
+ }, []);
235
+
236
+ // スクラブ開始(ドラッグで値を変更)
237
+ // 改善: クリックでフォーカスを許可し、ドラッグ閾値を超えた場合のみスクラブを開始
238
+ const handleMouseDown = useCallback(
239
+ (e: React.MouseEvent<HTMLInputElement>) => {
240
+ if (disabled || isLinked || mode !== 'fixed') return;
241
+ if (e.button !== 0) return;
242
+ // 既にフォーカスされている場合は通常の入力を許可
243
+ if (document.activeElement === inputRef.current) return;
244
+
245
+ const startX = e.clientX;
246
+ const startValue = parsed.numericValue;
247
+ let hasDragged = false;
248
+
249
+ const handleMouseMove = (moveEvent: MouseEvent) => {
250
+ const delta = moveEvent.clientX - startX;
251
+
252
+ // ドラッグ閾値を超えるまでは何もしない
253
+ if (!hasDragged && Math.abs(delta) < DRAG_THRESHOLD) {
254
+ return;
255
+ }
256
+
257
+ // 閾値を超えたらドラッグモードに入る
258
+ if (!hasDragged) {
259
+ hasDragged = true;
260
+ setIsDragging(true);
261
+ // ドラッグ開始時にフォーカスを外す
262
+ inputRef.current?.blur();
263
+ }
264
+
265
+ const sensitivity = moveEvent.shiftKey ? 0.1 : 1;
266
+ let newValue = startValue + delta * sensitivity;
267
+
268
+ if (min !== undefined && newValue < min) newValue = min;
269
+ if (max !== undefined && newValue > max) newValue = max;
270
+
271
+ newValue = Math.round(newValue);
272
+ onChangeValue(formatValueWithUnit(newValue, currentUnit));
273
+ };
274
+
275
+ const handleMouseUp = () => {
276
+ document.removeEventListener('mousemove', handleMouseMove);
277
+ document.removeEventListener('mouseup', handleMouseUp);
278
+
279
+ if (hasDragged) {
280
+ setIsDragging(false);
281
+ } else {
282
+ // ドラッグしなかった場合はinputにフォーカスを当てる
283
+ inputRef.current?.focus();
284
+ inputRef.current?.select();
285
+ }
286
+ };
287
+
288
+ document.addEventListener('mousemove', handleMouseMove);
289
+ document.addEventListener('mouseup', handleMouseUp);
290
+ },
291
+ [disabled, isLinked, mode, parsed, min, max, currentUnit, onChangeValue]
292
+ );
293
+
294
+ return (
295
+ <div className="flex items-center gap-0.5 group/size-input">
296
+ {/* Label area (W/H) */}
297
+ {label && <div className="shrink-0">{label}</div>}
298
+
299
+ <Popover open={isOpen} onOpenChange={setIsOpen}>
300
+ <PopoverTrigger asChild>
301
+ <div
302
+ className={cn(
303
+ "flex items-center h-5 bg-[#383838] border border-[#4a4a4a] rounded-l cursor-pointer",
304
+ "hover:border-[#5d5d5d]",
305
+ disabled && "opacity-50 cursor-not-allowed",
306
+ isOpen && "ring-1 ring-[#0d99ff] border-[#0d99ff]",
307
+ mode === 'fixed' ? 'w-14' : 'flex-1'
308
+ )}
309
+ >
310
+ {/* コンテンツ */}
311
+ <div className="flex-1 min-w-0 h-full flex items-center px-1.5">
312
+ {mode === 'fixed' ? (
313
+ isLinked ? (
314
+ <div className="flex items-center gap-0.5 flex-1 min-w-0">
315
+ <Link2 className="w-2.5 h-2.5 text-purple-400 flex-shrink-0" />
316
+ <span className="text-purple-300 truncate text-[9px] font-medium">
317
+ {linkedVariable?.name || '?'}
318
+ </span>
319
+ <button
320
+ type="button"
321
+ onClick={(e) => {
322
+ e.preventDefault();
323
+ e.stopPropagation();
324
+ handleDetach(e);
325
+ }}
326
+ onMouseDown={(e) => e.stopPropagation()}
327
+ className="p-0.5 hover:text-red-400 transition-colors flex-shrink-0"
328
+ title="変数を解除"
329
+ >
330
+ <X className="w-2 h-2 text-gray-400 hover:text-red-400" />
331
+ </button>
332
+ </div>
333
+ ) : (
334
+ <input
335
+ ref={inputRef}
336
+ type="number"
337
+ value={localValue || displayValue}
338
+ onChange={handleInputChange}
339
+ onFocus={handleInputFocus}
340
+ onMouseDown={handleMouseDown}
341
+ onClick={(e) => e.stopPropagation()}
342
+ disabled={disabled}
343
+ className={cn(
344
+ "w-full bg-transparent border-none outline-none text-gray-200 text-[10px]",
345
+ "[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none",
346
+ isDragging ? "cursor-ew-resize" : "cursor-ew-resize"
347
+ )}
348
+ />
349
+ )
350
+ ) : mode === 'fill' ? (
351
+ <span className="text-[8px] text-gray-400 truncate select-none">
352
+ Fill
353
+ </span>
354
+ ) : (
355
+ <span className="text-[8px] text-gray-400 truncate select-none">
356
+ Hug
357
+ </span>
358
+ )}
359
+ </div>
360
+
361
+ {/* ドロップダウンアイコン */}
362
+ <button
363
+ type="button"
364
+ className="h-full px-0.5 flex items-center justify-center text-gray-500 hover:text-white transition-colors"
365
+ disabled={disabled}
366
+ >
367
+ <ChevronDown className="h-2.5 w-2.5" />
368
+ </button>
369
+ </div>
370
+ </PopoverTrigger>
371
+
372
+ <PopoverContent
373
+ className="w-52 p-0 bg-[#2c2c2c] border-[#444444]"
374
+ align="start"
375
+ side="bottom"
376
+ sideOffset={4}
377
+ >
378
+ {/* 検索 */}
379
+ <div className="p-2 border-b border-[#444444]">
380
+ <div className="relative">
381
+ <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-gray-500" />
382
+ <Input
383
+ type="text"
384
+ placeholder="検索..."
385
+ value={searchQuery}
386
+ onChange={(e) => setSearchQuery(e.target.value)}
387
+ className="h-6 pl-7 text-[10px] bg-[#383838] border-[#444444] text-white placeholder:text-gray-500"
388
+ autoFocus
389
+ />
390
+ </div>
391
+ </div>
392
+
393
+ <ScrollArea className="max-h-56">
394
+ {/* リサイズモード */}
395
+ <div className="p-1 border-b border-[#444444]">
396
+ <div className="px-2 py-0.5 text-[9px] text-gray-500 uppercase">
397
+ リサイズ
398
+ </div>
399
+
400
+ <button
401
+ type="button"
402
+ onClick={(e) => {
403
+ e.preventDefault();
404
+ e.stopPropagation();
405
+ handleModeChange('fixed');
406
+ }}
407
+ className={cn(
408
+ "w-full flex items-center justify-between px-2 py-1 rounded text-[10px] text-white",
409
+ "hover:bg-[#444444] transition-colors",
410
+ mode === 'fixed' && "text-[#4fb8ff]"
411
+ )}
412
+ >
413
+ <span>固定 (Fixed)</span>
414
+ {mode === 'fixed' && <span>✓</span>}
415
+ </button>
416
+
417
+ <button
418
+ type="button"
419
+ onClick={(e) => {
420
+ e.preventDefault();
421
+ e.stopPropagation();
422
+ if (canFill) handleModeChange('fill');
423
+ }}
424
+ disabled={!canFill}
425
+ className={cn(
426
+ "w-full flex items-center justify-between px-2 py-1 rounded text-[10px] text-white",
427
+ "hover:bg-[#444444] transition-colors",
428
+ !canFill && "opacity-50 cursor-not-allowed",
429
+ mode === 'fill' && "text-[#4fb8ff]"
430
+ )}
431
+ >
432
+ <span>コンテナ (Fill)</span>
433
+ {mode === 'fill' && <span>✓</span>}
434
+ </button>
435
+
436
+ <button
437
+ type="button"
438
+ onClick={(e) => {
439
+ e.preventDefault();
440
+ e.stopPropagation();
441
+ if (canHug) handleModeChange('hug');
442
+ }}
443
+ disabled={!canHug}
444
+ className={cn(
445
+ "w-full flex items-center justify-between px-2 py-1 rounded text-[10px] text-white",
446
+ "hover:bg-[#444444] transition-colors",
447
+ !canHug && "opacity-50 cursor-not-allowed",
448
+ mode === 'hug' && "text-[#4fb8ff]"
449
+ )}
450
+ >
451
+ <span>コンテンツ (Hug)</span>
452
+ {mode === 'hug' && <span>✓</span>}
453
+ </button>
454
+ </div>
455
+
456
+ {/* スペーシング変数 */}
457
+ <div className="p-1">
458
+ <div className="px-2 py-0.5 text-[9px] text-gray-500 uppercase flex items-center justify-between">
459
+ <div className="flex items-center gap-1">
460
+ <Ruler className="w-2.5 h-2.5" />
461
+ 変数
462
+ </div>
463
+ {!isCreating && !isLinked && (
464
+ <button
465
+ type="button"
466
+ onClick={(e) => {
467
+ e.preventDefault();
468
+ e.stopPropagation();
469
+ handleStartCreating();
470
+ }}
471
+ className="flex items-center gap-0.5 px-1 py-0.5 text-[9px] text-gray-400 hover:text-white hover:bg-[#444444] rounded transition-colors"
472
+ title="変数を作成"
473
+ >
474
+ <Plus className="w-2.5 h-2.5" />
475
+ </button>
476
+ )}
477
+ </div>
478
+
479
+ {/* 変数作成UI */}
480
+ {isCreating && (
481
+ <div className="px-2 py-1.5 mb-1 bg-[#2a2a2a] rounded">
482
+ <div className="space-y-1.5">
483
+ <Input
484
+ ref={createInputRef}
485
+ type="text"
486
+ placeholder="変数名"
487
+ value={newVarName}
488
+ onChange={(e) => setNewVarName(e.target.value)}
489
+ onKeyDown={(e) => {
490
+ if (e.key === 'Enter') handleCreateVariable();
491
+ else if (e.key === 'Escape') handleCancelCreating();
492
+ }}
493
+ className="h-5 text-[9px] bg-[#383838] border-[#444444] text-white placeholder:text-gray-600"
494
+ />
495
+ <Input
496
+ type="text"
497
+ placeholder="値 (例: 16px)"
498
+ value={newVarValue}
499
+ onChange={(e) => setNewVarValue(e.target.value)}
500
+ onKeyDown={(e) => {
501
+ if (e.key === 'Enter') handleCreateVariable();
502
+ else if (e.key === 'Escape') handleCancelCreating();
503
+ }}
504
+ className="h-5 text-[9px] bg-[#383838] border-[#444444] text-white placeholder:text-gray-600"
505
+ />
506
+ <div className="flex justify-end gap-1">
507
+ <Button
508
+ size="sm"
509
+ variant="ghost"
510
+ onClick={handleCancelCreating}
511
+ className="h-5 px-2 text-[9px] text-gray-400 hover:text-white"
512
+ >
513
+ キャンセル
514
+ </Button>
515
+ <Button
516
+ size="sm"
517
+ onClick={handleCreateVariable}
518
+ disabled={!newVarName.trim()}
519
+ className="h-5 px-2 text-[9px] bg-[#0d99ff] hover:bg-[#0c8ce9]"
520
+ >
521
+ 作成
522
+ </Button>
523
+ </div>
524
+ </div>
525
+ </div>
526
+ )}
527
+
528
+ {spacingVariables.length === 0 ? (
529
+ <div className="px-2 py-2 text-[9px] text-gray-500 text-center">
530
+ 変数なし
531
+ </div>
532
+ ) : (
533
+ spacingVariables.map((variable) => (
534
+ <button
535
+ key={variable.id}
536
+ type="button"
537
+ onClick={(e) => {
538
+ e.preventDefault();
539
+ e.stopPropagation();
540
+ handleSelectVariable(variable);
541
+ }}
542
+ className={cn(
543
+ "w-full flex items-center justify-between px-2 py-1 rounded text-[10px]",
544
+ "hover:bg-[#444444] transition-colors",
545
+ linkedVariable?.id === variable.id
546
+ ? "bg-purple-900/30 text-purple-300"
547
+ : "text-white"
548
+ )}
549
+ >
550
+ <span className="truncate">{variable.name}</span>
551
+ <span className="text-gray-500 font-mono text-[8px]">{variable.value}</span>
552
+ </button>
553
+ ))
554
+ )}
555
+ </div>
556
+ </ScrollArea>
557
+ </PopoverContent>
558
+ </Popover>
559
+
560
+ {/* 単位セレクター(fixed モードで変数リンクがない場合のみ) */}
561
+ {mode === 'fixed' && !isLinked && (
562
+ <select
563
+ value={currentUnit}
564
+ onChange={handleUnitChange}
565
+ disabled={disabled}
566
+ className={cn(
567
+ "h-5 w-10 px-0 text-[8px] border border-l-0 rounded-r border-[#4a4a4a]",
568
+ "bg-[#4a4a4a] text-gray-300 focus:outline-none focus:border-[#0d99ff]",
569
+ "disabled:bg-[#383838] disabled:text-gray-500 cursor-pointer"
570
+ )}
571
+ >
572
+ {unitList.map((u) => (
573
+ <option key={u} value={u}>{u}</option>
574
+ ))}
575
+ </select>
576
+ )}
577
+
578
+ {/* fill/hug モードまたはリンク時は角丸を調整 */}
579
+ {(mode !== 'fixed' || isLinked) && (
580
+ <style>{`
581
+ :global(.group\\/size-input > div:first-of-type > div:first-child) {
582
+ border-radius: 0.25rem !important;
583
+ }
584
+ `}</style>
585
+ )}
586
+ </div>
587
+ );
588
+ }