@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,104 @@
1
+ 'use client';
2
+
3
+ import { Input } from '../../../components/ui/input';
4
+ import { Label } from '../../../components/ui/label';
5
+ import {
6
+ Select,
7
+ SelectContent,
8
+ SelectItem,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ } from '../../../components/ui/select';
12
+ import {
13
+ Collapsible,
14
+ CollapsibleContent,
15
+ CollapsibleTrigger,
16
+ } from '../../../components/ui/collapsible';
17
+ import { Circle, ChevronRight } from 'lucide-react';
18
+ import { rgbToHex } from '../../utils/style-utils';
19
+ import { BORDER_STYLES } from '../../constants';
20
+ import type { SelectedElementInfo } from '../../types';
21
+
22
+ interface BorderSectionProps {
23
+ selectedElement: SelectedElementInfo;
24
+ open: boolean;
25
+ onOpenChange: (open: boolean) => void;
26
+ onStyleChange: (styles: Record<string, string>) => void;
27
+ }
28
+
29
+ export function BorderSection({
30
+ selectedElement,
31
+ open,
32
+ onOpenChange,
33
+ onStyleChange,
34
+ }: BorderSectionProps) {
35
+ return (
36
+ <Collapsible open={open} onOpenChange={onOpenChange}>
37
+ <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]">
38
+ <span className="flex items-center gap-2">
39
+ <Circle className="w-3.5 h-3.5" />
40
+
41
+ </span>
42
+ <ChevronRight
43
+ className={`w-3.5 h-3.5 transition-transform ${open ? 'rotate-90' : ''}`}
44
+ />
45
+ </CollapsibleTrigger>
46
+ <CollapsibleContent className="space-y-2 pb-3">
47
+ <div className="grid grid-cols-2 gap-2">
48
+ <div>
49
+ <Label className="text-[10px] text-gray-500">線幅</Label>
50
+ <Input
51
+ type="number"
52
+ value={Math.round(selectedElement.borderWidth)}
53
+ onChange={(e) =>
54
+ onStyleChange({
55
+ borderWidth: `${e.target.value}px`,
56
+ borderStyle:
57
+ selectedElement.borderStyle === 'none'
58
+ ? 'solid'
59
+ : selectedElement.borderStyle,
60
+ })
61
+ }
62
+ className="h-7 text-xs bg-[#383838] border-[#444444] text-white"
63
+ />
64
+ </div>
65
+ <div>
66
+ <Label className="text-[10px] text-gray-500">スタイル</Label>
67
+ <Select
68
+ value={selectedElement.borderStyle}
69
+ onValueChange={(value) => onStyleChange({ borderStyle: value })}
70
+ >
71
+ <SelectTrigger className="h-7 text-xs bg-[#383838] border-[#444444] text-white">
72
+ <SelectValue />
73
+ </SelectTrigger>
74
+ <SelectContent>
75
+ {BORDER_STYLES.map((style) => (
76
+ <SelectItem key={style.value} value={style.value}>
77
+ {style.label}
78
+ </SelectItem>
79
+ ))}
80
+ </SelectContent>
81
+ </Select>
82
+ </div>
83
+ </div>
84
+ <div>
85
+ <Label className="text-[10px] text-gray-500">線色</Label>
86
+ <div className="flex items-center gap-2 mt-1">
87
+ <input
88
+ type="color"
89
+ value={rgbToHex(selectedElement.borderColor)}
90
+ onChange={(e) => onStyleChange({ borderColor: e.target.value })}
91
+ className="w-8 h-7 rounded border border-[#444444] bg-transparent cursor-pointer"
92
+ />
93
+ <Input
94
+ type="text"
95
+ value={rgbToHex(selectedElement.borderColor)}
96
+ onChange={(e) => onStyleChange({ borderColor: e.target.value })}
97
+ className="flex-1 h-7 text-xs bg-[#383838] border-[#444444] text-white font-mono"
98
+ />
99
+ </div>
100
+ </div>
101
+ </CollapsibleContent>
102
+ </Collapsible>
103
+ );
104
+ }
@@ -0,0 +1,237 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CompactNumberInput - Figmaライクな数値入力コンポーネント
5
+ *
6
+ * 特徴:
7
+ * - コンパクトなサイズでオーバーフローしない
8
+ * - 空値も許容(削除可能)
9
+ * - フォーカス時のみ編集可能、非フォーカス時は値表示
10
+ * - 上下矢印キーで値を増減
11
+ */
12
+
13
+ import React, { useState, useRef, useEffect, useCallback } from "react";
14
+ import { cn } from "../../../lib/utils";
15
+
16
+ interface CompactNumberInputProps {
17
+ value: number;
18
+ onChange: (value: number) => void;
19
+ min?: number;
20
+ max?: number;
21
+ step?: number;
22
+ suffix?: string;
23
+ placeholder?: string;
24
+ className?: string;
25
+ disabled?: boolean;
26
+ }
27
+
28
+ export function CompactNumberInput({
29
+ value,
30
+ onChange,
31
+ min = -Infinity,
32
+ max = Infinity,
33
+ step = 1,
34
+ suffix,
35
+ placeholder = "0",
36
+ className,
37
+ disabled = false,
38
+ }: CompactNumberInputProps) {
39
+ const [isEditing, setIsEditing] = useState(false);
40
+ const [inputValue, setInputValue] = useState(String(value));
41
+ const inputRef = useRef<HTMLInputElement>(null);
42
+
43
+ const isScrubbing = useRef(false);
44
+ const hasMoved = useRef(false);
45
+ const startX = useRef(0);
46
+ const startValue = useRef(0);
47
+
48
+ // 外部値が変更されたら入力値を更新
49
+ useEffect(() => {
50
+ if (!isEditing && !isScrubbing.current) {
51
+ setInputValue(String(value));
52
+ }
53
+ }, [value, isEditing]);
54
+
55
+ // 値をクランプして変更を通知
56
+ const commitValue = useCallback(
57
+ (val: string) => {
58
+ const parsed = parseFloat(val);
59
+ if (isNaN(parsed)) {
60
+ // 空の場合は0にリセット
61
+ onChange(0);
62
+ setInputValue("0");
63
+ } else {
64
+ const clamped = Math.max(min, Math.min(max, parsed));
65
+ onChange(clamped);
66
+ setInputValue(String(clamped));
67
+ }
68
+ },
69
+ [onChange, min, max],
70
+ );
71
+
72
+ const handleFocus = () => {
73
+ if (isScrubbing.current) return;
74
+ setIsEditing(true);
75
+ setInputValue(String(value));
76
+ // 全選択
77
+ setTimeout(() => {
78
+ inputRef.current?.select();
79
+ }, 0);
80
+ };
81
+
82
+ const handleBlur = () => {
83
+ if (isScrubbing.current) return;
84
+ setIsEditing(false);
85
+ commitValue(inputValue);
86
+ };
87
+
88
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
89
+ const val = e.target.value;
90
+ // 数値、マイナス、小数点のみ許可
91
+ if (val === "" || val === "-" || /^-?\d*\.?\d*$/.test(val)) {
92
+ setInputValue(val);
93
+ // 有効な数値ならリアルタイムで反映
94
+ const parsed = parseFloat(val);
95
+ if (!isNaN(parsed)) {
96
+ const clamped = Math.max(min, Math.min(max, parsed));
97
+ onChange(clamped);
98
+ }
99
+ }
100
+ };
101
+
102
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
103
+ if (e.key === "Enter") {
104
+ commitValue(inputValue);
105
+ inputRef.current?.blur();
106
+ } else if (e.key === "Escape") {
107
+ setInputValue(String(value));
108
+ inputRef.current?.blur();
109
+ } else if (e.key === "ArrowUp") {
110
+ e.preventDefault();
111
+ const amount = e.shiftKey ? step * 10 : step;
112
+ const newVal = (parseFloat(inputValue) || 0) + amount;
113
+ const clamped = Math.max(min, Math.min(max, newVal));
114
+ setInputValue(String(clamped));
115
+ onChange(clamped);
116
+ } else if (e.key === "ArrowDown") {
117
+ e.preventDefault();
118
+ const amount = e.shiftKey ? step * 10 : step;
119
+ const newVal = (parseFloat(inputValue) || 0) - amount;
120
+ const clamped = Math.max(min, Math.min(max, newVal));
121
+ setInputValue(String(clamped));
122
+ onChange(clamped);
123
+ }
124
+ };
125
+
126
+ // スクラブ操作のハンドラ
127
+ const handleMouseDown = (e: React.MouseEvent) => {
128
+ // 入力中はスクラブさせない
129
+ if (isEditing) return;
130
+
131
+ e.preventDefault();
132
+ isScrubbing.current = true;
133
+ hasMoved.current = false;
134
+ startX.current = e.clientX;
135
+ startValue.current = value;
136
+
137
+ document.body.style.cursor = "ew-resize";
138
+
139
+ const handleMouseMove = (mmEvent: MouseEvent) => {
140
+ if (!isScrubbing.current) return;
141
+
142
+ const delta = mmEvent.clientX - startX.current;
143
+ // 小さな移動は無視(クリック判定のため)
144
+ if (Math.abs(delta) < 3) return;
145
+
146
+ hasMoved.current = true;
147
+
148
+ // Shiftキーで10倍、Altキーで0.1倍
149
+ let multiplier = 1;
150
+ if (mmEvent.shiftKey) multiplier = 10;
151
+ if (mmEvent.altKey) multiplier = 0.1;
152
+
153
+ const change = Math.round(delta * step * multiplier);
154
+ const newVal = startValue.current + change;
155
+ const clamped = Math.max(min, Math.min(max, newVal));
156
+
157
+ setInputValue(String(clamped));
158
+ onChange(clamped);
159
+ };
160
+
161
+ const handleMouseUp = () => {
162
+ const didMove = hasMoved.current;
163
+ isScrubbing.current = false;
164
+ hasMoved.current = false;
165
+ document.body.style.cursor = "";
166
+ window.removeEventListener("mousemove", handleMouseMove);
167
+ window.removeEventListener("mouseup", handleMouseUp);
168
+
169
+ // ドラッグしていなければ編集モードに入る(クリック扱い)
170
+ if (!didMove) {
171
+ setIsEditing(true);
172
+ setInputValue(String(value));
173
+ setTimeout(() => {
174
+ inputRef.current?.focus();
175
+ inputRef.current?.select();
176
+ }, 0);
177
+ }
178
+ };
179
+
180
+ window.addEventListener("mousemove", handleMouseMove);
181
+ window.addEventListener("mouseup", handleMouseUp);
182
+ };
183
+
184
+ // ダブルクリックで編集モードに入る
185
+ const handleDoubleClick = (e: React.MouseEvent) => {
186
+ if (disabled) return;
187
+ e.preventDefault();
188
+ e.stopPropagation();
189
+ setIsEditing(true);
190
+ setInputValue(String(value));
191
+ setTimeout(() => {
192
+ inputRef.current?.focus();
193
+ inputRef.current?.select();
194
+ }, 0);
195
+ };
196
+
197
+ return (
198
+ <div
199
+ className={cn(
200
+ "relative flex items-center bg-[#383838] rounded border border-[#444444] h-5 group",
201
+ disabled && "opacity-50 cursor-not-allowed",
202
+ !isEditing && !disabled && "cursor-ew-resize hover:border-[#555]",
203
+ className,
204
+ )}
205
+ onMouseDown={!disabled ? handleMouseDown : undefined}
206
+ onDoubleClick={!disabled ? handleDoubleClick : undefined}
207
+ >
208
+ <input
209
+ ref={inputRef}
210
+ type="text"
211
+ inputMode="numeric"
212
+ value={inputValue}
213
+ onChange={handleChange}
214
+ onFocus={handleFocus}
215
+ onBlur={handleBlur}
216
+ onKeyDown={handleKeyDown}
217
+ placeholder={placeholder}
218
+ disabled={disabled}
219
+ className={cn(
220
+ "w-full h-full bg-transparent text-white text-center text-[10px] px-1 outline-none",
221
+ "focus:ring-1 focus:ring-[#0d99ff] rounded",
222
+ !isEditing &&
223
+ !disabled &&
224
+ "cursor-ew-resize selection:bg-transparent", // スクラブ中はカーソル変更&選択無効
225
+ suffix && "pr-4",
226
+ )}
227
+ />
228
+ {suffix && (
229
+ <span className="absolute right-1 text-[9px] text-gray-500 pointer-events-none">
230
+ {suffix}
231
+ </span>
232
+ )}
233
+ </div>
234
+ );
235
+ }
236
+
237
+ export default CompactNumberInput;
@@ -0,0 +1,148 @@
1
+ 'use client';
2
+
3
+ import React, { useRef, useState } from 'react';
4
+ import { ChevronDown } from 'lucide-react';
5
+ import { cn } from '../../../lib/utils';
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuTrigger,
11
+ DropdownMenuSeparator,
12
+ DropdownMenuLabel,
13
+ } from '../../../components/ui/dropdown-menu';
14
+ import { CompactNumberInput } from './CompactNumberInput';
15
+
16
+ export type SizeMode = 'fixed' | 'fill' | 'hug';
17
+
18
+ interface CompactSizeInputProps {
19
+ value: number;
20
+ mode: SizeMode;
21
+ onChangeValue: (value: number) => void;
22
+ onChangeMode: (mode: SizeMode) => void;
23
+ label?: React.ReactNode; // W or H label component
24
+ disabled?: boolean;
25
+ min?: number;
26
+ max?: number;
27
+ canFill?: boolean; // "Fill container" is available
28
+ canHug?: boolean; // "Hug contents" is available
29
+ }
30
+
31
+ export function CompactSizeInput({
32
+ value,
33
+ mode,
34
+ onChangeValue,
35
+ onChangeMode,
36
+ label,
37
+ disabled = false,
38
+ min = 0,
39
+ max,
40
+ canFill = false,
41
+ canHug = false,
42
+ }: CompactSizeInputProps) {
43
+ const [isOpen, setIsOpen] = useState(false);
44
+
45
+ // 表示するコンテンツの決定
46
+ const renderContent = () => {
47
+ if (mode === 'fixed') {
48
+ return (
49
+ <CompactNumberInput
50
+ value={value}
51
+ onChange={onChangeValue}
52
+ min={min}
53
+ max={max}
54
+ className="flex-1 border-0 bg-transparent h-full"
55
+ disabled={disabled}
56
+ />
57
+ );
58
+ } else if (mode === 'fill') {
59
+ return (
60
+ <span className="flex-1 text-[9px] text-gray-400 px-1 truncate select-none flex items-center">
61
+ コンテナに合わせて拡大
62
+ </span>
63
+ );
64
+ } else if (mode === 'hug') {
65
+ return (
66
+ <span className="flex-1 text-[9px] text-gray-400 px-1 truncate select-none flex items-center">
67
+ コンテンツを内包
68
+ </span>
69
+ );
70
+ }
71
+ return null;
72
+ };
73
+
74
+ return (
75
+ <div className="flex items-center gap-1 group/size-input">
76
+ {/* Label area (W/H) */}
77
+ {label && <div className="shrink-0">{label}</div>}
78
+
79
+ {/* Input / Display area + Dropdown trigger */}
80
+ <div
81
+ className={cn(
82
+ "flex-1 flex items-center h-7 bg-[#383838] border border-[#444444] rounded relative",
83
+ disabled && "opacity-50 cursor-not-allowed",
84
+ isOpen && "ring-1 ring-[#0d99ff] border-[#0d99ff]"
85
+ )}
86
+ >
87
+ {/* Main Content */}
88
+ <div className="flex-1 min-w-0 h-full flex flex-col justify-center">
89
+ {renderContent()}
90
+ </div>
91
+
92
+ {/* Dropdown Trigger */}
93
+ <DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
94
+ <DropdownMenuTrigger asChild disabled={disabled}>
95
+ <button
96
+ className={cn(
97
+ "h-full px-1 flex items-center justify-center text-gray-500 hover:text-white transition-colors focus:outline-none",
98
+ // Fixedモードの時はホバー時のみ表示、それ以外は常時表示っぽく見せるが、
99
+ // デザイン的には右端に常に矢印があったほうがわかりやすいかも
100
+ )}
101
+ >
102
+ <ChevronDown className="h-3 w-3" />
103
+ </button>
104
+ </DropdownMenuTrigger>
105
+ <DropdownMenuContent align="end" className="w-48 bg-[#2c2c2c] border-[#444444] text-white">
106
+ <DropdownMenuLabel className="text-[10px] text-gray-500 font-normal">
107
+ リサイズ戦略
108
+ </DropdownMenuLabel>
109
+
110
+ <DropdownMenuSeparator className="bg-[#444444]" />
111
+
112
+ <DropdownMenuItem
113
+ className="text-xs focus:bg-[#383838] focus:text-white cursor-pointer flex justify-between"
114
+ onClick={() => onChangeMode('fixed')}
115
+ >
116
+ <span>幅を固定 (Fixed)</span>
117
+ {mode === 'fixed' && <span className="text-[#4fb8ff]">✓</span>}
118
+ </DropdownMenuItem>
119
+
120
+ <DropdownMenuItem
121
+ className={cn(
122
+ "text-xs focus:bg-[#383838] focus:text-white cursor-pointer flex justify-between",
123
+ !canFill && "opacity-50 cursor-not-allowed text-gray-500"
124
+ )}
125
+ disabled={!canFill}
126
+ onClick={() => canFill && onChangeMode('fill')}
127
+ >
128
+ <span>コンテナに合わせて拡大 (Fill)</span>
129
+ {mode === 'fill' && <span className="text-[#4fb8ff]">✓</span>}
130
+ </DropdownMenuItem>
131
+
132
+ <DropdownMenuItem
133
+ className={cn(
134
+ "text-xs focus:bg-[#383838] focus:text-white cursor-pointer flex justify-between",
135
+ !canHug && "opacity-50 cursor-not-allowed text-gray-500"
136
+ )}
137
+ disabled={!canHug}
138
+ onClick={() => canHug && onChangeMode('hug')}
139
+ >
140
+ <span>コンテンツを内包 (Hug)</span>
141
+ {mode === 'hug' && <span className="text-[#4fb8ff]">✓</span>}
142
+ </DropdownMenuItem>
143
+ </DropdownMenuContent>
144
+ </DropdownMenu>
145
+ </div>
146
+ </div>
147
+ );
148
+ }
@@ -0,0 +1,157 @@
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 { Circle, ChevronRight } from 'lucide-react';
12
+ import { buildBoxShadow } from '../../utils/style-utils';
13
+ import { DEFAULT_SHADOW } from '../../constants';
14
+ import type { SelectedElementInfo } from '../../types';
15
+
16
+ interface EffectSectionProps {
17
+ selectedElement: SelectedElementInfo;
18
+ open: boolean;
19
+ onOpenChange: (open: boolean) => void;
20
+ onStyleChange: (styles: Record<string, string>) => void;
21
+ }
22
+
23
+ export function EffectSection({
24
+ selectedElement,
25
+ open,
26
+ onOpenChange,
27
+ onStyleChange,
28
+ }: EffectSectionProps) {
29
+ const toggleShadow = () => {
30
+ if (selectedElement.hasShadow) {
31
+ onStyleChange({ boxShadow: 'none' });
32
+ } else {
33
+ onStyleChange({
34
+ boxShadow: buildBoxShadow(
35
+ DEFAULT_SHADOW.color,
36
+ DEFAULT_SHADOW.x,
37
+ DEFAULT_SHADOW.y,
38
+ DEFAULT_SHADOW.blur,
39
+ DEFAULT_SHADOW.spread
40
+ ),
41
+ });
42
+ }
43
+ };
44
+
45
+ return (
46
+ <Collapsible open={open} onOpenChange={onOpenChange}>
47
+ <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]">
48
+ <span className="flex items-center gap-2">
49
+ <Circle className="w-3.5 h-3.5" />
50
+ エフェクト
51
+ </span>
52
+ <ChevronRight
53
+ className={`w-3.5 h-3.5 transition-transform ${open ? 'rotate-90' : ''}`}
54
+ />
55
+ </CollapsibleTrigger>
56
+ <CollapsibleContent className="space-y-2 pb-3">
57
+ <div>
58
+ <div className="flex items-center justify-between">
59
+ <Label className="text-[10px] text-gray-500">ドロップシャドウ</Label>
60
+ <Button
61
+ variant="ghost"
62
+ size="sm"
63
+ onClick={toggleShadow}
64
+ className="h-5 text-[10px] text-gray-400 hover:text-white"
65
+ >
66
+ {selectedElement.hasShadow ? '削除' : '追加'}
67
+ </Button>
68
+ </div>
69
+ {selectedElement.hasShadow && (
70
+ <div className="space-y-2 mt-2">
71
+ <div className="grid grid-cols-2 gap-2">
72
+ <div>
73
+ <Label className="text-[10px] text-gray-500">X</Label>
74
+ <Input
75
+ type="number"
76
+ value={selectedElement.shadowX}
77
+ onChange={(e) => {
78
+ onStyleChange({
79
+ boxShadow: buildBoxShadow(
80
+ selectedElement.shadowColor,
81
+ parseInt(e.target.value) || 0,
82
+ selectedElement.shadowY,
83
+ selectedElement.shadowBlur,
84
+ selectedElement.shadowSpread
85
+ ),
86
+ });
87
+ }}
88
+ className="h-7 text-xs bg-[#383838] border-[#444444] text-white"
89
+ />
90
+ </div>
91
+ <div>
92
+ <Label className="text-[10px] text-gray-500">Y</Label>
93
+ <Input
94
+ type="number"
95
+ value={selectedElement.shadowY}
96
+ onChange={(e) => {
97
+ onStyleChange({
98
+ boxShadow: buildBoxShadow(
99
+ selectedElement.shadowColor,
100
+ selectedElement.shadowX,
101
+ parseInt(e.target.value) || 0,
102
+ selectedElement.shadowBlur,
103
+ selectedElement.shadowSpread
104
+ ),
105
+ });
106
+ }}
107
+ className="h-7 text-xs bg-[#383838] border-[#444444] text-white"
108
+ />
109
+ </div>
110
+ </div>
111
+ <div className="grid grid-cols-2 gap-2">
112
+ <div>
113
+ <Label className="text-[10px] text-gray-500">ぼかし</Label>
114
+ <Input
115
+ type="number"
116
+ value={selectedElement.shadowBlur}
117
+ onChange={(e) => {
118
+ onStyleChange({
119
+ boxShadow: buildBoxShadow(
120
+ selectedElement.shadowColor,
121
+ selectedElement.shadowX,
122
+ selectedElement.shadowY,
123
+ parseInt(e.target.value) || 0,
124
+ selectedElement.shadowSpread
125
+ ),
126
+ });
127
+ }}
128
+ className="h-7 text-xs bg-[#383838] border-[#444444] text-white"
129
+ />
130
+ </div>
131
+ <div>
132
+ <Label className="text-[10px] text-gray-500">広がり</Label>
133
+ <Input
134
+ type="number"
135
+ value={selectedElement.shadowSpread}
136
+ onChange={(e) => {
137
+ onStyleChange({
138
+ boxShadow: buildBoxShadow(
139
+ selectedElement.shadowColor,
140
+ selectedElement.shadowX,
141
+ selectedElement.shadowY,
142
+ selectedElement.shadowBlur,
143
+ parseInt(e.target.value) || 0
144
+ ),
145
+ });
146
+ }}
147
+ className="h-7 text-xs bg-[#383838] border-[#444444] text-white"
148
+ />
149
+ </div>
150
+ </div>
151
+ </div>
152
+ )}
153
+ </div>
154
+ </CollapsibleContent>
155
+ </Collapsible>
156
+ );
157
+ }