@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,606 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * VariableAwareInput
5
+ *
6
+ * Figmaスタイルの変数対応入力コンポーネント
7
+ * 入力欄から直接CSS変数を選択可能
8
+ */
9
+
10
+ import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
11
+ import {
12
+ Popover,
13
+ PopoverContent,
14
+ PopoverTrigger,
15
+ } from '../../../components/ui/popover';
16
+ import { Input } from '../../../components/ui/input';
17
+ import { Button } from '../../../components/ui/button';
18
+ import { ScrollArea } from '../../../components/ui/scroll-area';
19
+ import { Search, X, ChevronDown, Plus } from 'lucide-react';
20
+ import { cn } from '../../../lib/utils';
21
+ import { useEditorVariables } from '../../EditorContext';
22
+ import type { CSSVariableDefinition, CSSVariableCategory } from '../../../types/css-variables';
23
+ import { isVariableReference, extractVariableName, generateVarReference } from '../../../types/css-variables';
24
+
25
+ // ============================================
26
+ // 型定義
27
+ // ============================================
28
+
29
+ export interface VariableAwareInputProps {
30
+ /** 現在の値(数値または var(--xxx) 形式) */
31
+ value: string | number;
32
+ /** 値変更時のコールバック */
33
+ onChange: (value: string) => void;
34
+ /** フィルタするカテゴリ */
35
+ category?: CSSVariableCategory | null;
36
+ /** プレースホルダー */
37
+ placeholder?: string;
38
+ /** ラベル(入力欄の左に表示) */
39
+ label?: string;
40
+ /** 単位(入力欄の右に表示) */
41
+ suffix?: string;
42
+ /** 無効状態 */
43
+ disabled?: boolean;
44
+ /** クラス名 */
45
+ className?: string;
46
+ /** 数値のみ許可 */
47
+ numericOnly?: boolean;
48
+ /** 最小値(数値の場合) */
49
+ min?: number;
50
+ /** 最大値(数値の場合) */
51
+ max?: number;
52
+ /** コンパクトモード(パディング等の小さい入力用) */
53
+ compact?: boolean;
54
+ }
55
+
56
+ // ============================================
57
+ // メインコンポーネント
58
+ // ============================================
59
+
60
+ export function VariableAwareInput({
61
+ value,
62
+ onChange,
63
+ category,
64
+ placeholder,
65
+ label,
66
+ suffix,
67
+ disabled = false,
68
+ className,
69
+ numericOnly = true,
70
+ min,
71
+ max,
72
+ compact = false,
73
+ }: VariableAwareInputProps) {
74
+ const [isOpen, setIsOpen] = useState(false);
75
+ const [searchQuery, setSearchQuery] = useState('');
76
+ const [localValue, setLocalValue] = useState('');
77
+ const [isCreating, setIsCreating] = useState(false);
78
+ const [newVarName, setNewVarName] = useState('');
79
+ const [newVarValue, setNewVarValue] = useState('');
80
+ const inputRef = useRef<HTMLInputElement>(null);
81
+ const createInputRef = useRef<HTMLInputElement>(null);
82
+
83
+ // Scrubbing state
84
+ const isScrubbing = useRef(false);
85
+ const hasMoved = useRef(false);
86
+ const startX = useRef(0);
87
+ const startValue = useRef(0);
88
+ const containerRef = useRef<HTMLDivElement>(null);
89
+
90
+ // CSS変数を取得
91
+ const { variables, addVariable } = useEditorVariables();
92
+
93
+ // 現在の値がCSS変数参照かどうか
94
+ const isLinked = useMemo(() => {
95
+ return typeof value === 'string' && isVariableReference(value);
96
+ }, [value]);
97
+
98
+ // リンクされている変数を取得
99
+ const linkedVariable = useMemo(() => {
100
+ if (!isLinked || typeof value !== 'string') return null;
101
+ const varName = extractVariableName(value);
102
+ return variables.find(v => v.cssName === varName);
103
+ }, [isLinked, value, variables]);
104
+
105
+ // 表示値を計算
106
+ const displayValue = useMemo(() => {
107
+ if (isLinked && linkedVariable) {
108
+ return linkedVariable.name;
109
+ }
110
+ return String(value);
111
+ }, [isLinked, linkedVariable, value]);
112
+
113
+ // ローカル値を同期
114
+ useEffect(() => {
115
+ if (!isLinked) {
116
+ setLocalValue(String(value));
117
+ }
118
+ }, [value, isLinked]);
119
+
120
+ // フィルタリングされた変数
121
+ const filteredVariables = useMemo(() => {
122
+ let filtered = variables;
123
+
124
+ // カテゴリでフィルタ
125
+ if (category) {
126
+ filtered = filtered.filter(v => v.category === category);
127
+ }
128
+
129
+ // 検索でフィルタ
130
+ if (searchQuery) {
131
+ const query = searchQuery.toLowerCase();
132
+ filtered = filtered.filter(v =>
133
+ v.name.toLowerCase().includes(query) ||
134
+ v.cssName.toLowerCase().includes(query)
135
+ );
136
+ }
137
+
138
+ return filtered;
139
+ }, [variables, category, searchQuery]);
140
+
141
+ // 変数を選択
142
+ const handleSelectVariable = useCallback((variable: CSSVariableDefinition) => {
143
+ const varRef = generateVarReference(variable);
144
+ onChange(varRef);
145
+ setIsOpen(false);
146
+ setSearchQuery('');
147
+ setIsCreating(false);
148
+ }, [onChange]);
149
+
150
+ // 現在の値から変数を作成
151
+ const handleCreateVariable = useCallback(() => {
152
+ if (!newVarName.trim() || !category || !addVariable) return;
153
+
154
+ // 編集された値を使用(空の場合は現在の値を使う)
155
+ let finalValue = newVarValue.trim();
156
+ if (!finalValue) {
157
+ if (typeof value === 'number') {
158
+ finalValue = suffix ? `${value}${suffix}` : `${value}px`;
159
+ } else if (isLinked) {
160
+ return;
161
+ } else {
162
+ finalValue = String(value);
163
+ if (numericOnly && !isNaN(parseFloat(finalValue)) && !finalValue.includes('px') && !finalValue.includes('%')) {
164
+ finalValue = suffix ? `${finalValue}${suffix}` : `${finalValue}px`;
165
+ }
166
+ }
167
+ }
168
+
169
+ // 変数を作成
170
+ const newVar = addVariable(newVarName.trim(), finalValue, category);
171
+
172
+ // 作成した変数を選択
173
+ const varRef = generateVarReference(newVar);
174
+ onChange(varRef);
175
+
176
+ // UIをリセット
177
+ setNewVarName('');
178
+ setNewVarValue('');
179
+ setIsCreating(false);
180
+ setIsOpen(false);
181
+ }, [newVarName, newVarValue, value, suffix, category, addVariable, onChange, isLinked, numericOnly]);
182
+
183
+ // 変数作成UIを開く
184
+ const handleStartCreating = useCallback(() => {
185
+ setIsCreating(true);
186
+ setSearchQuery('');
187
+ // 現在の値を初期値として設定
188
+ if (typeof value === 'number') {
189
+ setNewVarValue(suffix ? `${value}${suffix}` : `${value}px`);
190
+ } else {
191
+ setNewVarValue(String(value));
192
+ }
193
+ // 次のフレームでフォーカス
194
+ setTimeout(() => createInputRef.current?.focus(), 0);
195
+ }, [value, suffix]);
196
+
197
+ // 変数作成をキャンセル
198
+ const handleCancelCreating = useCallback(() => {
199
+ setIsCreating(false);
200
+ setNewVarName('');
201
+ setNewVarValue('');
202
+ }, []);
203
+
204
+ // リンク解除
205
+ const handleDetach = useCallback((e: React.MouseEvent) => {
206
+ e.preventDefault();
207
+ e.stopPropagation();
208
+
209
+ if (linkedVariable) {
210
+ // リンクされた変数の実際の値を使用
211
+ // 値が既に単位を含んでいる場合(例: "16px")は数値部分のみ抽出
212
+ let resolvedValue = linkedVariable.value;
213
+ if (numericOnly) {
214
+ const numMatch = resolvedValue.match(/^([\d.]+)/);
215
+ if (numMatch) {
216
+ resolvedValue = numMatch[1];
217
+ }
218
+ }
219
+ onChange(resolvedValue);
220
+ } else {
221
+ // 変数が見つからない場合(削除された等)はデフォルト値を使用
222
+ const defaultValue = numericOnly ? (min !== undefined ? String(min) : '0') : '';
223
+ onChange(defaultValue);
224
+ }
225
+ setIsOpen(false);
226
+ }, [linkedVariable, onChange, numericOnly, min]);
227
+
228
+ // 入力値の変更
229
+ const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
230
+ const newValue = e.target.value;
231
+ setLocalValue(newValue);
232
+
233
+ if (numericOnly) {
234
+ const num = parseFloat(newValue);
235
+ if (!isNaN(num)) {
236
+ let clampedValue = num;
237
+ if (min !== undefined) clampedValue = Math.max(min, clampedValue);
238
+ if (max !== undefined) clampedValue = Math.min(max, clampedValue);
239
+ onChange(String(clampedValue));
240
+ }
241
+ } else {
242
+ onChange(newValue);
243
+ }
244
+ }, [onChange, numericOnly, min, max]);
245
+
246
+ // キーボード操作
247
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
248
+ if (e.key === 'Escape') {
249
+ setIsOpen(false);
250
+ } else if (e.key === 'ArrowDown' && !isOpen) {
251
+ setIsOpen(true);
252
+ } else if (numericOnly && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
253
+ e.preventDefault();
254
+ const currentNum = parseFloat(localValue) || 0;
255
+ const step = e.shiftKey ? 10 : 1;
256
+ const delta = e.key === 'ArrowUp' ? step : -step;
257
+ let newVal = currentNum + delta;
258
+ if (min !== undefined) newVal = Math.max(min, newVal);
259
+ if (max !== undefined) newVal = Math.min(max, newVal);
260
+ setLocalValue(String(newVal));
261
+ onChange(String(newVal));
262
+ }
263
+ }, [isOpen, numericOnly, localValue, min, max, onChange]);
264
+
265
+ // Scrubbing handler (horizontal drag to change value)
266
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
267
+ if (disabled || isLinked || !numericOnly) return;
268
+ // Don't start scrubbing if clicking on the dropdown button
269
+ if ((e.target as HTMLElement).closest('button')) return;
270
+
271
+ e.preventDefault();
272
+ isScrubbing.current = true;
273
+ hasMoved.current = false;
274
+ startX.current = e.clientX;
275
+ startValue.current = parseFloat(localValue) || 0;
276
+
277
+ document.body.style.cursor = 'ew-resize';
278
+
279
+ const handleMouseMove = (mmEvent: MouseEvent) => {
280
+ if (!isScrubbing.current) return;
281
+
282
+ const delta = mmEvent.clientX - startX.current;
283
+ if (Math.abs(delta) < 3) return;
284
+
285
+ hasMoved.current = true;
286
+
287
+ let multiplier = 1;
288
+ if (mmEvent.shiftKey) multiplier = 10;
289
+ if (mmEvent.altKey) multiplier = 0.1;
290
+
291
+ let newVal = startValue.current + Math.round(delta * multiplier);
292
+ if (min !== undefined) newVal = Math.max(min, newVal);
293
+ if (max !== undefined) newVal = Math.min(max, newVal);
294
+
295
+ setLocalValue(String(newVal));
296
+ onChange(String(newVal));
297
+ };
298
+
299
+ const handleMouseUp = () => {
300
+ const didMove = hasMoved.current;
301
+ isScrubbing.current = false;
302
+ hasMoved.current = false;
303
+ document.body.style.cursor = '';
304
+ window.removeEventListener('mousemove', handleMouseMove);
305
+ window.removeEventListener('mouseup', handleMouseUp);
306
+
307
+ // If didn't move, focus the input for editing
308
+ if (!didMove) {
309
+ setTimeout(() => {
310
+ inputRef.current?.focus();
311
+ inputRef.current?.select();
312
+ }, 0);
313
+ }
314
+ };
315
+
316
+ window.addEventListener('mousemove', handleMouseMove);
317
+ window.addEventListener('mouseup', handleMouseUp);
318
+ }, [disabled, isLinked, numericOnly, localValue, min, max, onChange]);
319
+
320
+ return (
321
+ <Popover open={isOpen} onOpenChange={setIsOpen}>
322
+ <PopoverTrigger asChild>
323
+ <div
324
+ ref={containerRef}
325
+ onMouseDown={handleMouseDown}
326
+ className={cn(
327
+ 'relative flex items-center bg-[#383838] border border-[#444444] rounded text-xs group/var-input overflow-hidden',
328
+ 'hover:border-[#5d5d5d] focus-within:border-[#0d99ff] focus-within:ring-1 focus-within:ring-[#0d99ff]',
329
+ disabled && 'opacity-50 cursor-not-allowed',
330
+ !disabled && !isLinked && numericOnly && 'cursor-ew-resize',
331
+ compact ? 'h-5' : 'h-7',
332
+ 'min-w-0 w-full',
333
+ className
334
+ )}
335
+ >
336
+ {/* ラベル */}
337
+ {label && (
338
+ <span className={cn(
339
+ "text-gray-500 select-none flex-shrink-0",
340
+ compact ? "px-1 text-[10px]" : "px-2"
341
+ )}>
342
+ {label}
343
+ </span>
344
+ )}
345
+
346
+ {/* 入力またはリンク表示 */}
347
+ {isLinked ? (
348
+ <div className={cn(
349
+ "flex-1 flex items-center min-w-0 overflow-hidden",
350
+ compact ? "px-0.5" : "px-1"
351
+ )}>
352
+ <div className={cn(
353
+ "flex items-center gap-0.5 bg-[#2a2a2a] border border-[#4a4a4a] rounded min-w-0 max-w-full overflow-hidden",
354
+ compact ? "px-1 py-0" : "px-1.5 py-0.5"
355
+ )}>
356
+ <span className={cn(
357
+ "text-white truncate font-medium min-w-0",
358
+ compact ? "text-[9px] max-w-[40px]" : "text-[10px]"
359
+ )}>
360
+ {linkedVariable?.name || 'Unknown'}
361
+ </span>
362
+ <button
363
+ type="button"
364
+ onClick={(e) => {
365
+ e.preventDefault();
366
+ e.stopPropagation();
367
+ handleDetach(e);
368
+ }}
369
+ onMouseDown={(e) => {
370
+ e.preventDefault();
371
+ e.stopPropagation();
372
+ }}
373
+ onPointerDown={(e) => {
374
+ e.preventDefault();
375
+ e.stopPropagation();
376
+ }}
377
+ className={cn(
378
+ "hover:text-red-400 transition-colors flex-shrink-0 flex items-center justify-center",
379
+ compact ? "p-0.5 -m-0.5" : "p-1 -m-0.5"
380
+ )}
381
+ title="変数を解除"
382
+ >
383
+ <X className={cn(
384
+ "text-gray-400 hover:text-red-400",
385
+ compact ? "w-2.5 h-2.5" : "w-3 h-3"
386
+ )} />
387
+ </button>
388
+ </div>
389
+ </div>
390
+ ) : (
391
+ <input
392
+ ref={inputRef}
393
+ type={numericOnly ? 'number' : 'text'}
394
+ value={localValue}
395
+ onChange={handleInputChange}
396
+ onKeyDown={handleKeyDown}
397
+ placeholder={placeholder}
398
+ disabled={disabled}
399
+ className={cn(
400
+ 'flex-1 bg-transparent border-none outline-none text-white min-w-0 w-full',
401
+ '[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',
402
+ compact ? "text-[10px] px-1" : "text-xs px-2"
403
+ )}
404
+ />
405
+ )}
406
+
407
+ {/* サフィックスまたはドロップダウンアイコン */}
408
+ {suffix && !isLinked && (
409
+ <span className={cn(
410
+ "text-gray-500 select-none flex-shrink-0",
411
+ compact ? "px-0.5 text-[9px]" : "px-1.5"
412
+ )}>
413
+ {suffix}
414
+ </span>
415
+ )}
416
+
417
+ {/* 変数インジケーター - コンパクトモードではホバー時のみ表示 */}
418
+ <button
419
+ type="button"
420
+ onClick={(e) => {
421
+ e.preventDefault();
422
+ e.stopPropagation();
423
+ setIsOpen(true);
424
+ }}
425
+ className={cn(
426
+ 'h-full flex items-center justify-center flex-shrink-0 text-gray-400',
427
+ 'hover:bg-[#4a4a4a] rounded-r transition-colors',
428
+ isLinked && 'text-purple-400',
429
+ compact ? 'px-0.5 opacity-0 group-hover/var-input:opacity-100' : 'px-1'
430
+ )}
431
+ title="CSS変数を選択"
432
+ >
433
+ <ChevronDown className={compact ? "w-2 h-2" : "w-3 h-3"} />
434
+ </button>
435
+ </div>
436
+ </PopoverTrigger>
437
+
438
+ <PopoverContent
439
+ className={cn(
440
+ "p-0 bg-[#2c2c2c] border-[#444444]",
441
+ compact ? "w-48" : "w-64"
442
+ )}
443
+ align="start"
444
+ side="bottom"
445
+ sideOffset={4}
446
+ >
447
+ {/* 検索 */}
448
+ <div className="p-2 border-b border-[#444444]">
449
+ <div className="relative">
450
+ <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-500" />
451
+ <Input
452
+ type="text"
453
+ placeholder="検索..."
454
+ value={searchQuery}
455
+ onChange={(e) => setSearchQuery(e.target.value)}
456
+ className="h-7 pl-7 text-xs bg-[#383838] border-[#444444] text-white placeholder:text-gray-500"
457
+ autoFocus
458
+ />
459
+ </div>
460
+ </div>
461
+
462
+ {/* カテゴリフィルター */}
463
+ <div className="px-2 py-1.5 border-b border-[#444444] flex items-center justify-between">
464
+ <span className="text-[10px] text-gray-500 uppercase">
465
+ {category ? `${category} 変数` : 'すべての変数'}
466
+ </span>
467
+ {/* 変数作成ボタン(カテゴリがある場合のみ) */}
468
+ {category && !isCreating && !isLinked && (
469
+ <button
470
+ type="button"
471
+ onClick={handleStartCreating}
472
+ className="flex items-center gap-1 px-1.5 py-0.5 text-[10px] text-gray-400 hover:text-white hover:bg-[#444444] rounded transition-colors"
473
+ title="現在の値から変数を作成"
474
+ >
475
+ <Plus className="w-3 h-3" />
476
+ <span>作成</span>
477
+ </button>
478
+ )}
479
+ </div>
480
+
481
+ {/* 変数作成UI */}
482
+ {isCreating && category && (
483
+ <div className="p-2 border-b border-[#444444] bg-[#2a2a2a]">
484
+ <div className="text-[10px] text-gray-400 mb-1.5">新しい変数を作成</div>
485
+ <div className="space-y-2">
486
+ {/* 変数名入力 */}
487
+ <div className="flex items-center gap-1.5">
488
+ <span className="text-[10px] text-gray-500 w-8 flex-shrink-0">名前</span>
489
+ <Input
490
+ ref={createInputRef}
491
+ type="text"
492
+ placeholder="variable-name"
493
+ value={newVarName}
494
+ onChange={(e) => setNewVarName(e.target.value)}
495
+ onKeyDown={(e) => {
496
+ if (e.key === 'Enter') {
497
+ handleCreateVariable();
498
+ } else if (e.key === 'Escape') {
499
+ handleCancelCreating();
500
+ }
501
+ }}
502
+ className="h-6 text-[10px] bg-[#383838] border-[#444444] text-white placeholder:text-gray-600 flex-1"
503
+ />
504
+ </div>
505
+ {/* 値を編集 */}
506
+ <div className="flex items-center gap-1.5">
507
+ <span className="text-[10px] text-gray-500 w-8 flex-shrink-0">値</span>
508
+ <Input
509
+ type="text"
510
+ placeholder="16px"
511
+ value={newVarValue}
512
+ onChange={(e) => setNewVarValue(e.target.value)}
513
+ onKeyDown={(e) => {
514
+ if (e.key === 'Enter') {
515
+ handleCreateVariable();
516
+ } else if (e.key === 'Escape') {
517
+ handleCancelCreating();
518
+ }
519
+ }}
520
+ className="h-6 text-[10px] bg-[#383838] border-[#444444] text-white placeholder:text-gray-600 flex-1 font-mono"
521
+ />
522
+ </div>
523
+ {/* 生成されるCSS変数名のプレビュー */}
524
+ {newVarName.trim() && (
525
+ <div className="text-[9px] text-gray-500 font-mono">
526
+ --{category}-{newVarName.trim().toLowerCase().replace(/\s+/g, '-')}
527
+ </div>
528
+ )}
529
+ {/* ボタン */}
530
+ <div className="flex items-center gap-2 pt-1">
531
+ <Button
532
+ type="button"
533
+ size="sm"
534
+ onClick={handleCreateVariable}
535
+ disabled={!newVarName.trim()}
536
+ className="h-6 px-3 text-[10px] bg-purple-600 hover:bg-purple-700 text-white"
537
+ >
538
+ 作成
539
+ </Button>
540
+ <Button
541
+ type="button"
542
+ size="sm"
543
+ variant="ghost"
544
+ onClick={handleCancelCreating}
545
+ className="h-6 px-3 text-[10px] text-gray-400 hover:text-white hover:bg-[#444444]"
546
+ >
547
+ キャンセル
548
+ </Button>
549
+ </div>
550
+ </div>
551
+ </div>
552
+ )}
553
+
554
+ {/* 変数リスト */}
555
+ <ScrollArea className="max-h-48">
556
+ {filteredVariables.length === 0 ? (
557
+ <div className="p-4 text-center text-xs text-gray-500">
558
+ {searchQuery ? '検索結果がありません' : '変数がありません'}
559
+ </div>
560
+ ) : (
561
+ <div className="p-1">
562
+ {filteredVariables.map((variable) => {
563
+ const isSelected = linkedVariable?.id === variable.id;
564
+
565
+ return (
566
+ <button
567
+ type="button"
568
+ key={variable.id}
569
+ onClick={(e) => {
570
+ e.preventDefault();
571
+ e.stopPropagation();
572
+ handleSelectVariable(variable);
573
+ }}
574
+ className={cn(
575
+ 'w-full flex items-center justify-between gap-2 px-2 py-1.5 rounded text-left text-white',
576
+ 'hover:bg-[#444444] transition-colors',
577
+ isSelected && 'bg-purple-500/20'
578
+ )}
579
+ >
580
+ <div className="flex items-center gap-2 min-w-0">
581
+ {/* カラープレビュー */}
582
+ {variable.category === 'color' && (
583
+ <div
584
+ className="w-3 h-3 rounded border border-[#5d5d5d] flex-shrink-0"
585
+ style={{ backgroundColor: variable.value }}
586
+ />
587
+ )}
588
+ <span className="text-xs text-white truncate">
589
+ {variable.name}
590
+ </span>
591
+ </div>
592
+ <span className="text-[10px] text-gray-500 font-mono flex-shrink-0">
593
+ {variable.value}
594
+ </span>
595
+ </button>
596
+ );
597
+ })}
598
+ </div>
599
+ )}
600
+ </ScrollArea>
601
+ </PopoverContent>
602
+ </Popover>
603
+ );
604
+ }
605
+
606
+ export default VariableAwareInput;