@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,69 @@
1
+ 'use client';
2
+
3
+ import { Button } from '../../../components/ui/button';
4
+ import { Input } from '../../../components/ui/input';
5
+ import { Label } from '../../../components/ui/label';
6
+ import {
7
+ Collapsible,
8
+ CollapsibleContent,
9
+ CollapsibleTrigger,
10
+ } from '../../../components/ui/collapsible';
11
+ import { Palette, ChevronRight, EyeOff } from 'lucide-react';
12
+ import { rgbToHex } from '../../utils/style-utils';
13
+ import type { SelectedElementInfo } from '../../types';
14
+
15
+ interface FillSectionProps {
16
+ selectedElement: SelectedElementInfo;
17
+ open: boolean;
18
+ onOpenChange: (open: boolean) => void;
19
+ onStyleChange: (styles: Record<string, string>) => void;
20
+ }
21
+
22
+ export function FillSection({
23
+ selectedElement,
24
+ open,
25
+ onOpenChange,
26
+ onStyleChange,
27
+ }: FillSectionProps) {
28
+ return (
29
+ <Collapsible open={open} onOpenChange={onOpenChange}>
30
+ <CollapsibleTrigger className="flex items-center justify-between w-full py-2 text-xs font-medium text-gray-300 hover:text-white border-t border-[#444444]">
31
+ <span className="flex items-center gap-2">
32
+ <Palette className="w-3.5 h-3.5" />
33
+ 塗り
34
+ </span>
35
+ <ChevronRight
36
+ className={`w-3.5 h-3.5 transition-transform ${open ? 'rotate-90' : ''}`}
37
+ />
38
+ </CollapsibleTrigger>
39
+ <CollapsibleContent className="space-y-2 pb-3">
40
+ <div>
41
+ <Label className="text-[10px] text-gray-500">背景色</Label>
42
+ <div className="flex items-center gap-2 mt-1">
43
+ <input
44
+ type="color"
45
+ value={rgbToHex(selectedElement.backgroundColor)}
46
+ onChange={(e) => onStyleChange({ backgroundColor: e.target.value })}
47
+ className="w-8 h-7 rounded border border-[#444444] bg-transparent cursor-pointer"
48
+ />
49
+ <Input
50
+ type="text"
51
+ value={rgbToHex(selectedElement.backgroundColor)}
52
+ onChange={(e) => onStyleChange({ backgroundColor: e.target.value })}
53
+ className="flex-1 h-7 text-xs bg-[#383838] border-[#444444] text-white font-mono"
54
+ />
55
+ <Button
56
+ variant="ghost"
57
+ size="icon"
58
+ onClick={() => onStyleChange({ backgroundColor: 'transparent' })}
59
+ className="h-7 w-7 text-gray-400 hover:text-white hover:bg-[#4a4a4a]"
60
+ title="透明にする"
61
+ >
62
+ <EyeOff className="w-3.5 h-3.5" />
63
+ </Button>
64
+ </div>
65
+ </div>
66
+ </CollapsibleContent>
67
+ </Collapsible>
68
+ );
69
+ }
@@ -0,0 +1,192 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback, useRef, useEffect } from 'react';
4
+ import { Input } from '../../../components/ui/input';
5
+ import { Button } from '../../../components/ui/button';
6
+ import {
7
+ Popover,
8
+ PopoverContent,
9
+ PopoverTrigger,
10
+ } from '../../../components/ui/popover';
11
+ import { ScrollArea } from '../../../components/ui/scroll-area';
12
+ import { ChevronDown, Search, Check, Loader2 } from 'lucide-react';
13
+ import {
14
+ useGoogleFonts,
15
+ useFilteredFonts,
16
+ FONT_CATEGORIES,
17
+ type FontCategory,
18
+ type GoogleFont,
19
+ } from '../../hooks/useGoogleFonts';
20
+
21
+ interface GoogleFontPickerProps {
22
+ value: string;
23
+ onChange: (fontFamily: string) => void;
24
+ iframeDoc?: Document | null;
25
+ className?: string;
26
+ }
27
+
28
+ /**
29
+ * Google Fontsフォントピッカー
30
+ * 検索、カテゴリフィルター、プレビュー付きのフォント選択コンポーネント
31
+ */
32
+ export function GoogleFontPicker({
33
+ value,
34
+ onChange,
35
+ iframeDoc,
36
+ className = '',
37
+ }: GoogleFontPickerProps) {
38
+ const [open, setOpen] = useState(false);
39
+ const [searchQuery, setSearchQuery] = useState('');
40
+ const [category, setCategory] = useState<FontCategory>('all');
41
+ const searchInputRef = useRef<HTMLInputElement>(null);
42
+
43
+ const { fonts, isLoading, loadFontInIframe, getFontPreviewStyle } = useGoogleFonts();
44
+ const filteredFonts = useFilteredFonts(fonts, searchQuery, category);
45
+
46
+ // ポップオーバーが開いたら検索フィールドにフォーカス
47
+ useEffect(() => {
48
+ if (open && searchInputRef.current) {
49
+ setTimeout(() => {
50
+ searchInputRef.current?.focus();
51
+ }, 100);
52
+ }
53
+ }, [open]);
54
+
55
+ // フォント選択ハンドラー
56
+ const handleSelectFont = useCallback((font: GoogleFont) => {
57
+ // デバイスフォントはOSで名前が違うので、スタックごと書き込む
58
+ onChange(font.stack ?? font.family);
59
+
60
+ // iframeにフォントをロード
61
+ if (iframeDoc) {
62
+ loadFontInIframe(font.family, iframeDoc, font.variants);
63
+ }
64
+
65
+ setOpen(false);
66
+ setSearchQuery('');
67
+ }, [onChange, iframeDoc, loadFontInIframe]);
68
+
69
+ // プレビュー用にフォントをロード(ホバー時)
70
+ const handleFontHover = useCallback((font: GoogleFont) => {
71
+ // プレビュー用のlinkタグをメインドキュメントに追加
72
+ const existingLink = document.querySelector(`link[data-font-preview="${font.family}"]`);
73
+ if (existingLink) return;
74
+
75
+ const link = document.createElement('link');
76
+ link.rel = 'stylesheet';
77
+ link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(font.family)}:wght@400&display=swap`;
78
+ link.dataset.fontPreview = font.family;
79
+ document.head.appendChild(link);
80
+ }, []);
81
+
82
+ // 表示する現在のフォント名
83
+ const displayValue = value || 'フォントを選択';
84
+
85
+ return (
86
+ <Popover open={open} onOpenChange={setOpen}>
87
+ <PopoverTrigger asChild>
88
+ <Button
89
+ variant="outline"
90
+ role="combobox"
91
+ aria-expanded={open}
92
+ className={`h-7 justify-between text-xs bg-[#383838] border-[#444444] text-white hover:bg-[#4c4c4c] hover:text-white ${className}`}
93
+ >
94
+ <span
95
+ className="truncate"
96
+ style={{
97
+ // スタック(カンマ入り)が入っているときは二重引用しない
98
+ fontFamily: value ? (value.includes(',') ? value : `"${value}", sans-serif`) : 'inherit',
99
+ }}
100
+ >
101
+ {displayValue}
102
+ </span>
103
+ <ChevronDown className="ml-2 h-3.5 w-3.5 shrink-0 opacity-50" />
104
+ </Button>
105
+ </PopoverTrigger>
106
+ <PopoverContent
107
+ className="w-72 p-0 bg-[#2c2c2c] border-[#444444]"
108
+ align="start"
109
+ >
110
+ {/* 検索フィールド */}
111
+ <div className="p-2 border-b border-[#444444]">
112
+ <div className="relative">
113
+ <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-gray-500" />
114
+ <Input
115
+ ref={searchInputRef}
116
+ placeholder="フォントを検索..."
117
+ value={searchQuery}
118
+ onChange={(e) => setSearchQuery(e.target.value)}
119
+ className="h-7 pl-8 text-xs bg-[#383838] border-[#444444] text-white"
120
+ />
121
+ </div>
122
+ </div>
123
+
124
+ {/* カテゴリタブ */}
125
+ <div className="flex gap-1 p-2 border-b border-[#444444] overflow-x-auto">
126
+ {FONT_CATEGORIES.map((cat) => (
127
+ <Button
128
+ key={cat.value}
129
+ variant="ghost"
130
+ size="sm"
131
+ onClick={() => setCategory(cat.value)}
132
+ className={`h-6 px-2 text-[10px] whitespace-nowrap ${
133
+ category === cat.value
134
+ ? 'bg-[#0d99ff] text-white hover:bg-[#0c8ce9]'
135
+ : 'text-gray-400 hover:text-white hover:bg-[#383838]'
136
+ }`}
137
+ >
138
+ {cat.label}
139
+ </Button>
140
+ ))}
141
+ </div>
142
+
143
+ {/* フォントリスト */}
144
+ <ScrollArea className="h-64">
145
+ {isLoading ? (
146
+ <div className="flex items-center justify-center h-32">
147
+ <Loader2 className="h-5 w-5 animate-spin text-gray-400" />
148
+ </div>
149
+ ) : filteredFonts.length === 0 ? (
150
+ <div className="flex items-center justify-center h-32 text-xs text-gray-500">
151
+ フォントが見つかりません
152
+ </div>
153
+ ) : (
154
+ <div className="p-1">
155
+ {filteredFonts.map((font) => (
156
+ <button
157
+ key={font.family}
158
+ onClick={() => handleSelectFont(font)}
159
+ onMouseEnter={() => handleFontHover(font)}
160
+ className={`w-full flex items-center justify-between px-2 py-1.5 text-xs rounded transition-colors ${
161
+ value === font.family
162
+ ? 'bg-[#0d99ff] text-white'
163
+ : 'text-gray-300 hover:bg-[#383838] hover:text-white'
164
+ }`}
165
+ >
166
+ <span
167
+ className="truncate"
168
+ style={getFontPreviewStyle(font.family)}
169
+ >
170
+ {font.family}
171
+ </span>
172
+ {value === font.family && (
173
+ <Check className="h-3.5 w-3.5 shrink-0 ml-2" />
174
+ )}
175
+ </button>
176
+ ))}
177
+ </div>
178
+ )}
179
+ </ScrollArea>
180
+
181
+ {/* フッター情報 */}
182
+ <div className="p-2 border-t border-[#444444]">
183
+ <p className="text-[10px] text-gray-500 text-center">
184
+ {filteredFonts.length}件のフォント
185
+ </p>
186
+ </div>
187
+ </PopoverContent>
188
+ </Popover>
189
+ );
190
+ }
191
+
192
+ export default GoogleFontPicker;
@@ -0,0 +1,206 @@
1
+ 'use client';
2
+
3
+ import { Button } from '../../../components/ui/button';
4
+ import { Input } from '../../../components/ui/input';
5
+ import { Label } from '../../../components/ui/label';
6
+ import { Slider } from '../../../components/ui/slider';
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from '../../../components/ui/select';
14
+ import {
15
+ Collapsible,
16
+ CollapsibleContent,
17
+ CollapsibleTrigger,
18
+ } from '../../../components/ui/collapsible';
19
+ import { Image, ChevronRight, Upload, Trash2 } from 'lucide-react';
20
+ import { IMAGE_FILL_MODES } from '../../constants';
21
+ import {
22
+ getBackgroundSizeFromFillMode,
23
+ getBackgroundRepeatFromFillMode,
24
+ } from '../../utils/style-utils';
25
+ import type { SelectedElementInfo, ImageFillMode } from '../../types';
26
+
27
+ interface ImageSectionProps {
28
+ selectedElement: SelectedElementInfo;
29
+ open: boolean;
30
+ onOpenChange: (open: boolean) => void;
31
+ onStyleChange: (styles: Record<string, string>) => void;
32
+ }
33
+
34
+ export function ImageSection({
35
+ selectedElement,
36
+ open,
37
+ onOpenChange,
38
+ onStyleChange,
39
+ }: ImageSectionProps) {
40
+ const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
41
+ const file = e.target.files?.[0];
42
+ if (!file) return;
43
+
44
+ const reader = new FileReader();
45
+ reader.onload = (event) => {
46
+ const dataUrl = event.target?.result as string;
47
+ onStyleChange({
48
+ backgroundImage: `url(${dataUrl})`,
49
+ backgroundSize: getBackgroundSizeFromFillMode(selectedElement.imageFillMode),
50
+ backgroundPosition: `${selectedElement.imagePositionX}% ${selectedElement.imagePositionY}%`,
51
+ backgroundRepeat: getBackgroundRepeatFromFillMode(selectedElement.imageFillMode),
52
+ });
53
+ };
54
+ reader.readAsDataURL(file);
55
+ };
56
+
57
+ const handleClearImage = () => {
58
+ onStyleChange({
59
+ backgroundImage: 'none',
60
+ backgroundSize: 'auto',
61
+ backgroundPosition: 'center',
62
+ backgroundRepeat: 'no-repeat',
63
+ });
64
+ };
65
+
66
+ const handleFillModeChange = (value: ImageFillMode) => {
67
+ onStyleChange({
68
+ backgroundSize: getBackgroundSizeFromFillMode(value),
69
+ backgroundRepeat: getBackgroundRepeatFromFillMode(value),
70
+ });
71
+ };
72
+
73
+ return (
74
+ <Collapsible open={open} onOpenChange={onOpenChange}>
75
+ <CollapsibleTrigger className="flex items-center justify-between w-full py-2 text-xs font-medium text-gray-300 hover:text-white border-t border-[#444444]">
76
+ <span className="flex items-center gap-2">
77
+ <Image className="w-3.5 h-3.5" />
78
+ 画像
79
+ </span>
80
+ <ChevronRight
81
+ className={`w-3.5 h-3.5 transition-transform ${open ? 'rotate-90' : ''}`}
82
+ />
83
+ </CollapsibleTrigger>
84
+ <CollapsibleContent className="space-y-2 pb-3">
85
+ {/* 画像アップロード */}
86
+ <div>
87
+ <Label className="text-[10px] text-gray-500">背景画像</Label>
88
+ <div className="flex items-center gap-2 mt-1">
89
+ <label className="flex-1 flex items-center justify-center gap-2 h-7 text-xs bg-[#383838] border border-[#444444] text-white rounded cursor-pointer hover:bg-[#4a4a4a]">
90
+ <Upload className="h-3 w-3" />
91
+ 画像を選択
92
+ <input
93
+ type="file"
94
+ accept="image/*"
95
+ onChange={handleImageUpload}
96
+ className="hidden"
97
+ />
98
+ </label>
99
+ <Button
100
+ variant="ghost"
101
+ size="icon"
102
+ className="h-7 w-7 hover:bg-[#4a4a4a]"
103
+ onClick={handleClearImage}
104
+ >
105
+ <Trash2 className="h-3 w-3" />
106
+ </Button>
107
+ </div>
108
+ </div>
109
+
110
+ {/* フィルモード選択 */}
111
+ {selectedElement.hasBackgroundImage && (
112
+ <>
113
+ <div>
114
+ <Label className="text-[10px] text-gray-500">フィルモード</Label>
115
+ <Select
116
+ value={selectedElement.imageFillMode}
117
+ onValueChange={handleFillModeChange}
118
+ >
119
+ <SelectTrigger className="h-7 text-xs bg-[#383838] border-[#444444] text-white">
120
+ <SelectValue />
121
+ </SelectTrigger>
122
+ <SelectContent>
123
+ {IMAGE_FILL_MODES.map((mode) => (
124
+ <SelectItem key={mode.value} value={mode.value}>
125
+ {mode.label}
126
+ </SelectItem>
127
+ ))}
128
+ </SelectContent>
129
+ </Select>
130
+ </div>
131
+
132
+ {/* 位置調整(タイルモード以外) */}
133
+ {selectedElement.imageFillMode !== 'tile' && (
134
+ <div className="grid grid-cols-2 gap-2">
135
+ <div>
136
+ <Label className="text-[10px] text-gray-500">X位置</Label>
137
+ <div className="flex items-center gap-2">
138
+ <Slider
139
+ value={[selectedElement.imagePositionX]}
140
+ onValueChange={([value]) => {
141
+ onStyleChange({
142
+ backgroundPosition: `${value}% ${selectedElement.imagePositionY}%`,
143
+ });
144
+ }}
145
+ min={0}
146
+ max={100}
147
+ step={1}
148
+ className="flex-1"
149
+ />
150
+ <span className="text-xs text-gray-400 w-8">
151
+ {selectedElement.imagePositionX}%
152
+ </span>
153
+ </div>
154
+ </div>
155
+ <div>
156
+ <Label className="text-[10px] text-gray-500">Y位置</Label>
157
+ <div className="flex items-center gap-2">
158
+ <Slider
159
+ value={[selectedElement.imagePositionY]}
160
+ onValueChange={([value]) => {
161
+ onStyleChange({
162
+ backgroundPosition: `${selectedElement.imagePositionX}% ${value}%`,
163
+ });
164
+ }}
165
+ min={0}
166
+ max={100}
167
+ step={1}
168
+ className="flex-1"
169
+ />
170
+ <span className="text-xs text-gray-400 w-8">
171
+ {selectedElement.imagePositionY}%
172
+ </span>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ )}
177
+
178
+ {/* スケール(タイルモードのみ) */}
179
+ {selectedElement.imageFillMode === 'tile' && (
180
+ <div>
181
+ <Label className="text-[10px] text-gray-500">スケール</Label>
182
+ <div className="flex items-center gap-2">
183
+ <Slider
184
+ value={[selectedElement.imageScale]}
185
+ onValueChange={([value]) => {
186
+ onStyleChange({
187
+ backgroundSize: `${value}%`,
188
+ });
189
+ }}
190
+ min={10}
191
+ max={200}
192
+ step={1}
193
+ className="flex-1"
194
+ />
195
+ <span className="text-xs text-gray-400 w-10">
196
+ {selectedElement.imageScale}%
197
+ </span>
198
+ </div>
199
+ </div>
200
+ )}
201
+ </>
202
+ )}
203
+ </CollapsibleContent>
204
+ </Collapsible>
205
+ );
206
+ }
@@ -0,0 +1,125 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { Input } from '../../../components/ui/input';
3
+ import { Label } from '../../../components/ui/label';
4
+ import {
5
+ Collapsible,
6
+ CollapsibleContent,
7
+ CollapsibleTrigger,
8
+ } from '../../../components/ui/collapsible';
9
+ import { Image as ImageIcon, ChevronRight, Upload } from 'lucide-react';
10
+ import { uploadEditorImage } from '../../../lib/firebase/storage';
11
+ import type { SelectedElementInfo } from '../../types';
12
+
13
+ interface ImgSrcSectionProps {
14
+ selectedElement: SelectedElementInfo;
15
+ open: boolean;
16
+ onOpenChange: (open: boolean) => void;
17
+ onAttributeChange: (attrs: Record<string, string>) => void;
18
+ /** 選択中要素の現在のsrc(iframeから取得済みのもの) */
19
+ currentSrc?: string;
20
+ }
21
+
22
+ /**
23
+ * [移植時の追加] <img> 要素の src を差し替えるセクション。
24
+ * ファイル選択でアップロード(public/media/uploads/ に実ファイル保存)するか、
25
+ * URLを直接指定できる。既存の ImageSection は「背景画像」用で <img> は扱えなかった。
26
+ */
27
+ export function ImgSrcSection({
28
+ selectedElement,
29
+ open,
30
+ onOpenChange,
31
+ onAttributeChange,
32
+ currentSrc = '',
33
+ }: ImgSrcSectionProps) {
34
+ const [urlValue, setUrlValue] = useState(currentSrc);
35
+ const [uploading, setUploading] = useState(false);
36
+ const fileRef = useRef<HTMLInputElement>(null);
37
+
38
+ // 選択要素が変わったとき / メディアライブラリなど外部から src が差し替わったときに追従する
39
+ // (ローカルstateのままだとプレビューとURL欄が古い画像を指し続ける)
40
+ useEffect(() => {
41
+ setUrlValue(currentSrc);
42
+ }, [currentSrc, selectedElement.id]);
43
+
44
+ if (selectedElement.tagName?.toUpperCase() !== 'IMG') return null;
45
+
46
+ const applySrc = (src: string) => {
47
+ if (!src) return;
48
+ onAttributeChange({ src });
49
+ setUrlValue(src);
50
+ };
51
+
52
+ const handleFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
53
+ const file = e.target.files?.[0];
54
+ if (!file) return;
55
+ setUploading(true);
56
+ try {
57
+ const { storageUrl } = await uploadEditorImage(file, { fileName: file.name });
58
+ applySrc(storageUrl);
59
+ } catch (err) {
60
+ console.error('[ImgSrcSection] 画像の差し替えに失敗:', err);
61
+ alert(`画像の差し替えに失敗しました: ${err instanceof Error ? err.message : String(err)}`);
62
+ } finally {
63
+ setUploading(false);
64
+ if (fileRef.current) fileRef.current.value = '';
65
+ }
66
+ };
67
+
68
+ return (
69
+ <Collapsible open={open} onOpenChange={onOpenChange}>
70
+ <CollapsibleTrigger className="flex items-center justify-between w-full py-2 text-xs font-medium text-gray-300 hover:text-white border-t border-[#444444]">
71
+ <span className="flex items-center gap-2">
72
+ <ImageIcon className="w-3.5 h-3.5" />
73
+ 画像の差し替え
74
+ </span>
75
+ <ChevronRight
76
+ className={`w-3.5 h-3.5 transition-transform ${open ? 'rotate-90' : ''}`}
77
+ />
78
+ </CollapsibleTrigger>
79
+ <CollapsibleContent className="space-y-2 pb-3">
80
+ {/* プレビュー */}
81
+ {urlValue && (
82
+ <div className="flex items-center justify-center rounded border border-[#444444] bg-[#2c2c2c] p-2">
83
+ <img
84
+ src={urlValue}
85
+ alt=""
86
+ className="max-h-24 max-w-full object-contain"
87
+ />
88
+ </div>
89
+ )}
90
+
91
+ {/* ファイル選択 */}
92
+ <div>
93
+ <Label className="text-[10px] text-gray-500">ファイルから差し替え</Label>
94
+ <label className="mt-1 flex h-7 cursor-pointer items-center justify-center gap-2 rounded border border-[#444444] bg-[#383838] text-xs text-white hover:bg-[#4a4a4a]">
95
+ <Upload className="h-3 w-3" />
96
+ {uploading ? 'アップロード中…' : '画像を選択'}
97
+ <input
98
+ ref={fileRef}
99
+ type="file"
100
+ accept="image/*"
101
+ onChange={handleFile}
102
+ className="hidden"
103
+ disabled={uploading}
104
+ />
105
+ </label>
106
+ </div>
107
+
108
+ {/* URL直接指定 */}
109
+ <div>
110
+ <Label className="text-[10px] text-gray-500">画像のURL / パス</Label>
111
+ <Input
112
+ value={urlValue}
113
+ onChange={(e) => setUrlValue(e.target.value)}
114
+ onBlur={() => applySrc(urlValue)}
115
+ onKeyDown={(e) => {
116
+ if (e.key === 'Enter') applySrc(urlValue);
117
+ }}
118
+ placeholder="/media/xxx.png"
119
+ className="mt-1 h-7 border-[#444444] bg-[#383838] text-xs text-white"
120
+ />
121
+ </div>
122
+ </CollapsibleContent>
123
+ </Collapsible>
124
+ );
125
+ }