@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,564 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * CSS変数管理ダイアログ
5
+ * FigmaライクなCSS変数(デザイントークン)の定義・編集UI
6
+ */
7
+
8
+ import { useState, useCallback, useMemo } from 'react';
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ DialogDescription,
15
+ } from '../../components/ui/dialog';
16
+ import { Button } from '../../components/ui/button';
17
+ import { Input } from '../../components/ui/input';
18
+ import { Label } from '../../components/ui/label';
19
+ import {
20
+ Collapsible,
21
+ CollapsibleContent,
22
+ CollapsibleTrigger,
23
+ } from '../../components/ui/collapsible';
24
+ import {
25
+ Select,
26
+ SelectContent,
27
+ SelectItem,
28
+ SelectTrigger,
29
+ SelectValue,
30
+ } from '../../components/ui/select';
31
+ import { ScrollArea } from '../../components/ui/scroll-area';
32
+ import {
33
+ Palette,
34
+ Ruler,
35
+ Type,
36
+ Square,
37
+ Layers,
38
+ Settings,
39
+ ChevronRight,
40
+ Plus,
41
+ Pencil,
42
+ Trash2,
43
+ RefreshCw,
44
+ Save,
45
+ X,
46
+ Check,
47
+ Loader2,
48
+ type LucideIcon,
49
+ } from 'lucide-react';
50
+ import { cn } from '../../lib/utils';
51
+ import type {
52
+ CSSVariableDefinition,
53
+ CSSVariableCategory,
54
+ CSSVariableGroup,
55
+ } from '../../types/css-variables';
56
+ import {
57
+ CSS_VARIABLE_CATEGORIES,
58
+ generateCSSName,
59
+ generateVariableId,
60
+ } from '../../types/css-variables';
61
+
62
+ // ============================================
63
+ // アイコンマッピング
64
+ // ============================================
65
+
66
+ const CATEGORY_ICONS: Record<string, LucideIcon> = {
67
+ Palette,
68
+ Ruler,
69
+ Type,
70
+ Square,
71
+ Layers,
72
+ Settings,
73
+ };
74
+
75
+ function getCategoryIcon(iconName: string): LucideIcon {
76
+ return CATEGORY_ICONS[iconName] ?? Settings;
77
+ }
78
+
79
+ // ============================================
80
+ // 型定義
81
+ // ============================================
82
+
83
+ interface VariablesDialogProps {
84
+ open: boolean;
85
+ onOpenChange: (open: boolean) => void;
86
+ variables: CSSVariableDefinition[];
87
+ variableGroups: CSSVariableGroup[];
88
+ isLoading?: boolean;
89
+ isSaving?: boolean;
90
+ hasChanges?: boolean;
91
+ onAddVariable: (name: string, value: string, category: CSSVariableCategory, description?: string) => CSSVariableDefinition;
92
+ onUpdateVariable: (id: string, updates: Partial<Omit<CSSVariableDefinition, 'id'>>) => void;
93
+ onDeleteVariable: (id: string) => void;
94
+ onSave: () => Promise<void>;
95
+ onSyncFromTheme?: () => void;
96
+ }
97
+
98
+ // ============================================
99
+ // 変数編集フォーム
100
+ // ============================================
101
+
102
+ interface VariableEditorFormProps {
103
+ variable?: CSSVariableDefinition;
104
+ category: CSSVariableCategory;
105
+ onSubmit: (data: { name: string; value: string; category: CSSVariableCategory; description?: string }) => void;
106
+ onCancel: () => void;
107
+ isNew?: boolean;
108
+ }
109
+
110
+ function VariableEditorForm({
111
+ variable,
112
+ category: initialCategory,
113
+ onSubmit,
114
+ onCancel,
115
+ isNew = false,
116
+ }: VariableEditorFormProps) {
117
+ const [name, setName] = useState(variable?.name ?? '');
118
+ const [value, setValue] = useState(variable?.value ?? '');
119
+ const [category, setCategory] = useState<CSSVariableCategory>(variable?.category ?? initialCategory);
120
+ const [description, setDescription] = useState(variable?.description ?? '');
121
+
122
+ const handleSubmit = (e: React.FormEvent) => {
123
+ e.preventDefault();
124
+ if (!name.trim() || !value.trim()) return;
125
+ onSubmit({ name: name.trim(), value: value.trim(), category, description: description.trim() || undefined });
126
+ };
127
+
128
+ const cssName = useMemo(() => generateCSSName(name, category), [name, category]);
129
+
130
+ return (
131
+ <form onSubmit={handleSubmit} className="space-y-4 p-4 bg-gray-50 rounded-lg border">
132
+ <div className="flex items-center justify-between">
133
+ <h4 className="font-medium text-sm">
134
+ {isNew ? '新しい変数を追加' : '変数を編集'}
135
+ </h4>
136
+ <Button type="button" variant="ghost" size="icon" className="h-6 w-6" onClick={onCancel} aria-label="閉じる">
137
+ <X className="h-3.5 w-3.5" />
138
+ </Button>
139
+ </div>
140
+
141
+ <div className="grid gap-3">
142
+ {/* カテゴリ選択 */}
143
+ <div className="grid gap-1.5">
144
+ <Label className="text-xs">カテゴリ</Label>
145
+ <Select value={category} onValueChange={(v) => setCategory(v as CSSVariableCategory)}>
146
+ <SelectTrigger className="h-8 text-sm">
147
+ <SelectValue />
148
+ </SelectTrigger>
149
+ <SelectContent>
150
+ {CSS_VARIABLE_CATEGORIES.map((cat) => {
151
+ const Icon = getCategoryIcon(cat.icon);
152
+ return (
153
+ <SelectItem key={cat.id} value={cat.id}>
154
+ <div className="flex items-center gap-2">
155
+ <Icon className="h-3.5 w-3.5" />
156
+ <span>{cat.label}</span>
157
+ </div>
158
+ </SelectItem>
159
+ );
160
+ })}
161
+ </SelectContent>
162
+ </Select>
163
+ </div>
164
+
165
+ {/* 名前 */}
166
+ <div className="grid gap-1.5">
167
+ <Label className="text-xs">名前</Label>
168
+ <Input
169
+ value={name}
170
+ onChange={(e) => setName(e.target.value)}
171
+ placeholder="primary"
172
+ className="h-8 text-sm"
173
+ />
174
+ {name && (
175
+ <p className="text-xs text-gray-500 font-mono">{cssName}</p>
176
+ )}
177
+ </div>
178
+
179
+ {/* 値 */}
180
+ <div className="grid gap-1.5">
181
+ <Label className="text-xs">値</Label>
182
+ <div className="flex gap-2">
183
+ {category === 'color' && (
184
+ <input
185
+ type="color"
186
+ value={value.startsWith('#') ? value : '#000000'}
187
+ onChange={(e) => setValue(e.target.value)}
188
+ className="w-8 h-8 rounded border cursor-pointer"
189
+ />
190
+ )}
191
+ <Input
192
+ value={value}
193
+ onChange={(e) => setValue(e.target.value)}
194
+ placeholder={category === 'color' ? '#3b82f6' : '16px'}
195
+ className="h-8 text-sm font-mono flex-1"
196
+ />
197
+ </div>
198
+ </div>
199
+
200
+ {/* 説明 */}
201
+ <div className="grid gap-1.5">
202
+ <Label className="text-xs">説明(オプション)</Label>
203
+ <Input
204
+ value={description}
205
+ onChange={(e) => setDescription(e.target.value)}
206
+ placeholder="変数の説明..."
207
+ className="h-8 text-sm"
208
+ />
209
+ </div>
210
+ </div>
211
+
212
+ <div className="flex gap-2 justify-end">
213
+ <Button type="button" variant="outline" size="sm" onClick={onCancel}>
214
+ キャンセル
215
+ </Button>
216
+ <Button type="submit" size="sm" disabled={!name.trim() || !value.trim()}>
217
+ <Check className="h-3.5 w-3.5 mr-1" />
218
+ {isNew ? '追加' : '更新'}
219
+ </Button>
220
+ </div>
221
+ </form>
222
+ );
223
+ }
224
+
225
+ // ============================================
226
+ // 変数アイテム表示
227
+ // ============================================
228
+
229
+ interface VariableItemProps {
230
+ variable: CSSVariableDefinition;
231
+ onEdit: () => void;
232
+ onDelete: () => void;
233
+ }
234
+
235
+ function VariableItem({ variable, onEdit, onDelete }: VariableItemProps) {
236
+ return (
237
+ <div className="flex items-center gap-3 py-2 px-3 hover:bg-gray-50 rounded-lg group">
238
+ {/* カラープレビュー */}
239
+ {variable.category === 'color' && (
240
+ <div
241
+ className="w-6 h-6 rounded border shadow-sm flex-shrink-0"
242
+ style={{ backgroundColor: variable.value }}
243
+ />
244
+ )}
245
+ {variable.category !== 'color' && (
246
+ <div className="w-6 h-6 rounded border bg-gray-100 flex items-center justify-center flex-shrink-0">
247
+ <span className="text-[8px] text-gray-500 font-mono">
248
+ {variable.value.slice(0, 3)}
249
+ </span>
250
+ </div>
251
+ )}
252
+
253
+ {/* 変数情報 */}
254
+ <div className="flex-1 min-w-0">
255
+ <div className="flex items-center gap-2">
256
+ <span className="font-mono text-sm text-gray-700 truncate">
257
+ {variable.cssName}
258
+ </span>
259
+ </div>
260
+ <span className="font-mono text-xs text-gray-500 truncate block">
261
+ {variable.value}
262
+ </span>
263
+ </div>
264
+
265
+ {/* アクション */}
266
+ <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
267
+ <Button
268
+ variant="ghost"
269
+ size="icon"
270
+ className="h-7 w-7 text-gray-400 hover:text-gray-600"
271
+ onClick={onEdit}
272
+ >
273
+ <Pencil className="h-3.5 w-3.5" />
274
+ </Button>
275
+ <Button
276
+ variant="ghost"
277
+ size="icon"
278
+ className="h-7 w-7 text-gray-400 hover:text-red-600"
279
+ onClick={onDelete}
280
+ >
281
+ <Trash2 className="h-3.5 w-3.5" />
282
+ </Button>
283
+ </div>
284
+ </div>
285
+ );
286
+ }
287
+
288
+ // ============================================
289
+ // カテゴリセクション
290
+ // ============================================
291
+
292
+ interface CategorySectionProps {
293
+ group: CSSVariableGroup;
294
+ isOpen: boolean;
295
+ onToggle: () => void;
296
+ editingId: string | null;
297
+ onStartEdit: (id: string) => void;
298
+ onCancelEdit: () => void;
299
+ onUpdateVariable: (id: string, updates: Partial<Omit<CSSVariableDefinition, 'id'>>) => void;
300
+ onDeleteVariable: (id: string) => void;
301
+ onAddVariable: (name: string, value: string, category: CSSVariableCategory, description?: string) => void;
302
+ showAddForm: boolean;
303
+ onToggleAddForm: () => void;
304
+ }
305
+
306
+ function CategorySection({
307
+ group,
308
+ isOpen,
309
+ onToggle,
310
+ editingId,
311
+ onStartEdit,
312
+ onCancelEdit,
313
+ onUpdateVariable,
314
+ onDeleteVariable,
315
+ onAddVariable,
316
+ showAddForm,
317
+ onToggleAddForm,
318
+ }: CategorySectionProps) {
319
+ const config = CSS_VARIABLE_CATEGORIES.find(c => c.id === group.category);
320
+ const Icon = getCategoryIcon(config?.icon ?? 'Settings');
321
+
322
+ const handleAdd = (data: { name: string; value: string; category: CSSVariableCategory; description?: string }) => {
323
+ onAddVariable(data.name, data.value, data.category, data.description);
324
+ onToggleAddForm();
325
+ };
326
+
327
+ const handleUpdate = (id: string, data: { name: string; value: string; category: CSSVariableCategory; description?: string }) => {
328
+ onUpdateVariable(id, data);
329
+ onCancelEdit();
330
+ };
331
+
332
+ return (
333
+ <Collapsible open={isOpen} onOpenChange={onToggle}>
334
+ <CollapsibleTrigger className="flex items-center justify-between w-full py-2.5 px-3 text-sm font-medium text-gray-700 hover:bg-gray-50 rounded-lg">
335
+ <span className="flex items-center gap-2">
336
+ <Icon className="w-4 h-4 text-gray-500" />
337
+ {config?.label ?? group.category}
338
+ <span className="text-xs text-gray-400 font-normal">
339
+ ({group.variables.length})
340
+ </span>
341
+ </span>
342
+ <ChevronRight
343
+ className={cn('w-4 h-4 text-gray-400 transition-transform', isOpen && 'rotate-90')}
344
+ />
345
+ </CollapsibleTrigger>
346
+
347
+ <CollapsibleContent className="pl-2">
348
+ <div className="space-y-1 py-1">
349
+ {group.variables.map((variable) => (
350
+ editingId === variable.id ? (
351
+ <VariableEditorForm
352
+ key={variable.id}
353
+ variable={variable}
354
+ category={variable.category}
355
+ onSubmit={(data) => handleUpdate(variable.id, data)}
356
+ onCancel={onCancelEdit}
357
+ />
358
+ ) : (
359
+ <VariableItem
360
+ key={variable.id}
361
+ variable={variable}
362
+ onEdit={() => onStartEdit(variable.id)}
363
+ onDelete={() => onDeleteVariable(variable.id)}
364
+ />
365
+ )
366
+ ))}
367
+
368
+ {/* 追加フォーム */}
369
+ {showAddForm && (
370
+ <VariableEditorForm
371
+ category={group.category}
372
+ onSubmit={handleAdd}
373
+ onCancel={onToggleAddForm}
374
+ isNew
375
+ />
376
+ )}
377
+
378
+ {/* 追加ボタン */}
379
+ {!showAddForm && (
380
+ <Button
381
+ variant="ghost"
382
+ size="sm"
383
+ className="w-full justify-start text-gray-500 hover:text-gray-700 mt-1"
384
+ onClick={onToggleAddForm}
385
+ >
386
+ <Plus className="h-3.5 w-3.5 mr-1.5" />
387
+ {config?.label}を追加
388
+ </Button>
389
+ )}
390
+ </div>
391
+ </CollapsibleContent>
392
+ </Collapsible>
393
+ );
394
+ }
395
+
396
+ // ============================================
397
+ // メインダイアログ
398
+ // ============================================
399
+
400
+ export function VariablesDialog({
401
+ open,
402
+ onOpenChange,
403
+ variables,
404
+ variableGroups,
405
+ isLoading = false,
406
+ isSaving = false,
407
+ hasChanges = false,
408
+ onAddVariable,
409
+ onUpdateVariable,
410
+ onDeleteVariable,
411
+ onSave,
412
+ onSyncFromTheme,
413
+ }: VariablesDialogProps) {
414
+ // 開いているセクション
415
+ const [openSections, setOpenSections] = useState<Set<CSSVariableCategory>>(
416
+ new Set(['color', 'spacing'])
417
+ );
418
+
419
+ // 編集中の変数ID
420
+ const [editingId, setEditingId] = useState<string | null>(null);
421
+
422
+ // 追加フォーム表示中のカテゴリ
423
+ const [addingCategory, setAddingCategory] = useState<CSSVariableCategory | null>(null);
424
+
425
+ // セクショントグル
426
+ const toggleSection = useCallback((category: CSSVariableCategory) => {
427
+ setOpenSections(prev => {
428
+ const next = new Set(prev);
429
+ if (next.has(category)) {
430
+ next.delete(category);
431
+ } else {
432
+ next.add(category);
433
+ }
434
+ return next;
435
+ });
436
+ }, []);
437
+
438
+ // 変数追加ハンドラ
439
+ const handleAdd = useCallback((
440
+ name: string,
441
+ value: string,
442
+ category: CSSVariableCategory,
443
+ description?: string
444
+ ) => {
445
+ onAddVariable(name, value, category, description);
446
+ }, [onAddVariable]);
447
+
448
+ // 保存ハンドラ
449
+ const handleSave = useCallback(async () => {
450
+ try {
451
+ await onSave();
452
+ } catch (error) {
453
+ console.error('Failed to save variables:', error);
454
+ }
455
+ }, [onSave]);
456
+
457
+ // 空のカテゴリも含めた全カテゴリリスト
458
+ const allCategories = useMemo(() => {
459
+ return CSS_VARIABLE_CATEGORIES.map(config => {
460
+ const group = variableGroups.find(g => g.category === config.id);
461
+ return {
462
+ category: config.id,
463
+ label: config.label,
464
+ icon: config.icon,
465
+ variables: group?.variables ?? [],
466
+ } as CSSVariableGroup;
467
+ });
468
+ }, [variableGroups]);
469
+
470
+ return (
471
+ <Dialog open={open} onOpenChange={onOpenChange}>
472
+ <DialogContent className="max-w-2xl max-h-[85vh] flex flex-col">
473
+ <DialogHeader>
474
+ <DialogTitle className="flex items-center gap-2">
475
+ <Settings className="h-5 w-5" />
476
+ CSS変数管理
477
+ </DialogTitle>
478
+ <DialogDescription>
479
+ デザイントークン(カラー、スペーシング、タイポグラフィ等)を定義・管理します。
480
+ 定義した変数はプロジェクト全体で使用できます。
481
+ </DialogDescription>
482
+ </DialogHeader>
483
+
484
+ {/* ツールバー */}
485
+ <div className="flex items-center justify-between gap-2 py-2 border-b">
486
+ <div className="flex items-center gap-2">
487
+ {onSyncFromTheme && (
488
+ <Button
489
+ variant="outline"
490
+ size="sm"
491
+ onClick={onSyncFromTheme}
492
+ disabled={isLoading || isSaving}
493
+ >
494
+ <RefreshCw className="h-3.5 w-3.5 mr-1.5" />
495
+ テーマから同期
496
+ </Button>
497
+ )}
498
+ </div>
499
+
500
+ <div className="flex items-center gap-2">
501
+ <span className="text-xs text-gray-500">
502
+ {variables.length} 個の変数
503
+ </span>
504
+ {hasChanges && (
505
+ <span className="text-xs text-amber-600">未保存の変更あり</span>
506
+ )}
507
+ </div>
508
+ </div>
509
+
510
+ {/* 変数リスト */}
511
+ <ScrollArea className="flex-1 -mx-6 px-6">
512
+ {isLoading ? (
513
+ <div className="flex items-center justify-center py-12">
514
+ <Loader2 className="h-6 w-6 animate-spin text-gray-400" />
515
+ </div>
516
+ ) : (
517
+ <div className="space-y-2 py-2">
518
+ {allCategories.map((group) => (
519
+ <CategorySection
520
+ key={group.category}
521
+ group={group}
522
+ isOpen={openSections.has(group.category)}
523
+ onToggle={() => toggleSection(group.category)}
524
+ editingId={editingId}
525
+ onStartEdit={setEditingId}
526
+ onCancelEdit={() => setEditingId(null)}
527
+ onUpdateVariable={onUpdateVariable}
528
+ onDeleteVariable={onDeleteVariable}
529
+ onAddVariable={handleAdd}
530
+ showAddForm={addingCategory === group.category}
531
+ onToggleAddForm={() => setAddingCategory(
532
+ addingCategory === group.category ? null : group.category
533
+ )}
534
+ />
535
+ ))}
536
+ </div>
537
+ )}
538
+ </ScrollArea>
539
+
540
+ {/* フッター */}
541
+ <div className="flex items-center justify-end gap-2 pt-4 border-t">
542
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
543
+ 閉じる
544
+ </Button>
545
+ <Button onClick={handleSave} disabled={isSaving || !hasChanges}>
546
+ {isSaving ? (
547
+ <>
548
+ <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
549
+ 保存中...
550
+ </>
551
+ ) : (
552
+ <>
553
+ <Save className="h-4 w-4 mr-1.5" />
554
+ 保存
555
+ </>
556
+ )}
557
+ </Button>
558
+ </div>
559
+ </DialogContent>
560
+ </Dialog>
561
+ );
562
+ }
563
+
564
+ export default VariablesDialog;