@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,477 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * VariableAwareColorInput
5
+ *
6
+ * Figmaスタイルの変数対応カラー入力コンポーネント
7
+ * カラーピッカーから直接CSS変数を選択可能
8
+ */
9
+
10
+ import { useState, useMemo, useCallback, useRef } 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 { Tabs, TabsList, TabsTrigger, TabsContent } from '../../../components/ui/tabs';
20
+ import { Search, X, Plus, Pipette } from 'lucide-react';
21
+ import { cn } from '../../../lib/utils';
22
+ import { useEditorVariables } from '../../EditorContext';
23
+ import { isEyeDropperSupported, pickScreenColor } from '../../utils/eyedropper';
24
+ import type { CSSVariableDefinition } from '../../../types/css-variables';
25
+ import { isVariableReference, extractVariableName, generateVarReference } from '../../../types/css-variables';
26
+
27
+ // ============================================
28
+ // 型定義
29
+ // ============================================
30
+
31
+ export interface VariableAwareColorInputProps {
32
+ /** ラベル */
33
+ label: string;
34
+ /** 現在の色の値(hex, rgb, または var(--xxx) 形式) */
35
+ value: string;
36
+ /** 値変更時のコールバック */
37
+ onChange: (value: string) => void;
38
+ /** 無効状態 */
39
+ disabled?: boolean;
40
+ /** クラス名 */
41
+ className?: string;
42
+ /** プリセットカラー */
43
+ presetColors?: string[];
44
+ }
45
+
46
+ // RGB を HEX に変換
47
+ const rgbToHex = (rgb: string): string => {
48
+ if (!rgb || rgb === 'transparent' || rgb === 'none') return '#ffffff';
49
+ if (rgb.startsWith('#')) return rgb.slice(0, 7);
50
+ if (rgb.startsWith('var(')) return '#ffffff';
51
+ const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
52
+ if (!match) return '#ffffff';
53
+ const r = parseInt(match[1]).toString(16).padStart(2, '0');
54
+ const g = parseInt(match[2]).toString(16).padStart(2, '0');
55
+ const b = parseInt(match[3]).toString(16).padStart(2, '0');
56
+ return `#${r}${g}${b}`;
57
+ };
58
+
59
+ // ============================================
60
+ // メインコンポーネント
61
+ // ============================================
62
+
63
+ export function VariableAwareColorInput({
64
+ label,
65
+ value,
66
+ onChange,
67
+ disabled = false,
68
+ className,
69
+ presetColors = [],
70
+ }: VariableAwareColorInputProps) {
71
+ const [isOpen, setIsOpen] = useState(false);
72
+ const [searchQuery, setSearchQuery] = useState('');
73
+ const [activeTab, setActiveTab] = useState<'solid' | 'variable'>('solid');
74
+ const [isCreating, setIsCreating] = useState(false);
75
+ const [newVarName, setNewVarName] = useState('');
76
+ const [newVarValue, setNewVarValue] = useState('');
77
+ const createInputRef = useRef<HTMLInputElement>(null);
78
+
79
+ // CSS変数を取得
80
+ const { variables, addVariable } = useEditorVariables();
81
+
82
+ // 現在の値がCSS変数参照かどうか
83
+ const isLinked = useMemo(() => {
84
+ return isVariableReference(value);
85
+ }, [value]);
86
+
87
+ // リンクされている変数を取得
88
+ const linkedVariable = useMemo(() => {
89
+ if (!isLinked) return null;
90
+ const varName = extractVariableName(value);
91
+ return variables.find(v => v.cssName === varName);
92
+ }, [isLinked, value, variables]);
93
+
94
+ // 表示用の色
95
+ const displayColor = useMemo(() => {
96
+ if (isLinked && linkedVariable) {
97
+ return linkedVariable.value;
98
+ }
99
+ return rgbToHex(value);
100
+ }, [isLinked, linkedVariable, value]);
101
+
102
+ // 色変数のみフィルター
103
+ const colorVariables = useMemo(() => {
104
+ let filtered = variables.filter(v => v.category === 'color');
105
+
106
+ if (searchQuery) {
107
+ const query = searchQuery.toLowerCase();
108
+ filtered = filtered.filter(v =>
109
+ v.name.toLowerCase().includes(query) ||
110
+ v.cssName.toLowerCase().includes(query)
111
+ );
112
+ }
113
+
114
+ return filtered;
115
+ }, [variables, searchQuery]);
116
+
117
+ // 変数を選択
118
+ const handleSelectVariable = useCallback((variable: CSSVariableDefinition) => {
119
+ onChange(generateVarReference(variable));
120
+ setIsOpen(false);
121
+ setSearchQuery('');
122
+ }, [onChange]);
123
+
124
+ // リンク解除
125
+ const handleDetach = useCallback((e: React.MouseEvent) => {
126
+ e.stopPropagation();
127
+ if (linkedVariable) {
128
+ onChange(linkedVariable.value);
129
+ } else {
130
+ // 変数が見つからない場合はデフォルトの白色を使用
131
+ onChange('#ffffff');
132
+ }
133
+ }, [linkedVariable, onChange]);
134
+
135
+ // 色を直接選択
136
+ const handleColorChange = useCallback((color: string) => {
137
+ onChange(color);
138
+ }, [onChange]);
139
+
140
+ // スポイト(画面から色を拾う)。取り消し時は null が返るので何もしない
141
+ const eyeDropperSupported = isEyeDropperSupported();
142
+ const handleEyeDropper = useCallback(async () => {
143
+ const hex = await pickScreenColor();
144
+ if (hex) handleColorChange(hex);
145
+ }, [handleColorChange]);
146
+
147
+ // 現在の色から変数を作成
148
+ const handleCreateVariable = useCallback(() => {
149
+ if (!newVarName.trim() || !addVariable) return;
150
+
151
+ // 既に変数がリンクされている場合は作成しない
152
+ if (isLinked) return;
153
+
154
+ // 編集された値を使用(空の場合は現在の色を使う)
155
+ const finalValue = newVarValue.trim() || displayColor;
156
+
157
+ // 変数を作成(カテゴリはcolor)
158
+ const newVar = addVariable(newVarName.trim(), finalValue, 'color');
159
+
160
+ // 作成した変数を選択
161
+ const varRef = generateVarReference(newVar);
162
+ onChange(varRef);
163
+
164
+ // UIをリセット
165
+ setNewVarName('');
166
+ setNewVarValue('');
167
+ setIsCreating(false);
168
+ setIsOpen(false);
169
+ }, [newVarName, newVarValue, addVariable, onChange, isLinked, displayColor]);
170
+
171
+ // 変数作成UIを開く
172
+ const handleStartCreating = useCallback(() => {
173
+ setIsCreating(true);
174
+ setSearchQuery('');
175
+ // 現在の色を初期値として設定
176
+ setNewVarValue(displayColor);
177
+ setTimeout(() => createInputRef.current?.focus(), 0);
178
+ }, [displayColor]);
179
+
180
+ // 変数作成をキャンセル
181
+ const handleCancelCreating = useCallback(() => {
182
+ setIsCreating(false);
183
+ setNewVarName('');
184
+ setNewVarValue('');
185
+ }, []);
186
+
187
+ return (
188
+ <Popover open={isOpen} onOpenChange={setIsOpen}>
189
+ <PopoverTrigger asChild>
190
+ <button
191
+ disabled={disabled}
192
+ className={cn(
193
+ 'w-full flex items-center gap-2 px-2 py-1.5 rounded',
194
+ 'bg-[#383838] border border-[#444444] hover:border-[#5d5d5d]',
195
+ 'text-xs text-left transition-colors',
196
+ isLinked && 'border-purple-500/50',
197
+ disabled && 'opacity-50 cursor-not-allowed',
198
+ className
199
+ )}
200
+ >
201
+ {/* カラースウォッチ */}
202
+ <div
203
+ className="w-5 h-5 rounded border border-[#5d5d5d] flex-shrink-0"
204
+ style={{ backgroundColor: displayColor }}
205
+ />
206
+
207
+ {/* 値表示 */}
208
+ <div className="flex-1 min-w-0">
209
+ <div className="text-[10px] text-gray-500 mb-0.5">{label}</div>
210
+ {isLinked ? (
211
+ <div className="flex items-center gap-1">
212
+ <span className="text-purple-300 truncate text-xs">
213
+ {linkedVariable?.name || 'Unknown'}
214
+ </span>
215
+ <button
216
+ type="button"
217
+ onClick={(e) => {
218
+ e.preventDefault();
219
+ e.stopPropagation();
220
+ handleDetach(e);
221
+ }}
222
+ onMouseDown={(e) => {
223
+ e.preventDefault();
224
+ e.stopPropagation();
225
+ }}
226
+ onPointerDown={(e) => {
227
+ e.preventDefault();
228
+ e.stopPropagation();
229
+ }}
230
+ className="p-0.5 hover:bg-purple-500/20 rounded flex-shrink-0"
231
+ title="変数を解除"
232
+ >
233
+ <X className="w-3 h-3 text-purple-400" />
234
+ </button>
235
+ </div>
236
+ ) : (
237
+ <span className="text-white text-xs uppercase">
238
+ {displayColor}
239
+ </span>
240
+ )}
241
+ </div>
242
+ </button>
243
+ </PopoverTrigger>
244
+
245
+ <PopoverContent
246
+ className="w-64 p-0 bg-[#2c2c2c] border-[#444444]"
247
+ align="start"
248
+ side="left"
249
+ sideOffset={8}
250
+ >
251
+ <Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as 'solid' | 'variable')}>
252
+ <TabsList className="w-full grid grid-cols-2 bg-[#1e1e1e] p-1 gap-1">
253
+ <TabsTrigger
254
+ value="solid"
255
+ className="text-xs data-[state=active]:bg-[#383838] data-[state=active]:text-white"
256
+ >
257
+ カラー
258
+ </TabsTrigger>
259
+ <TabsTrigger
260
+ value="variable"
261
+ className="text-xs data-[state=active]:bg-[#383838] data-[state=active]:text-white"
262
+ >
263
+ 変数
264
+ </TabsTrigger>
265
+ </TabsList>
266
+
267
+ {/* カラータブ */}
268
+ <TabsContent value="solid" className="p-3 space-y-3">
269
+ {/* カラーピッカー */}
270
+ <div>
271
+ <input
272
+ type="color"
273
+ value={displayColor}
274
+ onChange={(e) => handleColorChange(e.target.value)}
275
+ className="w-full h-24 rounded cursor-pointer border border-[#444444]"
276
+ />
277
+ </div>
278
+
279
+ {/* HEX入力 + スポイト(未対応ブラウザではボタンを出さない) */}
280
+ <div className="flex items-center gap-2">
281
+ {eyeDropperSupported && (
282
+ <Button
283
+ variant="outline"
284
+ size="icon"
285
+ className="h-7 w-7 shrink-0 p-0 border-[#444444] bg-[#383838] hover:bg-[#4a4a4a]"
286
+ onClick={handleEyeDropper}
287
+ title="スポイト (画面から色を取得)"
288
+ >
289
+ <Pipette className="w-3.5 h-3.5 text-gray-300" />
290
+ </Button>
291
+ )}
292
+ <Input
293
+ type="text"
294
+ value={displayColor}
295
+ onChange={(e) => handleColorChange(e.target.value)}
296
+ className="h-7 text-xs bg-[#383838] border-[#444444] text-white font-mono"
297
+ />
298
+ </div>
299
+
300
+ {/* プリセット */}
301
+ {presetColors.length > 0 && (
302
+ <div>
303
+ <div className="text-[10px] text-gray-500 mb-1.5">プリセット</div>
304
+ <div className="grid grid-cols-8 gap-1">
305
+ {presetColors.slice(0, 16).map((color) => (
306
+ <button
307
+ key={color}
308
+ onClick={() => handleColorChange(color)}
309
+ className={cn(
310
+ 'w-5 h-5 rounded border border-[#5d5d5d] hover:scale-110 transition-transform',
311
+ displayColor === color && 'ring-2 ring-[#0d99ff]'
312
+ )}
313
+ style={{ backgroundColor: color }}
314
+ title={color}
315
+ />
316
+ ))}
317
+ </div>
318
+ </div>
319
+ )}
320
+ </TabsContent>
321
+
322
+ {/* 変数タブ */}
323
+ <TabsContent value="variable" className="p-0">
324
+ {/* 検索 */}
325
+ <div className="p-2 border-b border-[#444444]">
326
+ <div className="relative">
327
+ <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-500" />
328
+ <Input
329
+ type="text"
330
+ placeholder="検索..."
331
+ value={searchQuery}
332
+ onChange={(e) => setSearchQuery(e.target.value)}
333
+ className="h-7 pl-7 text-xs bg-[#383838] border-[#444444] text-white placeholder:text-gray-500"
334
+ />
335
+ </div>
336
+ </div>
337
+
338
+ {/* カテゴリヘッダーと作成ボタン */}
339
+ <div className="px-2 py-1.5 border-b border-[#444444] flex items-center justify-between">
340
+ <span className="text-[10px] text-gray-500 uppercase">カラー変数</span>
341
+ {!isCreating && !isLinked && (
342
+ <button
343
+ type="button"
344
+ onClick={handleStartCreating}
345
+ 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"
346
+ title="現在の色から変数を作成"
347
+ >
348
+ <Plus className="w-3 h-3" />
349
+ <span>作成</span>
350
+ </button>
351
+ )}
352
+ </div>
353
+
354
+ {/* 変数作成UI */}
355
+ {isCreating && (
356
+ <div className="p-2 border-b border-[#444444] bg-[#2a2a2a]">
357
+ <div className="text-[10px] text-gray-400 mb-1.5">新しい変数を作成</div>
358
+ <div className="space-y-2">
359
+ {/* 変数名入力 */}
360
+ <div className="flex items-center gap-1.5">
361
+ <span className="text-[10px] text-gray-500 w-8 flex-shrink-0">名前</span>
362
+ <Input
363
+ ref={createInputRef}
364
+ type="text"
365
+ placeholder="variable-name"
366
+ value={newVarName}
367
+ onChange={(e) => setNewVarName(e.target.value)}
368
+ onKeyDown={(e) => {
369
+ if (e.key === 'Enter') {
370
+ handleCreateVariable();
371
+ } else if (e.key === 'Escape') {
372
+ handleCancelCreating();
373
+ }
374
+ }}
375
+ className="h-6 text-[10px] bg-[#383838] border-[#444444] text-white placeholder:text-gray-600 flex-1"
376
+ />
377
+ </div>
378
+ {/* 値を編集 */}
379
+ <div className="flex items-center gap-1.5">
380
+ <span className="text-[10px] text-gray-500 w-8 flex-shrink-0">値</span>
381
+ <div className="flex-1 flex items-center gap-1">
382
+ <div
383
+ className="w-6 h-6 rounded border border-[#5d5d5d] flex-shrink-0"
384
+ style={{ backgroundColor: newVarValue || displayColor }}
385
+ />
386
+ <Input
387
+ type="text"
388
+ placeholder="#ffffff"
389
+ value={newVarValue}
390
+ onChange={(e) => setNewVarValue(e.target.value)}
391
+ onKeyDown={(e) => {
392
+ if (e.key === 'Enter') {
393
+ handleCreateVariable();
394
+ } else if (e.key === 'Escape') {
395
+ handleCancelCreating();
396
+ }
397
+ }}
398
+ className="h-6 text-[10px] bg-[#383838] border-[#444444] text-white placeholder:text-gray-600 flex-1 font-mono"
399
+ />
400
+ </div>
401
+ </div>
402
+ {/* 生成されるCSS変数名のプレビュー */}
403
+ {newVarName.trim() && (
404
+ <div className="text-[9px] text-gray-500 font-mono">
405
+ --color-{newVarName.trim().toLowerCase().replace(/\s+/g, '-')}
406
+ </div>
407
+ )}
408
+ {/* ボタン */}
409
+ <div className="flex items-center gap-2 pt-1">
410
+ <Button
411
+ type="button"
412
+ size="sm"
413
+ onClick={handleCreateVariable}
414
+ disabled={!newVarName.trim()}
415
+ className="h-6 px-3 text-[10px] bg-purple-600 hover:bg-purple-700 text-white"
416
+ >
417
+ 作成
418
+ </Button>
419
+ <Button
420
+ type="button"
421
+ size="sm"
422
+ variant="ghost"
423
+ onClick={handleCancelCreating}
424
+ className="h-6 px-3 text-[10px] text-gray-400 hover:text-white hover:bg-[#444444]"
425
+ >
426
+ キャンセル
427
+ </Button>
428
+ </div>
429
+ </div>
430
+ </div>
431
+ )}
432
+
433
+ {/* 変数リスト */}
434
+ <ScrollArea className="max-h-48">
435
+ {colorVariables.length === 0 ? (
436
+ <div className="p-4 text-center text-xs text-gray-500">
437
+ {searchQuery ? '検索結果がありません' : 'カラー変数がありません'}
438
+ </div>
439
+ ) : (
440
+ <div className="p-1">
441
+ {colorVariables.map((variable) => {
442
+ const isSelected = linkedVariable?.id === variable.id;
443
+
444
+ return (
445
+ <button
446
+ key={variable.id}
447
+ onClick={() => handleSelectVariable(variable)}
448
+ className={cn(
449
+ 'w-full flex items-center gap-2 px-2 py-1.5 rounded text-left',
450
+ 'hover:bg-[#444444] transition-colors',
451
+ isSelected && 'bg-purple-500/20'
452
+ )}
453
+ >
454
+ <div
455
+ className="w-4 h-4 rounded border border-[#5d5d5d] flex-shrink-0"
456
+ style={{ backgroundColor: variable.value }}
457
+ />
458
+ <span className="text-xs text-white truncate flex-1">
459
+ {variable.name}
460
+ </span>
461
+ <span className="text-[10px] text-gray-500 font-mono">
462
+ {variable.value}
463
+ </span>
464
+ </button>
465
+ );
466
+ })}
467
+ </div>
468
+ )}
469
+ </ScrollArea>
470
+ </TabsContent>
471
+ </Tabs>
472
+ </PopoverContent>
473
+ </Popover>
474
+ );
475
+ }
476
+
477
+ export default VariableAwareColorInput;