@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,546 @@
1
+ "use client";
2
+
3
+ /**
4
+ * VariableAwareUnitInput
5
+ *
6
+ * CSS変数と単位選択の両方をサポートする入力コンポーネント(ダークテーマ)
7
+ * - 単位選択: px, %, em, rem, vw, vh
8
+ * - CSS変数: var(--xxx) 形式での参照
9
+ */
10
+
11
+ import React, { useState, useMemo, useCallback, useRef, useEffect } from "react";
12
+ import {
13
+ Popover,
14
+ PopoverContent,
15
+ PopoverTrigger,
16
+ } from "../../../components/ui/popover";
17
+ import { ScrollArea } from "../../../components/ui/scroll-area";
18
+ import { Button } from "../../../components/ui/button";
19
+ import { Search, X, Plus, Link2, Link2Off } 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
+ import { parseValueWithUnit, formatValueWithUnit, convertUnit, SPACING_UNITS, type UnitConversionContext } from "./unit-utils";
25
+
26
+ // ============================================
27
+ // 型定義
28
+ // ============================================
29
+
30
+ export interface VariableAwareUnitInputProps {
31
+ /** CSS値文字列 (例: "16px", "1.5em", "100%", "var(--spacing)") */
32
+ value: string | number | undefined;
33
+ /** 値変更時のコールバック */
34
+ onChange: (value: string) => void;
35
+ /** 使用可能な単位リスト */
36
+ units?: readonly string[];
37
+ /** デフォルト単位 */
38
+ defaultUnit?: string;
39
+ /** フィルタするカテゴリ */
40
+ category?: CSSVariableCategory | null;
41
+ /** 最小値 */
42
+ min?: number;
43
+ /** 最大値 */
44
+ max?: number;
45
+ /** ステップ値 */
46
+ step?: number;
47
+ /** ラベル */
48
+ label?: string;
49
+ /** コンパクトモード */
50
+ compact?: boolean;
51
+ /** 無効化 */
52
+ disabled?: boolean;
53
+ /** 追加のクラス名 */
54
+ className?: string;
55
+ /** スクラブ機能を有効化 */
56
+ enableScrubbing?: boolean;
57
+ /** 変数リンクボタンを非表示 */
58
+ hideVariableLink?: boolean;
59
+ /** 単位変換コンテキスト */
60
+ conversionContext?: UnitConversionContext;
61
+ }
62
+
63
+ // ============================================
64
+ // メインコンポーネント
65
+ // ============================================
66
+
67
+ export function VariableAwareUnitInput({
68
+ value,
69
+ onChange,
70
+ units = SPACING_UNITS,
71
+ defaultUnit = "px",
72
+ category,
73
+ min,
74
+ max,
75
+ step = 1,
76
+ label,
77
+ compact = false,
78
+ disabled = false,
79
+ className,
80
+ enableScrubbing = true,
81
+ hideVariableLink = false,
82
+ conversionContext,
83
+ }: VariableAwareUnitInputProps) {
84
+ const inputRef = useRef<HTMLInputElement>(null);
85
+ const [isOpen, setIsOpen] = useState(false);
86
+ const [searchQuery, setSearchQuery] = useState("");
87
+ const [isDragging, setIsDragging] = useState(false);
88
+ const [isCreating, setIsCreating] = useState(false);
89
+ const [newVarName, setNewVarName] = useState("");
90
+ // 単位を独立して追跡(valueが数値に変わっても単位を保持)
91
+ const [trackedUnit, setTrackedUnit] = useState<string | null>(null);
92
+ const dragStartRef = useRef<{ x: number; startValue: number } | null>(null);
93
+ const createInputRef = useRef<HTMLInputElement>(null);
94
+ // ドラッグ開始のための移動閾値
95
+ const DRAG_THRESHOLD = 3;
96
+
97
+ // CSS変数を取得
98
+ const { variables, addVariable } = useEditorVariables();
99
+
100
+ // 現在の値がCSS変数参照かどうか
101
+ const isLinked = useMemo(() => {
102
+ return typeof value === "string" && isVariableReference(value);
103
+ }, [value]);
104
+
105
+ // リンクされている変数を取得
106
+ const linkedVariable = useMemo(() => {
107
+ if (!isLinked || typeof value !== "string") return null;
108
+ const varName = extractVariableName(value);
109
+ return variables.find((v) => v.cssName === varName);
110
+ }, [isLinked, value, variables]);
111
+
112
+ // 値をパース(CSS変数以外の場合)
113
+ const parsed = useMemo(() => {
114
+ if (isLinked) {
115
+ return {
116
+ numericValue: 0,
117
+ unit: defaultUnit,
118
+ isSpecial: true,
119
+ originalValue: typeof value === "string" ? value : "",
120
+ };
121
+ }
122
+ const result = parseValueWithUnit(value, defaultUnit);
123
+ // 数値が直接渡された場合、追跡中の単位があればそれを使用
124
+ if (typeof value === 'number' && trackedUnit) {
125
+ return { ...result, unit: trackedUnit };
126
+ }
127
+ return result;
128
+ }, [value, defaultUnit, isLinked, trackedUnit]);
129
+
130
+ // 文字列値から単位を追跡
131
+ useEffect(() => {
132
+ if (typeof value === 'string' && !isLinked) {
133
+ const parsedValue = parseValueWithUnit(value, defaultUnit);
134
+ if (!parsedValue.isSpecial && parsedValue.unit !== trackedUnit) {
135
+ setTrackedUnit(parsedValue.unit);
136
+ }
137
+ }
138
+ }, [value, isLinked, defaultUnit, trackedUnit]);
139
+
140
+ const currentUnit = parsed.isSpecial ? defaultUnit : parsed.unit;
141
+
142
+ // フィルタリングされた変数
143
+ const filteredVariables = useMemo(() => {
144
+ let filtered = variables;
145
+
146
+ if (category) {
147
+ filtered = filtered.filter((v) => v.category === category);
148
+ }
149
+
150
+ if (searchQuery) {
151
+ const query = searchQuery.toLowerCase();
152
+ filtered = filtered.filter(
153
+ (v) =>
154
+ v.name.toLowerCase().includes(query) ||
155
+ v.cssName.toLowerCase().includes(query)
156
+ );
157
+ }
158
+
159
+ return filtered;
160
+ }, [variables, category, searchQuery]);
161
+
162
+ // 数値変更ハンドラ
163
+ const handleNumberChange = useCallback(
164
+ (e: React.ChangeEvent<HTMLInputElement>) => {
165
+ const inputValue = e.target.value;
166
+
167
+ if (inputValue === "" || inputValue === "-") {
168
+ onChange(formatValueWithUnit(0, currentUnit));
169
+ return;
170
+ }
171
+
172
+ const newNum = parseFloat(inputValue);
173
+ if (isNaN(newNum)) return;
174
+
175
+ let clampedValue = newNum;
176
+ if (min !== undefined && newNum < min) clampedValue = min;
177
+ if (max !== undefined && newNum > max) clampedValue = max;
178
+
179
+ onChange(formatValueWithUnit(clampedValue, currentUnit));
180
+ },
181
+ [onChange, currentUnit, min, max]
182
+ );
183
+
184
+ // 単位変更ハンドラ(値を変換)
185
+ const handleUnitChange = useCallback(
186
+ (e: React.ChangeEvent<HTMLSelectElement>) => {
187
+ const newUnit = e.target.value;
188
+
189
+ if (newUnit === "auto") {
190
+ onChange("auto");
191
+ return;
192
+ }
193
+
194
+ // 新しい単位を追跡
195
+ setTrackedUnit(newUnit);
196
+
197
+ if (parsed.isSpecial && !isLinked) {
198
+ onChange(formatValueWithUnit(0, newUnit));
199
+ return;
200
+ }
201
+
202
+ if (isLinked) {
203
+ if (linkedVariable) {
204
+ const numMatch = linkedVariable.value.match(/^([\d.]+)/);
205
+ if (numMatch) {
206
+ const convertedValue = convertUnit(parseFloat(numMatch[1]), currentUnit, newUnit, conversionContext || {});
207
+ onChange(formatValueWithUnit(convertedValue, newUnit));
208
+ return;
209
+ }
210
+ }
211
+ onChange(formatValueWithUnit(0, newUnit));
212
+ return;
213
+ }
214
+
215
+ // 単位変換を適用
216
+ const convertedValue = convertUnit(parsed.numericValue, currentUnit, newUnit, conversionContext || {});
217
+ onChange(formatValueWithUnit(convertedValue, newUnit));
218
+ },
219
+ [onChange, parsed, isLinked, linkedVariable, currentUnit, conversionContext]
220
+ );
221
+
222
+ // 変数を選択
223
+ const handleSelectVariable = useCallback(
224
+ (variable: CSSVariableDefinition) => {
225
+ const varRef = generateVarReference(variable);
226
+ onChange(varRef);
227
+ setIsOpen(false);
228
+ setSearchQuery("");
229
+ setIsCreating(false);
230
+ },
231
+ [onChange]
232
+ );
233
+
234
+ // 変数作成
235
+ const handleCreateVariable = useCallback(() => {
236
+ if (!newVarName.trim() || !category || !addVariable) return;
237
+
238
+ let finalValue: string;
239
+ if (isLinked && linkedVariable) {
240
+ finalValue = linkedVariable.value;
241
+ } else {
242
+ finalValue = formatValueWithUnit(parsed.numericValue, currentUnit);
243
+ }
244
+
245
+ const newVar = addVariable(newVarName.trim(), finalValue, category);
246
+ const varRef = generateVarReference(newVar);
247
+ onChange(varRef);
248
+
249
+ setNewVarName("");
250
+ setIsCreating(false);
251
+ setIsOpen(false);
252
+ }, [newVarName, category, addVariable, isLinked, linkedVariable, parsed, currentUnit, onChange]);
253
+
254
+ // リンク解除
255
+ const handleDetach = useCallback(
256
+ (e: React.MouseEvent) => {
257
+ e.preventDefault();
258
+ e.stopPropagation();
259
+
260
+ if (linkedVariable) {
261
+ const parsedVar = parseValueWithUnit(linkedVariable.value, defaultUnit);
262
+ onChange(formatValueWithUnit(parsedVar.numericValue, parsedVar.unit));
263
+ } else {
264
+ onChange(formatValueWithUnit(0, defaultUnit));
265
+ }
266
+ setIsOpen(false);
267
+ },
268
+ [linkedVariable, onChange, defaultUnit]
269
+ );
270
+
271
+ // スクラブ開始
272
+ // 改善: クリックでフォーカスを許可し、ドラッグ閾値を超えた場合のみスクラブを開始
273
+ const handleMouseDown = useCallback(
274
+ (e: React.MouseEvent<HTMLInputElement>) => {
275
+ if (!enableScrubbing || disabled || isLinked) return;
276
+ if (e.button !== 0) return;
277
+ // 既にフォーカスされている場合は通常の入力を許可
278
+ if (document.activeElement === inputRef.current) return;
279
+
280
+ const startX = e.clientX;
281
+ const startValue = parsed.numericValue;
282
+ let hasDragged = false;
283
+
284
+ const handleMouseMove = (moveEvent: MouseEvent) => {
285
+ const delta = moveEvent.clientX - startX;
286
+
287
+ // ドラッグ閾値を超えるまでは何もしない
288
+ if (!hasDragged && Math.abs(delta) < DRAG_THRESHOLD) {
289
+ return;
290
+ }
291
+
292
+ // 閾値を超えたらドラッグモードに入る
293
+ if (!hasDragged) {
294
+ hasDragged = true;
295
+ setIsDragging(true);
296
+ // ドラッグ開始時にフォーカスを外す
297
+ inputRef.current?.blur();
298
+ }
299
+
300
+ const sensitivity = moveEvent.shiftKey ? 0.1 : 1;
301
+ let newValue = startValue + delta * sensitivity * step;
302
+
303
+ if (min !== undefined && newValue < min) newValue = min;
304
+ if (max !== undefined && newValue > max) newValue = max;
305
+
306
+ const precision = step < 1 ? Math.ceil(-Math.log10(step)) : 0;
307
+ newValue = Math.round(newValue * Math.pow(10, precision)) / Math.pow(10, precision);
308
+
309
+ onChange(formatValueWithUnit(newValue, currentUnit));
310
+ };
311
+
312
+ const handleMouseUp = () => {
313
+ document.removeEventListener("mousemove", handleMouseMove);
314
+ document.removeEventListener("mouseup", handleMouseUp);
315
+
316
+ if (hasDragged) {
317
+ setIsDragging(false);
318
+ } else {
319
+ // ドラッグしなかった場合はinputにフォーカスを当てる
320
+ inputRef.current?.focus();
321
+ inputRef.current?.select();
322
+ }
323
+ };
324
+
325
+ document.addEventListener("mousemove", handleMouseMove);
326
+ document.addEventListener("mouseup", handleMouseUp);
327
+ },
328
+ [enableScrubbing, disabled, isLinked, parsed, step, min, max, currentUnit, onChange]
329
+ );
330
+
331
+ // キーボード操作
332
+ const handleKeyDown = useCallback(
333
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
334
+ if (disabled || isLinked) return;
335
+
336
+ const multiplier = e.shiftKey ? 10 : 1;
337
+ let newValue = parsed.numericValue;
338
+
339
+ if (e.key === "ArrowUp") {
340
+ e.preventDefault();
341
+ newValue += step * multiplier;
342
+ } else if (e.key === "ArrowDown") {
343
+ e.preventDefault();
344
+ newValue -= step * multiplier;
345
+ } else {
346
+ return;
347
+ }
348
+
349
+ if (min !== undefined && newValue < min) newValue = min;
350
+ if (max !== undefined && newValue > max) newValue = max;
351
+
352
+ onChange(formatValueWithUnit(newValue, currentUnit));
353
+ },
354
+ [disabled, isLinked, parsed, step, min, max, currentUnit, onChange]
355
+ );
356
+
357
+ const inputWidth = compact ? "w-10" : "w-12";
358
+ const selectWidth = "w-9";
359
+
360
+ return (
361
+ <div className={cn("flex items-center gap-0.5", className)}>
362
+ {label && (
363
+ <span className="text-[10px] text-gray-500 w-5 flex-shrink-0">{label}</span>
364
+ )}
365
+
366
+ <div className="flex items-center relative">
367
+ {/* 変数リンクボタン */}
368
+ {!hideVariableLink && (
369
+ <Popover open={isOpen} onOpenChange={setIsOpen}>
370
+ <PopoverTrigger asChild>
371
+ <button
372
+ type="button"
373
+ disabled={disabled}
374
+ className={cn(
375
+ "h-5 w-4 flex items-center justify-center border border-r-0 rounded-l border-[#4a4a4a]",
376
+ isLinked
377
+ ? "bg-purple-900/50 text-purple-400"
378
+ : "bg-[#383838] text-gray-500 hover:text-gray-300 hover:bg-[#4a4a4a]",
379
+ disabled && "opacity-50 cursor-not-allowed"
380
+ )}
381
+ >
382
+ {isLinked ? <Link2 className="h-2.5 w-2.5" /> : <Link2Off className="h-2.5 w-2.5" />}
383
+ </button>
384
+ </PopoverTrigger>
385
+
386
+ <PopoverContent className="w-56 p-0 bg-[#2a2a2a] border-[#4a4a4a]" align="start">
387
+ <div className="p-2 border-b border-[#4a4a4a]">
388
+ <div className="relative">
389
+ <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-gray-500" />
390
+ <input
391
+ type="text"
392
+ placeholder="変数を検索..."
393
+ value={searchQuery}
394
+ onChange={(e) => setSearchQuery(e.target.value)}
395
+ className="w-full pl-7 pr-2 py-1 text-xs border border-[#4a4a4a] rounded bg-[#383838] text-gray-200 placeholder:text-gray-500 focus:outline-none focus:border-[#0d99ff]"
396
+ />
397
+ </div>
398
+ </div>
399
+
400
+ {isCreating ? (
401
+ <div className="p-2 space-y-2">
402
+ <input
403
+ ref={createInputRef}
404
+ type="text"
405
+ placeholder="変数名を入力..."
406
+ value={newVarName}
407
+ onChange={(e) => setNewVarName(e.target.value)}
408
+ onKeyDown={(e) => {
409
+ if (e.key === "Enter") handleCreateVariable();
410
+ if (e.key === "Escape") setIsCreating(false);
411
+ }}
412
+ className="w-full px-2 py-1 text-xs border border-[#4a4a4a] rounded bg-[#383838] text-gray-200 placeholder:text-gray-500 focus:outline-none focus:border-[#0d99ff]"
413
+ autoFocus
414
+ />
415
+ <div className="flex justify-end gap-1">
416
+ <Button
417
+ size="sm"
418
+ variant="ghost"
419
+ onClick={() => setIsCreating(false)}
420
+ className="h-5 text-[10px] text-gray-400 hover:text-white hover:bg-[#4a4a4a]"
421
+ >
422
+ キャンセル
423
+ </Button>
424
+ <Button
425
+ size="sm"
426
+ onClick={handleCreateVariable}
427
+ disabled={!newVarName.trim()}
428
+ className="h-5 text-[10px] bg-[#0d99ff] hover:bg-[#0c8ce9] text-white"
429
+ >
430
+ 作成
431
+ </Button>
432
+ </div>
433
+ </div>
434
+ ) : (
435
+ <>
436
+ <ScrollArea className="max-h-40">
437
+ <div className="p-1">
438
+ {isLinked && linkedVariable && (
439
+ <button
440
+ onClick={handleDetach}
441
+ className="w-full flex items-center gap-2 px-2 py-1 text-[10px] rounded hover:bg-red-900/30 text-red-400"
442
+ >
443
+ <X className="h-3 w-3" />
444
+ リンクを解除
445
+ </button>
446
+ )}
447
+
448
+ {filteredVariables.length === 0 ? (
449
+ <div className="px-2 py-3 text-[10px] text-gray-500 text-center">
450
+ 変数が見つかりません
451
+ </div>
452
+ ) : (
453
+ filteredVariables.map((variable) => (
454
+ <button
455
+ key={variable.id}
456
+ onClick={() => handleSelectVariable(variable)}
457
+ className={cn(
458
+ "w-full flex items-center justify-between px-2 py-1 text-[10px] rounded hover:bg-[#4a4a4a]",
459
+ linkedVariable?.id === variable.id && "bg-purple-900/30"
460
+ )}
461
+ >
462
+ <span className="font-medium text-gray-200">{variable.name}</span>
463
+ <span className="text-gray-500 font-mono text-[9px]">
464
+ {variable.value}
465
+ </span>
466
+ </button>
467
+ ))
468
+ )}
469
+ </div>
470
+ </ScrollArea>
471
+
472
+ {category && (
473
+ <div className="p-1 border-t border-[#4a4a4a]">
474
+ <button
475
+ onClick={() => {
476
+ setIsCreating(true);
477
+ setTimeout(() => createInputRef.current?.focus(), 0);
478
+ }}
479
+ className="w-full flex items-center gap-2 px-2 py-1 text-[10px] rounded hover:bg-[#4a4a4a] text-gray-400"
480
+ >
481
+ <Plus className="h-3 w-3" />
482
+ 新しい変数を作成
483
+ </button>
484
+ </div>
485
+ )}
486
+ </>
487
+ )}
488
+ </PopoverContent>
489
+ </Popover>
490
+ )}
491
+
492
+ {/* 数値入力 */}
493
+ <input
494
+ ref={inputRef}
495
+ type={isLinked ? "text" : "number"}
496
+ value={isLinked ? (linkedVariable?.name ?? extractVariableName(String(value)) ?? "") : parsed.numericValue}
497
+ onChange={handleNumberChange}
498
+ onMouseDown={handleMouseDown}
499
+ onKeyDown={handleKeyDown}
500
+ min={min}
501
+ max={max}
502
+ step={step}
503
+ disabled={disabled || isLinked}
504
+ readOnly={isLinked}
505
+ className={cn(
506
+ `${inputWidth} h-5 px-1 text-[10px] border border-[#4a4a4a]`,
507
+ hideVariableLink ? "rounded-l" : "",
508
+ "bg-[#383838] text-gray-200 focus:outline-none focus:border-[#0d99ff]",
509
+ "disabled:bg-[#2a2a2a] disabled:text-gray-500",
510
+ isDragging ? "cursor-ew-resize" : enableScrubbing && !isLinked ? "cursor-ew-resize" : "",
511
+ isLinked && "bg-purple-900/30 text-purple-300 font-medium text-[9px]",
512
+ "[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
513
+ )}
514
+ style={{ MozAppearance: "textfield" }}
515
+ />
516
+
517
+ {/* 単位選択。
518
+ [移植時の修正] compact(パディング/マージンの十字レイアウト)では隠す。
519
+ 入力9文字幅+単位9文字幅が3列×2ブロック並ぶとパネル幅を超えて中身がはみ出す。
520
+ この用途は事実上pxしか使わないため、単位は固定で十分。 */}
521
+ {!compact && (
522
+ <select
523
+ value={isLinked ? "" : currentUnit}
524
+ onChange={handleUnitChange}
525
+ disabled={disabled || isLinked}
526
+ className={cn(
527
+ `${selectWidth} h-5 px-0.5 text-[9px] border border-l-0 rounded-r border-[#4a4a4a]`,
528
+ "bg-[#4a4a4a] text-gray-300 focus:outline-none focus:border-[#0d99ff]",
529
+ "disabled:bg-[#383838] disabled:text-gray-500 cursor-pointer",
530
+ isLinked && "opacity-50"
531
+ )}
532
+ >
533
+ {isLinked && <option value="">--</option>}
534
+ {units.map((u) => (
535
+ <option key={u} value={u}>
536
+ {u}
537
+ </option>
538
+ ))}
539
+ </select>
540
+ )}
541
+ </div>
542
+ </div>
543
+ );
544
+ }
545
+
546
+ export default VariableAwareUnitInput;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * プロパティパネルコンポーネントのエクスポート
3
+ */
4
+
5
+ export { LayoutSection } from './LayoutSection';
6
+ export { TypographySection } from './TypographySection';
7
+ export { FillSection } from './FillSection';
8
+ export { BorderSection } from './BorderSection';
9
+ export { EffectSection } from './EffectSection';
10
+ export { ImageSection } from './ImageSection';
11
+ export { AlignmentPanel } from './AlignmentPanel';
12
+ export { AutoLayoutPanel } from './AutoLayoutPanel';
13
+ export { CompactNumberInput } from './CompactNumberInput';
14
+ export { CompactSizeInput } from './CompactSizeInput';
15
+ export { LinkSection } from './LinkSection';
16
+ export { PropertyVariablePicker } from './PropertyVariablePicker';
17
+ export { VariableAwareInput } from './VariableAwareInput';
18
+ export { VariableAwareColorInput } from './VariableAwareColorInput';
19
+ export { VariableAwareSizeInput } from './VariableAwareSizeInput';
20
+
21
+ // 単位選択対応コンポーネント
22
+ export { UnitAwareNumberInput } from './UnitAwareNumberInput';
23
+ export { VariableAwareUnitInput } from './VariableAwareUnitInput';
24
+ export * from './unit-utils';