@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,518 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * CSS変数管理パネル
5
+ * Figmaスタイルのフローティングパネル
6
+ */
7
+
8
+ import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
9
+ import { Button } from '../../../components/ui/button';
10
+ import { Input } from '../../../components/ui/input';
11
+ import { ScrollArea } from '../../../components/ui/scroll-area';
12
+ import {
13
+ Palette,
14
+ Ruler,
15
+ Type,
16
+ Square,
17
+ Layers,
18
+ Settings,
19
+ X,
20
+ Plus,
21
+ Search,
22
+ Save,
23
+ GripVertical,
24
+ Monitor,
25
+ Tablet,
26
+ Smartphone,
27
+ Trash2,
28
+ type LucideIcon,
29
+ } from 'lucide-react';
30
+ import { cn } from '../../../lib/utils';
31
+ import type {
32
+ CSSVariableDefinition,
33
+ CSSVariableCategory,
34
+ BreakpointId,
35
+ } from '../../../types/css-variables';
36
+ import {
37
+ CSS_VARIABLE_CATEGORIES,
38
+ BREAKPOINT_CONFIGS,
39
+ generateCSSName,
40
+ generateVariableId,
41
+ getValueForBreakpoint,
42
+ setValueForBreakpoint,
43
+ } from '../../../types/css-variables';
44
+ import { ColorValueCell } from './ColorValueCell';
45
+ import { TextValueCell } from './TextValueCell';
46
+
47
+ // ============================================
48
+ // アイコンマッピング
49
+ // ============================================
50
+
51
+ const CATEGORY_ICONS: Record<string, LucideIcon> = {
52
+ Palette,
53
+ Ruler,
54
+ Type,
55
+ Square,
56
+ Layers,
57
+ Settings,
58
+ };
59
+
60
+ const BREAKPOINT_ICONS: Record<string, LucideIcon> = {
61
+ Monitor,
62
+ Tablet,
63
+ Smartphone,
64
+ Watch: Smartphone, // fallback
65
+ };
66
+
67
+ // ============================================
68
+ // 型定義
69
+ // ============================================
70
+
71
+ interface VariablesPanelProps {
72
+ isOpen: boolean;
73
+ onClose: () => void;
74
+ variables: CSSVariableDefinition[];
75
+ onAddVariable: (name: string, value: string, category: CSSVariableCategory, description?: string) => CSSVariableDefinition;
76
+ onUpdateVariable: (id: string, updates: Partial<Omit<CSSVariableDefinition, 'id'>>) => void;
77
+ onDeleteVariable: (id: string) => void;
78
+ onSave: () => Promise<void>;
79
+ isSaving?: boolean;
80
+ hasChanges?: boolean;
81
+ }
82
+
83
+ interface PanelPosition {
84
+ x: number;
85
+ y: number;
86
+ }
87
+
88
+ // ============================================
89
+ // ドラッグフック
90
+ // ============================================
91
+
92
+ function useDraggable(initialPosition: PanelPosition) {
93
+ const [position, setPosition] = useState(initialPosition);
94
+ const [isDragging, setIsDragging] = useState(false);
95
+ const dragOffset = useRef({ x: 0, y: 0 });
96
+
97
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
98
+ if ((e.target as HTMLElement).closest('button, input, [data-no-drag]')) return;
99
+ setIsDragging(true);
100
+ dragOffset.current = {
101
+ x: e.clientX - position.x,
102
+ y: e.clientY - position.y,
103
+ };
104
+ }, [position]);
105
+
106
+ useEffect(() => {
107
+ if (!isDragging) return;
108
+
109
+ const handleMouseMove = (e: MouseEvent) => {
110
+ setPosition({
111
+ x: Math.max(0, Math.min(window.innerWidth - 500, e.clientX - dragOffset.current.x)),
112
+ y: Math.max(0, Math.min(window.innerHeight - 400, e.clientY - dragOffset.current.y)),
113
+ });
114
+ };
115
+
116
+ const handleMouseUp = () => {
117
+ setIsDragging(false);
118
+ };
119
+
120
+ document.addEventListener('mousemove', handleMouseMove);
121
+ document.addEventListener('mouseup', handleMouseUp);
122
+
123
+ return () => {
124
+ document.removeEventListener('mousemove', handleMouseMove);
125
+ document.removeEventListener('mouseup', handleMouseUp);
126
+ };
127
+ }, [isDragging]);
128
+
129
+ return { position, isDragging, handleMouseDown };
130
+ }
131
+
132
+ // ============================================
133
+ // メインコンポーネント
134
+ // ============================================
135
+
136
+ export function VariablesPanel({
137
+ isOpen,
138
+ onClose,
139
+ variables,
140
+ onAddVariable,
141
+ onUpdateVariable,
142
+ onDeleteVariable,
143
+ onSave,
144
+ isSaving = false,
145
+ hasChanges = false,
146
+ }: VariablesPanelProps) {
147
+ // パネル位置
148
+ const { position, isDragging, handleMouseDown } = useDraggable({ x: 100, y: 100 });
149
+
150
+ // 状態
151
+ const [activeCategory, setActiveCategory] = useState<CSSVariableCategory>('color');
152
+ const [searchQuery, setSearchQuery] = useState('');
153
+ const [editingCell, setEditingCell] = useState<{
154
+ variableId: string;
155
+ breakpoint: BreakpointId;
156
+ } | null>(null);
157
+ const [isAddingNew, setIsAddingNew] = useState(false);
158
+ const [newVarName, setNewVarName] = useState('');
159
+ const [newVarValue, setNewVarValue] = useState('');
160
+
161
+ // 表示するブレークポイント
162
+ const [visibleBreakpoints] = useState<BreakpointId[]>(['default', 'tablet', 'mobile']);
163
+
164
+ // カテゴリごとの変数カウント
165
+ const categoryCounts = useMemo(() => {
166
+ const counts: Record<CSSVariableCategory, number> = {
167
+ color: 0,
168
+ spacing: 0,
169
+ typography: 0,
170
+ border: 0,
171
+ shadow: 0,
172
+ other: 0,
173
+ };
174
+ variables.forEach(v => counts[v.category]++);
175
+ return counts;
176
+ }, [variables]);
177
+
178
+ // フィルタリングされた変数
179
+ const filteredVariables = useMemo(() => {
180
+ return variables.filter(v => {
181
+ if (v.category !== activeCategory) return false;
182
+ if (searchQuery && !v.name.toLowerCase().includes(searchQuery.toLowerCase())) return false;
183
+ return true;
184
+ });
185
+ }, [variables, activeCategory, searchQuery]);
186
+
187
+ // 新しい変数を追加
188
+ const handleAddVariable = useCallback(() => {
189
+ if (!newVarName.trim()) return;
190
+
191
+ const defaultValue = activeCategory === 'color' ? '#3b82f6' :
192
+ activeCategory === 'spacing' ? '16px' :
193
+ activeCategory === 'typography' ? '16px' :
194
+ activeCategory === 'border' ? '1px solid #e5e7eb' :
195
+ activeCategory === 'shadow' ? '0 1px 3px rgba(0,0,0,0.1)' : '';
196
+
197
+ onAddVariable(newVarName.trim(), newVarValue || defaultValue, activeCategory);
198
+ setNewVarName('');
199
+ setNewVarValue('');
200
+ setIsAddingNew(false);
201
+ }, [newVarName, newVarValue, activeCategory, onAddVariable]);
202
+
203
+ // 変数値を更新
204
+ const handleUpdateValue = useCallback((variableId: string, breakpoint: BreakpointId, value: string) => {
205
+ const variable = variables.find(v => v.id === variableId);
206
+ if (!variable) return;
207
+
208
+ if (breakpoint === 'default') {
209
+ onUpdateVariable(variableId, { value });
210
+ } else {
211
+ const updated = setValueForBreakpoint(variable, breakpoint, value);
212
+ onUpdateVariable(variableId, { breakpointValues: updated.breakpointValues });
213
+ }
214
+ }, [variables, onUpdateVariable]);
215
+
216
+ // 変数名を更新
217
+ const handleUpdateName = useCallback((variableId: string, name: string) => {
218
+ const variable = variables.find(v => v.id === variableId);
219
+ if (!variable) return;
220
+
221
+ const newCssName = generateCSSName(name, variable.category);
222
+ onUpdateVariable(variableId, { name, cssName: newCssName });
223
+ }, [variables, onUpdateVariable]);
224
+
225
+ if (!isOpen) return null;
226
+
227
+ return (
228
+ <div
229
+ className={cn(
230
+ 'fixed z-[1000] bg-[#2c2c2c] border border-[#444444] rounded-lg shadow-2xl',
231
+ 'flex flex-col',
232
+ isDragging && 'cursor-grabbing select-none'
233
+ )}
234
+ style={{
235
+ left: position.x,
236
+ top: position.y,
237
+ width: 600,
238
+ maxHeight: 'calc(100vh - 100px)',
239
+ }}
240
+ >
241
+ {/* ヘッダー */}
242
+ <div
243
+ className="flex items-center gap-2 px-3 py-2 border-b border-[#444444] cursor-grab"
244
+ onMouseDown={handleMouseDown}
245
+ >
246
+ <GripVertical className="w-4 h-4 text-gray-500 flex-shrink-0" />
247
+ <span className="text-sm font-medium text-white flex-1">CSS変数</span>
248
+
249
+ {/* 検索 */}
250
+ <div className="relative" data-no-drag>
251
+ <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-500" />
252
+ <Input
253
+ type="text"
254
+ placeholder="検索..."
255
+ value={searchQuery}
256
+ onChange={(e) => setSearchQuery(e.target.value)}
257
+ className="h-7 w-32 pl-7 text-xs bg-[#383838] border-[#444444] text-white placeholder:text-gray-500"
258
+ />
259
+ </div>
260
+
261
+ {/* 追加ボタン */}
262
+ <Button
263
+ variant="ghost"
264
+ size="icon"
265
+ className="h-7 w-7 text-gray-400 hover:text-white"
266
+ onClick={() => setIsAddingNew(true)}
267
+ data-no-drag
268
+ >
269
+ <Plus className="w-4 h-4" />
270
+ </Button>
271
+
272
+ {/* 閉じるボタン */}
273
+ <Button
274
+ variant="ghost"
275
+ size="icon"
276
+ className="h-7 w-7 text-gray-400 hover:text-white"
277
+ onClick={onClose}
278
+ data-no-drag
279
+ >
280
+ <X className="w-4 h-4" />
281
+ </Button>
282
+ </div>
283
+
284
+ {/* カテゴリタブ */}
285
+ <div className="flex items-center gap-1 px-2 py-1.5 border-b border-[#444444] overflow-x-auto">
286
+ {CSS_VARIABLE_CATEGORIES.map((cat) => {
287
+ const Icon = CATEGORY_ICONS[cat.icon] ?? Settings;
288
+ const count = categoryCounts[cat.id];
289
+ const isActive = activeCategory === cat.id;
290
+
291
+ return (
292
+ <button
293
+ key={cat.id}
294
+ onClick={() => setActiveCategory(cat.id)}
295
+ className={cn(
296
+ 'flex items-center gap-1.5 px-2.5 py-1 rounded text-xs whitespace-nowrap transition-colors',
297
+ isActive
298
+ ? 'bg-[#444444] text-white'
299
+ : 'text-gray-400 hover:text-white hover:bg-[#444444]/50'
300
+ )}
301
+ >
302
+ <Icon className="w-3.5 h-3.5" />
303
+ <span>{cat.label}</span>
304
+ {count > 0 && (
305
+ <span className={cn(
306
+ 'text-[10px] px-1 rounded-full',
307
+ isActive ? 'bg-[#0d99ff] text-white' : 'bg-[#4a4a4a] text-gray-300'
308
+ )}>
309
+ {count}
310
+ </span>
311
+ )}
312
+ </button>
313
+ );
314
+ })}
315
+ </div>
316
+
317
+ {/* テーブル */}
318
+ <ScrollArea className="flex-1 min-h-0">
319
+ <table className="w-full">
320
+ {/* テーブルヘッダー */}
321
+ <thead className="sticky top-0 bg-[#2c2c2c] z-10">
322
+ <tr className="border-b border-[#444444]">
323
+ <th className="px-3 py-2 text-left text-[10px] font-medium text-gray-500 uppercase w-[180px]">
324
+ 名前
325
+ </th>
326
+ {BREAKPOINT_CONFIGS.filter(bp => visibleBreakpoints.includes(bp.id)).map((bp) => {
327
+ const Icon = BREAKPOINT_ICONS[bp.icon] ?? Monitor;
328
+ return (
329
+ <th key={bp.id} className="px-3 py-2 text-left text-[10px] font-medium text-gray-500 uppercase">
330
+ <div className="flex items-center gap-1.5">
331
+ <Icon className="w-3.5 h-3.5" />
332
+ <span>{bp.label}</span>
333
+ </div>
334
+ </th>
335
+ );
336
+ })}
337
+ <th className="w-10" />
338
+ </tr>
339
+ </thead>
340
+
341
+ <tbody>
342
+ {filteredVariables.map((variable) => (
343
+ <tr
344
+ key={variable.id}
345
+ className="border-b border-[#444444]/50 hover:bg-[#444444]/30"
346
+ >
347
+ {/* 名前セル */}
348
+ <td className="px-3 py-1">
349
+ <TextValueCell
350
+ value={variable.name}
351
+ isEditing={editingCell?.variableId === variable.id && editingCell.breakpoint === 'default'}
352
+ onStartEdit={() => setEditingCell({ variableId: variable.id, breakpoint: 'default' })}
353
+ onFinishEdit={() => setEditingCell(null)}
354
+ onValueChange={(name) => handleUpdateName(variable.id, name)}
355
+ className="font-medium"
356
+ prefix={
357
+ activeCategory === 'color' ? (
358
+ <div
359
+ className="w-4 h-4 rounded border border-[#444444] flex-shrink-0"
360
+ style={{ backgroundColor: variable.value }}
361
+ />
362
+ ) : undefined
363
+ }
364
+ />
365
+ </td>
366
+
367
+ {/* 値セル(ブレークポイントごと) */}
368
+ {BREAKPOINT_CONFIGS.filter(bp => visibleBreakpoints.includes(bp.id)).map((bp) => {
369
+ const value = getValueForBreakpoint(variable, bp.id);
370
+ const isInherited = bp.id !== 'default' && !variable.breakpointValues?.find(v => v.breakpoint === bp.id);
371
+ const isEditing = editingCell?.variableId === variable.id && editingCell.breakpoint === bp.id;
372
+
373
+ return (
374
+ <td key={bp.id} className="px-3 py-1">
375
+ {activeCategory === 'color' ? (
376
+ <ColorValueCell
377
+ value={value}
378
+ isEditing={isEditing}
379
+ isInherited={isInherited}
380
+ onStartEdit={() => setEditingCell({ variableId: variable.id, breakpoint: bp.id })}
381
+ onFinishEdit={() => setEditingCell(null)}
382
+ onValueChange={(newValue) => handleUpdateValue(variable.id, bp.id, newValue)}
383
+ />
384
+ ) : (
385
+ <TextValueCell
386
+ value={value}
387
+ isEditing={isEditing}
388
+ isInherited={isInherited}
389
+ onStartEdit={() => setEditingCell({ variableId: variable.id, breakpoint: bp.id })}
390
+ onFinishEdit={() => setEditingCell(null)}
391
+ onValueChange={(newValue) => handleUpdateValue(variable.id, bp.id, newValue)}
392
+ className="font-mono text-xs"
393
+ />
394
+ )}
395
+ </td>
396
+ );
397
+ })}
398
+
399
+ {/* 削除ボタン */}
400
+ <td className="px-2">
401
+ <Button
402
+ variant="ghost"
403
+ size="icon"
404
+ className="h-6 w-6 text-gray-500 hover:text-red-400"
405
+ onClick={() => onDeleteVariable(variable.id)}
406
+ >
407
+ <Trash2 className="w-3.5 h-3.5" />
408
+ </Button>
409
+ </td>
410
+ </tr>
411
+ ))}
412
+
413
+ {/* 新規追加行 */}
414
+ {isAddingNew && (
415
+ <tr className="border-b border-[#444444]/50 bg-[#444444]/20">
416
+ <td className="px-3 py-2">
417
+ <Input
418
+ type="text"
419
+ placeholder="変数名"
420
+ value={newVarName}
421
+ onChange={(e) => setNewVarName(e.target.value)}
422
+ onKeyDown={(e) => {
423
+ if (e.key === 'Enter') handleAddVariable();
424
+ if (e.key === 'Escape') setIsAddingNew(false);
425
+ }}
426
+ autoFocus
427
+ className="h-7 text-xs bg-[#383838] border-[#444444] text-white"
428
+ />
429
+ </td>
430
+ <td className="px-3 py-2" colSpan={visibleBreakpoints.length}>
431
+ <Input
432
+ type="text"
433
+ placeholder={activeCategory === 'color' ? '#3b82f6' : '値を入力'}
434
+ value={newVarValue}
435
+ onChange={(e) => setNewVarValue(e.target.value)}
436
+ onKeyDown={(e) => {
437
+ if (e.key === 'Enter') handleAddVariable();
438
+ if (e.key === 'Escape') setIsAddingNew(false);
439
+ }}
440
+ className="h-7 text-xs bg-[#383838] border-[#444444] text-white font-mono"
441
+ />
442
+ </td>
443
+ <td className="px-2">
444
+ <div className="flex gap-1">
445
+ <Button
446
+ variant="ghost"
447
+ size="icon"
448
+ className="h-6 w-6 text-green-400 hover:text-green-300"
449
+ onClick={handleAddVariable}
450
+ >
451
+ <Plus className="w-3.5 h-3.5" />
452
+ </Button>
453
+ <Button
454
+ variant="ghost"
455
+ size="icon"
456
+ className="h-6 w-6 text-gray-500 hover:text-gray-300"
457
+ onClick={() => setIsAddingNew(false)}
458
+ >
459
+ <X className="w-3.5 h-3.5" />
460
+ </Button>
461
+ </div>
462
+ </td>
463
+ </tr>
464
+ )}
465
+ </tbody>
466
+ </table>
467
+
468
+ {/* 変数がない場合 */}
469
+ {filteredVariables.length === 0 && !isAddingNew && (
470
+ <div className="flex flex-col items-center justify-center py-12 text-gray-500">
471
+ <div className="text-sm mb-2">
472
+ {searchQuery ? '検索結果がありません' : 'このカテゴリには変数がありません'}
473
+ </div>
474
+ <Button
475
+ variant="outline"
476
+ size="sm"
477
+ onClick={() => setIsAddingNew(true)}
478
+ className="text-xs bg-transparent border-[#444444] text-gray-400 hover:text-white hover:bg-[#444444]"
479
+ >
480
+ <Plus className="w-3.5 h-3.5 mr-1" />
481
+ 新しい{CSS_VARIABLE_CATEGORIES.find(c => c.id === activeCategory)?.label}を追加
482
+ </Button>
483
+ </div>
484
+ )}
485
+ </ScrollArea>
486
+
487
+ {/* フッター */}
488
+ <div className="flex items-center justify-between px-3 py-2 border-t border-[#444444]">
489
+ <div className="text-[10px] text-gray-500">
490
+ {hasChanges && '未保存の変更があります'}
491
+ </div>
492
+ <div className="flex items-center gap-2">
493
+ <Button
494
+ variant="default"
495
+ size="sm"
496
+ onClick={onSave}
497
+ disabled={isSaving || !hasChanges}
498
+ className="h-7 text-xs"
499
+ >
500
+ {isSaving ? (
501
+ <>
502
+ <span className="animate-spin mr-1">⏳</span>
503
+ 保存中...
504
+ </>
505
+ ) : (
506
+ <>
507
+ <Save className="w-3.5 h-3.5 mr-1" />
508
+ 保存
509
+ </>
510
+ )}
511
+ </Button>
512
+ </div>
513
+ </div>
514
+ </div>
515
+ );
516
+ }
517
+
518
+ export default VariablesPanel;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * CSS変数パネルコンポーネント
3
+ */
4
+
5
+ export { VariablesPanel } from './VariablesPanel';
6
+ export { ColorValueCell } from './ColorValueCell';
7
+ export { TextValueCell } from './TextValueCell';