@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.
- package/LICENSE +21 -0
- package/NOTICE +52 -0
- package/README.md +143 -0
- package/dist/editor.css +2 -0
- package/package.json +94 -0
- package/src/components/DeckSlideRender.tsx +21 -0
- package/src/components/SaveNote.tsx +21 -0
- package/src/components/auth/AuthProvider.tsx +44 -0
- package/src/components/ui/accordion.tsx +57 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +76 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +11 -0
- package/src/components/ui/context-menu.tsx +200 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/scrubbable-label.tsx +95 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/components/viewer/useDeck.ts +58 -0
- package/src/editor/EditorContext.tsx +632 -0
- package/src/editor/EditorToolbar.tsx +446 -0
- package/src/editor/FrontendVisualEditor.tsx +2595 -0
- package/src/editor/autosave.ts +34 -0
- package/src/editor/components/AiPromptPopover.tsx +378 -0
- package/src/editor/components/BreakpointGuides.tsx +323 -0
- package/src/editor/components/BreakpointSelector.tsx +180 -0
- package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
- package/src/editor/components/CssEditorDialog.tsx +168 -0
- package/src/editor/components/EditorCanvas.tsx +987 -0
- package/src/editor/components/EditorContextMenu.tsx +515 -0
- package/src/editor/components/EditorFooter.tsx +81 -0
- package/src/editor/components/EditorHeader.tsx +524 -0
- package/src/editor/components/EditorLayerPanel.tsx +1197 -0
- package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
- package/src/editor/components/EditorSidebar.tsx +226 -0
- package/src/editor/components/FigmaColorPicker.tsx +1577 -0
- package/src/editor/components/HtmlEditorDialog.tsx +71 -0
- package/src/editor/components/HtmlImportDialog.tsx +346 -0
- package/src/editor/components/JsEditorDialog.tsx +175 -0
- package/src/editor/components/LayerTreeItem.tsx +494 -0
- package/src/editor/components/MasterComponentEditor.tsx +846 -0
- package/src/editor/components/MediaLibraryDialog.tsx +447 -0
- package/src/editor/components/PageSettingsDialog.tsx +776 -0
- package/src/editor/components/VariablePicker.tsx +355 -0
- package/src/editor/components/VariablesDialog.tsx +564 -0
- package/src/editor/components/VariantEditor.tsx +687 -0
- package/src/editor/components/VariantMatrix.tsx +507 -0
- package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
- package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
- package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
- package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
- package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
- package/src/editor/components/component-panel/VariantList.tsx +260 -0
- package/src/editor/components/component-panel/index.ts +12 -0
- package/src/editor/components/index.ts +48 -0
- package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
- package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
- package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
- package/src/editor/components/multi-page/PageLabel.tsx +34 -0
- package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
- package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
- package/src/editor/components/multi-page/index.ts +7 -0
- package/src/editor/components/ppt/PptChrome.tsx +2508 -0
- package/src/editor/components/ppt/PptComments.tsx +589 -0
- package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
- package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
- package/src/editor/components/ppt/PptNotes.tsx +246 -0
- package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
- package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
- package/src/editor/components/property-panel/BorderSection.tsx +104 -0
- package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
- package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
- package/src/editor/components/property-panel/EffectSection.tsx +157 -0
- package/src/editor/components/property-panel/FillSection.tsx +69 -0
- package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
- package/src/editor/components/property-panel/ImageSection.tsx +206 -0
- package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
- package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
- package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
- package/src/editor/components/property-panel/LinkSection.tsx +103 -0
- package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
- package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
- package/src/editor/components/property-panel/TypographySection.tsx +230 -0
- package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
- package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
- package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
- package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
- package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
- package/src/editor/components/property-panel/index.ts +24 -0
- package/src/editor/components/property-panel/unit-utils.ts +261 -0
- package/src/editor/components/shell/EditorTopBar.tsx +392 -0
- package/src/editor/components/shell/LeftPanel.tsx +47 -0
- package/src/editor/components/shell/PagesPanel.tsx +118 -0
- package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
- package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
- package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
- package/src/editor/components/variables-panel/index.ts +7 -0
- package/src/editor/constants.ts +468 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
- package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
- package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
- package/src/editor/contexts/EditorRefsContext.tsx +204 -0
- package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
- package/src/editor/contexts/EditorToolContext.tsx +58 -0
- package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
- package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
- package/src/editor/contexts/EditorViewContext.tsx +101 -0
- package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
- package/src/editor/contexts/index.ts +17 -0
- package/src/editor/editor-skin.css +122 -0
- package/src/editor/hooks/index.ts +38 -0
- package/src/editor/hooks/useAiReplace.ts +397 -0
- package/src/editor/hooks/useAltMeasure.ts +67 -0
- package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
- package/src/editor/hooks/useCanvasControls.ts +1222 -0
- package/src/editor/hooks/useComponentEditMode.ts +658 -0
- package/src/editor/hooks/useComponentInstances.ts +515 -0
- package/src/editor/hooks/useContextMenuHandler.ts +157 -0
- package/src/editor/hooks/useCoordinateTransform.ts +207 -0
- package/src/editor/hooks/useDragResize.ts +1317 -0
- package/src/editor/hooks/useDrawingMode.ts +472 -0
- package/src/editor/hooks/useEditorColors.ts +144 -0
- package/src/editor/hooks/useEditorMessages.ts +108 -0
- package/src/editor/hooks/useElementActions.ts +1754 -0
- package/src/editor/hooks/useElementSelection.ts +1573 -0
- package/src/editor/hooks/useFocusManagement.ts +249 -0
- package/src/editor/hooks/useGoogleFonts.ts +305 -0
- package/src/editor/hooks/useIframeInitializer.ts +584 -0
- package/src/editor/hooks/useIframeSetup.ts +360 -0
- package/src/editor/hooks/useImageUpload.ts +270 -0
- package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
- package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
- package/src/editor/hooks/useMarqueeSelection.ts +388 -0
- package/src/editor/hooks/useMediaLibrary.ts +129 -0
- package/src/editor/hooks/usePageSettingsManager.ts +739 -0
- package/src/editor/hooks/useResizablePanel.ts +163 -0
- package/src/editor/hooks/useRichPaste.ts +255 -0
- package/src/editor/hooks/useTouchGestures.ts +197 -0
- package/src/editor/index.ts +27 -0
- package/src/editor/types/page-settings.ts +513 -0
- package/src/editor/types.ts +382 -0
- package/src/editor/utils/align-elements.ts +169 -0
- package/src/editor/utils/api-json.ts +21 -0
- package/src/editor/utils/aspect-lock.ts +50 -0
- package/src/editor/utils/component-renderer.ts +702 -0
- package/src/editor/utils/component-sync.ts +845 -0
- package/src/editor/utils/crop-mode.ts +265 -0
- package/src/editor/utils/dom-utils.ts +2238 -0
- package/src/editor/utils/element-effects.ts +218 -0
- package/src/editor/utils/eyedropper.ts +35 -0
- package/src/editor/utils/figma-export.ts +560 -0
- package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
- package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
- package/src/editor/utils/figma-paste.ts +2262 -0
- package/src/editor/utils/flex-reorder.ts +231 -0
- package/src/editor/utils/flex-utils.ts +1230 -0
- package/src/editor/utils/geometry.ts +232 -0
- package/src/editor/utils/grid-layout.ts +210 -0
- package/src/editor/utils/html-utils.ts +470 -0
- package/src/editor/utils/index.ts +11 -0
- package/src/editor/utils/ink-style.ts +57 -0
- package/src/editor/utils/inline-format.ts +236 -0
- package/src/editor/utils/measure-distance.ts +120 -0
- package/src/editor/utils/override-apply.ts +526 -0
- package/src/editor/utils/override-detection.ts +613 -0
- package/src/editor/utils/paste-processors.ts +609 -0
- package/src/editor/utils/paste-sanitizer.ts +244 -0
- package/src/editor/utils/restore-flow.ts +188 -0
- package/src/editor/utils/shape-library.ts +171 -0
- package/src/editor/utils/slide-root.ts +29 -0
- package/src/editor/utils/smart-guides.ts +218 -0
- package/src/editor/utils/style-utils.ts +685 -0
- package/src/editor/utils/table-edit.ts +151 -0
- package/src/editor/utils/tailwind-mappings.ts +778 -0
- package/src/editor/utils/tailwind-utils.ts +804 -0
- package/src/editor/utils/text-highlight.ts +216 -0
- package/src/editor/utils/theme-to-variables.ts +341 -0
- package/src/editor/utils/variant-resolver.ts +456 -0
- package/src/editor/utils/viewport-utils.ts +295 -0
- package/src/hooks/useEditorHistory.ts +195 -0
- package/src/hooks/useEditorShortcuts.ts +260 -0
- package/src/index.ts +40 -0
- package/src/io.ts +134 -0
- package/src/lib/agent/slide-agent/types.ts +547 -0
- package/src/lib/agent/website-agent/types.ts +805 -0
- package/src/lib/api/auth-fetch.ts +58 -0
- package/src/lib/deck.ts +69 -0
- package/src/lib/export.ts +47 -0
- package/src/lib/firebase/config.ts +29 -0
- package/src/lib/firebase/css-variables.ts +378 -0
- package/src/lib/firebase/editor-components.ts +677 -0
- package/src/lib/firebase/storage.ts +469 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/editor.css +52 -0
- package/src/styles/skin.css +122 -0
- package/src/types/css-variables.ts +645 -0
- package/src/types/editor-components.ts +944 -0
- package/src/types/editor.ts +420 -0
- package/src/types/page-master.ts +197 -0
- package/src/types/page.ts +419 -0
- package/src/types/slide.ts +402 -0
- package/src/types/website-theme.ts +788 -0
- package/src/types/website.ts +374 -0
- package/src/vendor/firebase-firestore.ts +13 -0
- 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
|
+
}
|