@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,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS単位関連のユーティリティ関数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// サポートする単位の定義
|
|
6
|
+
export const CSS_UNITS = ['px', '%', 'em', 'rem', 'vw', 'vh'] as const;
|
|
7
|
+
export type CSSUnit = (typeof CSS_UNITS)[number];
|
|
8
|
+
|
|
9
|
+
// サイズ用単位(auto含む)
|
|
10
|
+
export const SIZE_UNITS = ['px', '%', 'vw', 'vh', 'auto'] as const;
|
|
11
|
+
export type SizeUnit = (typeof SIZE_UNITS)[number];
|
|
12
|
+
|
|
13
|
+
// フォントサイズ用単位
|
|
14
|
+
export const FONT_SIZE_UNITS = ['px', 'em', 'rem', '%'] as const;
|
|
15
|
+
export type FontSizeUnit = (typeof FONT_SIZE_UNITS)[number];
|
|
16
|
+
|
|
17
|
+
// スペーシング用単位(gap, padding, margin)
|
|
18
|
+
export const SPACING_UNITS = ['px', 'em', 'rem', '%'] as const;
|
|
19
|
+
export type SpacingUnit = (typeof SPACING_UNITS)[number];
|
|
20
|
+
|
|
21
|
+
// Border Radius用単位
|
|
22
|
+
export const BORDER_RADIUS_UNITS = ['px', '%', 'em'] as const;
|
|
23
|
+
export type BorderRadiusUnit = (typeof BORDER_RADIUS_UNITS)[number];
|
|
24
|
+
|
|
25
|
+
// Line Height用単位 (空文字は unitless を表す)
|
|
26
|
+
export const LINE_HEIGHT_UNITS = ['', 'em', 'px', '%'] as const;
|
|
27
|
+
export type LineHeightUnit = (typeof LINE_HEIGHT_UNITS)[number];
|
|
28
|
+
|
|
29
|
+
// Letter Spacing用単位
|
|
30
|
+
export const LETTER_SPACING_UNITS = ['em', 'px'] as const;
|
|
31
|
+
export type LetterSpacingUnit = (typeof LETTER_SPACING_UNITS)[number];
|
|
32
|
+
|
|
33
|
+
// Border Width用単位
|
|
34
|
+
export const BORDER_WIDTH_UNITS = ['px', 'em'] as const;
|
|
35
|
+
export type BorderWidthUnit = (typeof BORDER_WIDTH_UNITS)[number];
|
|
36
|
+
|
|
37
|
+
// Position用単位 (left, top, right, bottom)
|
|
38
|
+
export const POSITION_UNITS = ['px', '%', 'vw', 'vh'] as const;
|
|
39
|
+
export type PositionUnit = (typeof POSITION_UNITS)[number];
|
|
40
|
+
|
|
41
|
+
// 値と単位のパース結果
|
|
42
|
+
export interface ParsedValue {
|
|
43
|
+
numericValue: number;
|
|
44
|
+
unit: string;
|
|
45
|
+
isSpecial: boolean; // auto, inherit, var(...) などの特殊値
|
|
46
|
+
originalValue: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* CSS値を数値と単位にパースする
|
|
51
|
+
* @param value CSS値文字列 (例: "16px", "1.5em", "100%", "auto", "var(--spacing)")
|
|
52
|
+
* @param defaultUnit デフォルト単位 (パース失敗時に使用)
|
|
53
|
+
* @returns パース結果
|
|
54
|
+
*/
|
|
55
|
+
export function parseValueWithUnit(
|
|
56
|
+
value: string | number | undefined,
|
|
57
|
+
defaultUnit: string = 'px'
|
|
58
|
+
): ParsedValue {
|
|
59
|
+
// undefined または空の場合
|
|
60
|
+
if (value === undefined || value === '') {
|
|
61
|
+
return {
|
|
62
|
+
numericValue: 0,
|
|
63
|
+
unit: defaultUnit,
|
|
64
|
+
isSpecial: false,
|
|
65
|
+
originalValue: '',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 数値の場合
|
|
70
|
+
if (typeof value === 'number') {
|
|
71
|
+
return {
|
|
72
|
+
numericValue: value,
|
|
73
|
+
unit: defaultUnit,
|
|
74
|
+
isSpecial: false,
|
|
75
|
+
originalValue: `${value}${defaultUnit}`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const trimmedValue = value.trim();
|
|
80
|
+
|
|
81
|
+
// 特殊値のチェック (auto, inherit, initial, unset, var(...))
|
|
82
|
+
if (
|
|
83
|
+
trimmedValue === 'auto' ||
|
|
84
|
+
trimmedValue === 'inherit' ||
|
|
85
|
+
trimmedValue === 'initial' ||
|
|
86
|
+
trimmedValue === 'unset' ||
|
|
87
|
+
trimmedValue.startsWith('var(')
|
|
88
|
+
) {
|
|
89
|
+
return {
|
|
90
|
+
numericValue: 0,
|
|
91
|
+
unit: defaultUnit,
|
|
92
|
+
isSpecial: true,
|
|
93
|
+
originalValue: trimmedValue,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 数値+単位のパターンをマッチ
|
|
98
|
+
const match = trimmedValue.match(/^(-?\d*\.?\d+)(px|%|em|rem|vw|vh)?$/);
|
|
99
|
+
if (match) {
|
|
100
|
+
return {
|
|
101
|
+
numericValue: parseFloat(match[1]),
|
|
102
|
+
unit: match[2] || defaultUnit,
|
|
103
|
+
isSpecial: false,
|
|
104
|
+
originalValue: trimmedValue,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// パース失敗時はデフォルト値を返す
|
|
109
|
+
return {
|
|
110
|
+
numericValue: 0,
|
|
111
|
+
unit: defaultUnit,
|
|
112
|
+
isSpecial: false,
|
|
113
|
+
originalValue: trimmedValue,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 数値と単位を結合してCSS値を生成
|
|
119
|
+
* @param numericValue 数値
|
|
120
|
+
* @param unit 単位
|
|
121
|
+
* @returns CSS値文字列
|
|
122
|
+
*/
|
|
123
|
+
export function formatValueWithUnit(numericValue: number, unit: string): string {
|
|
124
|
+
// autoの場合は単位なし
|
|
125
|
+
if (unit === 'auto') {
|
|
126
|
+
return 'auto';
|
|
127
|
+
}
|
|
128
|
+
return `${numericValue}${unit}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* px値をパースして数値を返す(後方互換性用)
|
|
133
|
+
* @param value CSS値文字列
|
|
134
|
+
* @returns 数値
|
|
135
|
+
*/
|
|
136
|
+
export function parsePxValue(value: string | number | undefined): number {
|
|
137
|
+
const parsed = parseValueWithUnit(value, 'px');
|
|
138
|
+
return parsed.numericValue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 単位変換のコンテキスト情報
|
|
143
|
+
*/
|
|
144
|
+
export interface UnitConversionContext {
|
|
145
|
+
/** ビューポート幅 (px) */
|
|
146
|
+
viewportWidth?: number;
|
|
147
|
+
/** ビューポート高さ (px) */
|
|
148
|
+
viewportHeight?: number;
|
|
149
|
+
/** 親要素の幅 (px) */
|
|
150
|
+
parentWidth?: number;
|
|
151
|
+
/** 親要素の高さ (px) */
|
|
152
|
+
parentHeight?: number;
|
|
153
|
+
/** 要素のフォントサイズ (px) */
|
|
154
|
+
fontSize?: number;
|
|
155
|
+
/** ルートフォントサイズ (px) - 通常は16 */
|
|
156
|
+
rootFontSize?: number;
|
|
157
|
+
/** 変換対象のプロパティ (width/height/other) */
|
|
158
|
+
property?: 'width' | 'height' | 'other';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 値を一度pxに変換する
|
|
163
|
+
*/
|
|
164
|
+
function toPx(
|
|
165
|
+
value: number,
|
|
166
|
+
fromUnit: string,
|
|
167
|
+
context: UnitConversionContext
|
|
168
|
+
): number {
|
|
169
|
+
const {
|
|
170
|
+
viewportWidth = 1920,
|
|
171
|
+
viewportHeight = 1080,
|
|
172
|
+
parentWidth = 1920,
|
|
173
|
+
parentHeight = 1080,
|
|
174
|
+
fontSize = 16,
|
|
175
|
+
rootFontSize = 16,
|
|
176
|
+
property = 'width',
|
|
177
|
+
} = context;
|
|
178
|
+
|
|
179
|
+
switch (fromUnit) {
|
|
180
|
+
case 'px':
|
|
181
|
+
case '': // unitless
|
|
182
|
+
return value;
|
|
183
|
+
case 'vw':
|
|
184
|
+
return (value * viewportWidth) / 100;
|
|
185
|
+
case 'vh':
|
|
186
|
+
return (value * viewportHeight) / 100;
|
|
187
|
+
case '%':
|
|
188
|
+
return property === 'height'
|
|
189
|
+
? (value * parentHeight) / 100
|
|
190
|
+
: (value * parentWidth) / 100;
|
|
191
|
+
case 'em':
|
|
192
|
+
return value * fontSize;
|
|
193
|
+
case 'rem':
|
|
194
|
+
return value * rootFontSize;
|
|
195
|
+
default:
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* pxから目的の単位に変換する
|
|
202
|
+
*/
|
|
203
|
+
function fromPx(
|
|
204
|
+
pxValue: number,
|
|
205
|
+
toUnit: string,
|
|
206
|
+
context: UnitConversionContext
|
|
207
|
+
): number {
|
|
208
|
+
const {
|
|
209
|
+
viewportWidth = 1920,
|
|
210
|
+
viewportHeight = 1080,
|
|
211
|
+
parentWidth = 1920,
|
|
212
|
+
parentHeight = 1080,
|
|
213
|
+
fontSize = 16,
|
|
214
|
+
rootFontSize = 16,
|
|
215
|
+
property = 'width',
|
|
216
|
+
} = context;
|
|
217
|
+
|
|
218
|
+
switch (toUnit) {
|
|
219
|
+
case 'px':
|
|
220
|
+
return Math.round(pxValue * 100) / 100;
|
|
221
|
+
case '': // unitless (line-height等)
|
|
222
|
+
return Math.round((pxValue / fontSize) * 100) / 100;
|
|
223
|
+
case 'vw':
|
|
224
|
+
return Math.round((pxValue / viewportWidth) * 100 * 100) / 100;
|
|
225
|
+
case 'vh':
|
|
226
|
+
return Math.round((pxValue / viewportHeight) * 100 * 100) / 100;
|
|
227
|
+
case '%':
|
|
228
|
+
const parentDim = property === 'height' ? parentHeight : parentWidth;
|
|
229
|
+
return Math.round((pxValue / parentDim) * 100 * 100) / 100;
|
|
230
|
+
case 'em':
|
|
231
|
+
return Math.round((pxValue / fontSize) * 100) / 100;
|
|
232
|
+
case 'rem':
|
|
233
|
+
return Math.round((pxValue / rootFontSize) * 100) / 100;
|
|
234
|
+
default:
|
|
235
|
+
return pxValue;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 単位を変換する際の値調整
|
|
241
|
+
* @param value 現在の数値
|
|
242
|
+
* @param fromUnit 変換元の単位
|
|
243
|
+
* @param toUnit 変換先の単位
|
|
244
|
+
* @param context 変換コンテキスト(ビューポートサイズ、親要素サイズ等)
|
|
245
|
+
* @returns 変換後の数値
|
|
246
|
+
*/
|
|
247
|
+
export function convertUnit(
|
|
248
|
+
value: number,
|
|
249
|
+
fromUnit: string,
|
|
250
|
+
toUnit: string,
|
|
251
|
+
context: UnitConversionContext = {}
|
|
252
|
+
): number {
|
|
253
|
+
// 同じ単位なら変換不要
|
|
254
|
+
if (fromUnit === toUnit) {
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 一度pxに変換してから目的の単位に変換
|
|
259
|
+
const pxValue = toPx(value, fromUnit, context);
|
|
260
|
+
return fromPx(pxValue, toUnit, context);
|
|
261
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* エディタ共通の最上段バー。
|
|
5
|
+
*
|
|
6
|
+
* Figma風UIとPowerPoint風UIで「殻」は違っても、最上段でやることは同じ:
|
|
7
|
+
* 一覧へ戻る / UI切替 | デッキタイトル | 自動保存の状態 / 保存 / プレビュー / 共有 / 閉じる
|
|
8
|
+
* 同じ操作が2つのUIで別の場所・別の並びにあると、UIを切り替えるたびに探し直しになる。
|
|
9
|
+
* ここに1本化して、variant では**配色だけ**を変える。
|
|
10
|
+
*
|
|
11
|
+
* 【設計の約束】ここは見た目の殻。保存は effectiveSave、書き出しは lib/export、
|
|
12
|
+
* 閉じるは FVE の handleClose を呼ぶだけで、ロジックを二重に持たない。
|
|
13
|
+
* UI固有の操作(PPT: 元に戻す/検索/テーマ, Figma: ズーム/…メニュー)は
|
|
14
|
+
* leftExtra / rightExtra / menuExtra のスロットで受け取り、この並びを崩さない。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useState } from 'react';
|
|
18
|
+
import {
|
|
19
|
+
DropdownMenu,
|
|
20
|
+
DropdownMenuContent,
|
|
21
|
+
DropdownMenuItem,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
} from '../../../components/ui/dropdown-menu';
|
|
24
|
+
import { ArrowLeft, ChevronDown, Download, FileText, Loader2, PenTool, Play, Save, X } from 'lucide-react';
|
|
25
|
+
import { useEditorContext } from '../../EditorContext';
|
|
26
|
+
import { getCleanHtml } from '../../utils/html-utils';
|
|
27
|
+
import { startExport, waitForExport, downloadExport, type ExportFormat } from '../../../lib/export';
|
|
28
|
+
import { can } from '../../../io';
|
|
29
|
+
|
|
30
|
+
export type EditorSaveStatus = 'saved' | 'dirty' | 'saving' | 'error';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 配色。PowerPoint風は PPT_PALETTES[theme] をそのまま渡してもらう
|
|
34
|
+
* (PptChrome から import すると循環参照になるため、値は呼び出し側から受け取る)。
|
|
35
|
+
*/
|
|
36
|
+
export type TopBarPalette = {
|
|
37
|
+
chrome: string;
|
|
38
|
+
text: string;
|
|
39
|
+
sub: string;
|
|
40
|
+
border: string;
|
|
41
|
+
hover: string;
|
|
42
|
+
activeBg: string;
|
|
43
|
+
control: string;
|
|
44
|
+
disabled: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Figma風の配色(editor-skin.css のパネル色に合わせる) */
|
|
48
|
+
const FIGMA_PALETTE: TopBarPalette = {
|
|
49
|
+
chrome: '#2c2c2c',
|
|
50
|
+
text: '#ffffff',
|
|
51
|
+
sub: '#9ca3af',
|
|
52
|
+
border: '#444444',
|
|
53
|
+
hover: '#444444',
|
|
54
|
+
activeBg: '#3a3a3a',
|
|
55
|
+
control: '#1e1e1e',
|
|
56
|
+
disabled: '#5f5f5f',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** 共有ボタンの色だけはUIの「顔」なのでテーマごとに持つ */
|
|
60
|
+
const SHARE_COLOR = {
|
|
61
|
+
figma: { bg: '#0d99ff', fg: '#ffffff' },
|
|
62
|
+
ppt: { bg: '#E8912D', fg: '#33230a' },
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
export interface EditorTopBarProps {
|
|
66
|
+
variant: 'figma' | 'ppt';
|
|
67
|
+
/** PowerPoint風の配色。省略時はFigma風の配色 */
|
|
68
|
+
palette?: TopBarPalette;
|
|
69
|
+
/** 中央に出すデッキ(スライド)のタイトル */
|
|
70
|
+
title?: string;
|
|
71
|
+
/** 編集中スライドの番号(1始まり)。プレビューで開くページにも使う */
|
|
72
|
+
pageNumber?: number;
|
|
73
|
+
saveStatus?: EditorSaveStatus;
|
|
74
|
+
/** 保存の実体。FVEの effectiveSave をそのまま渡す */
|
|
75
|
+
onSave: (html: string) => Promise<void> | void;
|
|
76
|
+
/** ページ設定(CSS/JS等)も併せて保存する場合に渡す */
|
|
77
|
+
onSaveSettings?: () => Promise<void>;
|
|
78
|
+
onClose: () => void;
|
|
79
|
+
/** マルチページキャンバスからの編集。閉じる=「キャンバスに戻る」になる */
|
|
80
|
+
isCanvasEditing?: boolean;
|
|
81
|
+
/** もう一方のUIへ切り替える */
|
|
82
|
+
onSwitchUi?: () => void;
|
|
83
|
+
/** 一覧へ戻る/UI切替の右。UI固有の操作(PPT: 元に戻す・やり直し) */
|
|
84
|
+
leftExtra?: React.ReactNode;
|
|
85
|
+
/** 保存状態の左。UI固有の操作(Figma: ズーム / PPT: 検索・テーマ) */
|
|
86
|
+
rightExtra?: React.ReactNode;
|
|
87
|
+
/** 共有と閉じるの間。UI固有のメニュー(Figma: …) */
|
|
88
|
+
menuExtra?: React.ReactNode;
|
|
89
|
+
/**
|
|
90
|
+
* 「一覧へ戻る」を出すか。
|
|
91
|
+
* 一覧が無い使い方(1ページずつ編集する等)では出さない。
|
|
92
|
+
*/
|
|
93
|
+
showBackToList?: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function EditorTopBar({
|
|
97
|
+
variant,
|
|
98
|
+
palette,
|
|
99
|
+
title,
|
|
100
|
+
pageNumber,
|
|
101
|
+
saveStatus = 'saved',
|
|
102
|
+
onSave,
|
|
103
|
+
onSaveSettings,
|
|
104
|
+
onClose,
|
|
105
|
+
isCanvasEditing,
|
|
106
|
+
onSwitchUi,
|
|
107
|
+
leftExtra,
|
|
108
|
+
rightExtra,
|
|
109
|
+
menuExtra,
|
|
110
|
+
showBackToList = true,
|
|
111
|
+
}: EditorTopBarProps) {
|
|
112
|
+
const pal = palette ?? FIGMA_PALETTE;
|
|
113
|
+
const share = SHARE_COLOR[variant];
|
|
114
|
+
const { getIframeDoc, html, saving, hasChanges } = useEditorContext();
|
|
115
|
+
const [exporting, setExporting] = useState<string | null>(null);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 手動保存。自動保存と同じ入口(effectiveSave)を通すので、
|
|
119
|
+
* 保存中フラグ・書き戻し・状態表示はすべて共通の1本で動く。
|
|
120
|
+
*/
|
|
121
|
+
const handleSave = async () => {
|
|
122
|
+
try {
|
|
123
|
+
const doc = getIframeDoc();
|
|
124
|
+
// 保存してもエディタは閉じない。保存は作業の中断ではない
|
|
125
|
+
await onSave(doc ? getCleanHtml(doc) : html);
|
|
126
|
+
if (onSaveSettings) await onSaveSettings();
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.error('Save error:', e);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/** プレビュー = 表示専用(?clean)で今のページを別タブに開く */
|
|
133
|
+
const handlePreview = () => {
|
|
134
|
+
const page = pageNumber ?? 1;
|
|
135
|
+
window.open(`${window.location.origin}${window.location.pathname}#/${page}?clean`, '_blank');
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** 共有 = デッキ全体をPDF/PPTXへ書き出してダウンロード */
|
|
139
|
+
const runExport = async (format: ExportFormat, label: string) => {
|
|
140
|
+
setExporting(label);
|
|
141
|
+
try {
|
|
142
|
+
const jobId = await startExport(format);
|
|
143
|
+
const st = await waitForExport(jobId, () => undefined);
|
|
144
|
+
downloadExport(st);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
window.alert(`書き出しに失敗しました: ${String(e).slice(0, 120)}`);
|
|
147
|
+
} finally {
|
|
148
|
+
setExporting(null);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const iconBtn = 'flex h-7 w-7 shrink-0 items-center justify-center rounded transition-colors disabled:opacity-40';
|
|
153
|
+
const textBtn = 'flex h-7 shrink-0 items-center gap-1.5 rounded-md border px-2.5 text-[11px] font-medium transition-colors';
|
|
154
|
+
const hoverIn = (e: React.MouseEvent<HTMLElement>) => {
|
|
155
|
+
if (!(e.currentTarget as HTMLButtonElement).disabled) e.currentTarget.style.backgroundColor = pal.hover;
|
|
156
|
+
};
|
|
157
|
+
const hoverOut = (e: React.MouseEvent<HTMLElement>) => {
|
|
158
|
+
e.currentTarget.style.backgroundColor = '';
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div
|
|
163
|
+
data-editor-topbar={variant}
|
|
164
|
+
className={`flex shrink-0 items-center gap-2 border-b px-3 ${variant === 'ppt' ? 'h-10' : 'h-11'}`}
|
|
165
|
+
style={{ backgroundColor: pal.chrome, borderColor: pal.border, color: pal.text }}
|
|
166
|
+
>
|
|
167
|
+
{/* 左: 一覧へ戻る / UI切替 / UI固有の操作 */}
|
|
168
|
+
{showBackToList && (
|
|
169
|
+
<button
|
|
170
|
+
data-topbar="back"
|
|
171
|
+
onClick={onClose}
|
|
172
|
+
title="保存して一覧へ戻る"
|
|
173
|
+
className={textBtn}
|
|
174
|
+
style={{ borderColor: pal.border, color: pal.sub }}
|
|
175
|
+
onMouseEnter={hoverIn}
|
|
176
|
+
onMouseLeave={hoverOut}
|
|
177
|
+
>
|
|
178
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
179
|
+
一覧へ
|
|
180
|
+
</button>
|
|
181
|
+
)}
|
|
182
|
+
{onSwitchUi && (
|
|
183
|
+
<button
|
|
184
|
+
data-topbar="switch-ui"
|
|
185
|
+
onClick={onSwitchUi}
|
|
186
|
+
title={
|
|
187
|
+
variant === 'ppt'
|
|
188
|
+
? 'Figma風UIに切り替え(編集エンジンは共通)'
|
|
189
|
+
: 'PowerPoint風UIに切り替え(編集エンジンは共通)'
|
|
190
|
+
}
|
|
191
|
+
className={textBtn}
|
|
192
|
+
style={{ borderColor: pal.border, color: pal.sub }}
|
|
193
|
+
onMouseEnter={hoverIn}
|
|
194
|
+
onMouseLeave={hoverOut}
|
|
195
|
+
>
|
|
196
|
+
{variant === 'ppt' ? (
|
|
197
|
+
<PenTool className="h-3.5 w-3.5" />
|
|
198
|
+
) : (
|
|
199
|
+
<span className="flex h-3.5 w-3.5 items-center justify-center rounded-[2px] bg-[#C43E1C] text-[9px] font-bold leading-none text-white">
|
|
200
|
+
P
|
|
201
|
+
</span>
|
|
202
|
+
)}
|
|
203
|
+
{variant === 'ppt' ? 'Figma風' : 'PPT風'}
|
|
204
|
+
</button>
|
|
205
|
+
)}
|
|
206
|
+
{leftExtra}
|
|
207
|
+
|
|
208
|
+
{/* 中央: デッキタイトル + スライド番号 */}
|
|
209
|
+
<div className="flex min-w-0 flex-1 items-center justify-center gap-2">
|
|
210
|
+
{pageNumber != null && (
|
|
211
|
+
<span className="shrink-0 font-mono text-[11px] tabular-nums" style={{ color: pal.sub }}>
|
|
212
|
+
{String(pageNumber).padStart(3, '0')}
|
|
213
|
+
</span>
|
|
214
|
+
)}
|
|
215
|
+
<span data-topbar="title" className="truncate text-[12.5px] font-semibold" style={{ color: pal.text }}>
|
|
216
|
+
{title || 'ビジュアルエディタ'}
|
|
217
|
+
</span>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
{/* 右: UI固有の操作 / 自動保存 / 保存 / プレビュー / 共有 / …/ 閉じる */}
|
|
221
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
222
|
+
{rightExtra}
|
|
223
|
+
<SaveStatusPill variant={variant} status={saveStatus} pal={pal} />
|
|
224
|
+
<button
|
|
225
|
+
data-topbar="save"
|
|
226
|
+
onClick={() => void handleSave()}
|
|
227
|
+
disabled={saving || !hasChanges}
|
|
228
|
+
title={saving ? '保存中…' : hasChanges ? '今すぐ保存' : '保存済みです(変更があると自動でも保存されます)'}
|
|
229
|
+
className={iconBtn}
|
|
230
|
+
style={{ color: saving || !hasChanges ? pal.disabled : pal.text }}
|
|
231
|
+
onMouseEnter={hoverIn}
|
|
232
|
+
onMouseLeave={hoverOut}
|
|
233
|
+
>
|
|
234
|
+
<Save className="h-4 w-4" />
|
|
235
|
+
</button>
|
|
236
|
+
{/* 書き出しは利用側の処理が要る。渡されていなければ出さない
|
|
237
|
+
(押すと失敗するボタンを残さないため) */}
|
|
238
|
+
{can("exportDeck") && (
|
|
239
|
+
<>
|
|
240
|
+
<button
|
|
241
|
+
data-topbar="preview"
|
|
242
|
+
onClick={handlePreview}
|
|
243
|
+
title="プレビュー(表示専用で別タブに開く)"
|
|
244
|
+
className={iconBtn}
|
|
245
|
+
style={{ color: pal.text }}
|
|
246
|
+
onMouseEnter={hoverIn}
|
|
247
|
+
onMouseLeave={hoverOut}
|
|
248
|
+
>
|
|
249
|
+
<Play className="h-4 w-4" />
|
|
250
|
+
</button>
|
|
251
|
+
<DropdownMenu>
|
|
252
|
+
<DropdownMenuTrigger asChild>
|
|
253
|
+
<button
|
|
254
|
+
data-topbar="share"
|
|
255
|
+
title="書き出して共有"
|
|
256
|
+
className="flex h-7 shrink-0 items-center gap-1 rounded-md px-2.5 text-[11px] font-semibold transition-opacity hover:opacity-90"
|
|
257
|
+
style={{ backgroundColor: share.bg, color: share.fg }}
|
|
258
|
+
>
|
|
259
|
+
{exporting && <Loader2 className="h-3 w-3 animate-spin" />}
|
|
260
|
+
{exporting ? `${exporting}を書き出し中…` : '共有'}
|
|
261
|
+
<ChevronDown className="h-3 w-3" />
|
|
262
|
+
</button>
|
|
263
|
+
</DropdownMenuTrigger>
|
|
264
|
+
<DropdownMenuContent
|
|
265
|
+
align="end"
|
|
266
|
+
data-topbar-menu="share"
|
|
267
|
+
className="w-56"
|
|
268
|
+
style={{ backgroundColor: pal.control, borderColor: pal.border, color: pal.text }}
|
|
269
|
+
>
|
|
270
|
+
<DropdownMenuItem
|
|
271
|
+
onClick={() => void runExport('pdf', 'PDF')}
|
|
272
|
+
className="cursor-pointer gap-2 focus:bg-white/10"
|
|
273
|
+
style={{ color: pal.text }}
|
|
274
|
+
>
|
|
275
|
+
<FileText className="h-4 w-4" />
|
|
276
|
+
PDF を書き出し
|
|
277
|
+
</DropdownMenuItem>
|
|
278
|
+
<DropdownMenuItem
|
|
279
|
+
onClick={() => void runExport('pptx', 'PPTX')}
|
|
280
|
+
className="cursor-pointer gap-2 focus:bg-white/10"
|
|
281
|
+
style={{ color: pal.text }}
|
|
282
|
+
>
|
|
283
|
+
<Download className="h-4 w-4" />
|
|
284
|
+
PowerPoint (画像貼り込み)
|
|
285
|
+
</DropdownMenuItem>
|
|
286
|
+
<DropdownMenuItem
|
|
287
|
+
onClick={() => void runExport('pptx-edit', 'PPTX')}
|
|
288
|
+
className="cursor-pointer gap-2 focus:bg-white/10"
|
|
289
|
+
style={{ color: pal.text }}
|
|
290
|
+
>
|
|
291
|
+
<Download className="h-4 w-4" />
|
|
292
|
+
PowerPoint (編集できる)
|
|
293
|
+
</DropdownMenuItem>
|
|
294
|
+
</DropdownMenuContent>
|
|
295
|
+
</DropdownMenu>
|
|
296
|
+
</>
|
|
297
|
+
)}
|
|
298
|
+
{menuExtra}
|
|
299
|
+
{isCanvasEditing ? (
|
|
300
|
+
<button
|
|
301
|
+
data-topbar="close"
|
|
302
|
+
onClick={onClose}
|
|
303
|
+
title="キャンバスに戻る"
|
|
304
|
+
className={textBtn}
|
|
305
|
+
style={{ borderColor: pal.border, color: pal.sub }}
|
|
306
|
+
onMouseEnter={hoverIn}
|
|
307
|
+
onMouseLeave={hoverOut}
|
|
308
|
+
>
|
|
309
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
310
|
+
キャンバスに戻る
|
|
311
|
+
</button>
|
|
312
|
+
) : (
|
|
313
|
+
<button
|
|
314
|
+
data-topbar="close"
|
|
315
|
+
onClick={onClose}
|
|
316
|
+
title="エディタを閉じて一覧へ戻る"
|
|
317
|
+
className={iconBtn}
|
|
318
|
+
style={{ color: pal.sub }}
|
|
319
|
+
onMouseEnter={hoverIn}
|
|
320
|
+
onMouseLeave={hoverOut}
|
|
321
|
+
>
|
|
322
|
+
<X className="h-4 w-4" />
|
|
323
|
+
</button>
|
|
324
|
+
)}
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 自動保存の状態表示。押せるものではないので、色と文言だけで状態を示す。
|
|
332
|
+
* 自動保存は常時オンなので「未保存」は「このあと保存される」の意味。
|
|
333
|
+
*/
|
|
334
|
+
function SaveStatusPill({
|
|
335
|
+
variant,
|
|
336
|
+
status,
|
|
337
|
+
pal,
|
|
338
|
+
}: {
|
|
339
|
+
variant: 'figma' | 'ppt';
|
|
340
|
+
status: EditorSaveStatus;
|
|
341
|
+
pal: TopBarPalette;
|
|
342
|
+
}) {
|
|
343
|
+
const note = {
|
|
344
|
+
saved: '保存済み',
|
|
345
|
+
dirty: '保存待ち',
|
|
346
|
+
saving: '保存中…',
|
|
347
|
+
error: '保存できません',
|
|
348
|
+
}[status];
|
|
349
|
+
|
|
350
|
+
if (variant === 'ppt') {
|
|
351
|
+
// PowerPoint実機の「自動保存」ピル(トグルは無く、ノブが進み具合を示す)
|
|
352
|
+
const on = status !== 'error';
|
|
353
|
+
return (
|
|
354
|
+
<span
|
|
355
|
+
data-topbar="save-status"
|
|
356
|
+
data-save-status={status}
|
|
357
|
+
className="flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px]"
|
|
358
|
+
style={{ backgroundColor: pal.hover, color: status === 'error' ? '#ff6b6b' : pal.sub }}
|
|
359
|
+
title={`自動保存: ${note}(編集の手が止まると保存します)`}
|
|
360
|
+
>
|
|
361
|
+
自動保存
|
|
362
|
+
<span
|
|
363
|
+
className="relative inline-block h-3 w-6 rounded-full"
|
|
364
|
+
style={{ backgroundColor: on ? '#C43E1C' : '#8a3a3a' }}
|
|
365
|
+
>
|
|
366
|
+
<span
|
|
367
|
+
className={`absolute right-0.5 top-0.5 h-2 w-2 rounded-full bg-white ${status === 'saving' ? 'animate-pulse' : ''}`}
|
|
368
|
+
/>
|
|
369
|
+
</span>
|
|
370
|
+
{note}
|
|
371
|
+
</span>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const view = {
|
|
376
|
+
saved: { dot: 'bg-emerald-400', box: 'bg-emerald-400/10 text-emerald-300 ring-emerald-400/25', title: '自動保存済みです' },
|
|
377
|
+
dirty: { dot: 'bg-amber-400', box: 'bg-amber-400/15 text-amber-300 ring-amber-400/30', title: '変更が止まると自動で保存されます' },
|
|
378
|
+
saving: { dot: 'bg-sky-400 animate-pulse', box: 'bg-sky-400/15 text-sky-300 ring-sky-400/30', title: '保存しています' },
|
|
379
|
+
error: { dot: 'bg-rose-400', box: 'bg-rose-500/15 text-rose-300 ring-rose-400/30', title: '自動保存に失敗しました。保存ボタンで再試行してください' },
|
|
380
|
+
}[status];
|
|
381
|
+
return (
|
|
382
|
+
<span
|
|
383
|
+
data-topbar="save-status"
|
|
384
|
+
data-save-status={status}
|
|
385
|
+
title={view.title}
|
|
386
|
+
className={`inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold tracking-wide ring-1 ring-inset ${view.box}`}
|
|
387
|
+
>
|
|
388
|
+
<span className={`h-1.5 w-1.5 rounded-full ${view.dot}`} />
|
|
389
|
+
{status === 'dirty' ? '未保存' : note}
|
|
390
|
+
</span>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Figma風UIの左パネル(2段構成)。
|
|
5
|
+
* 上: ページ切替(PagesPanel。スライドのプレビュー付き)
|
|
6
|
+
* 下: 現在ページのレイヤーツリー(EditorLayerPanel hideSlideList)
|
|
7
|
+
*
|
|
8
|
+
* 幅はこのラッパーが1本のハンドルで持つ。段ごとに幅・ハンドルを持たせると
|
|
9
|
+
* 上下で幅が食い違ったり、リサイズが二重になったりするため。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useEditorContext } from '../../EditorContext';
|
|
13
|
+
import { useResizablePanel } from '../../hooks/useResizablePanel';
|
|
14
|
+
import { EditorLayerPanel } from '../EditorLayerPanel';
|
|
15
|
+
import { PagesPanel } from './PagesPanel';
|
|
16
|
+
import { can } from '../../../io';
|
|
17
|
+
|
|
18
|
+
export function LeftPanel({ page }: { page: number }) {
|
|
19
|
+
// 一覧のサムネイルは利用側が描く(io.renderContent)。描けないなら段ごと出さない。
|
|
20
|
+
// Webページの用途ではページ切替を利用側のUIが持つことが多い
|
|
21
|
+
const { editorMode } = useEditorContext();
|
|
22
|
+
const showPages = can('renderContent') && editorMode !== 'webpage';
|
|
23
|
+
const { width, isDragging, resizeHandleProps } = useResizablePanel({
|
|
24
|
+
initialWidth: 256,
|
|
25
|
+
minWidth: 180,
|
|
26
|
+
maxWidth: 480,
|
|
27
|
+
direction: 'right',
|
|
28
|
+
storageKey: 'gg-editor:left-panel-width',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className="relative flex shrink-0 flex-col overflow-hidden"
|
|
34
|
+
style={{ width: `${width}px` }}
|
|
35
|
+
>
|
|
36
|
+
{showPages && <PagesPanel page={page} />}
|
|
37
|
+
{/* 残りの高さいっぱいにレイヤーツリー(自前でスクロールする) */}
|
|
38
|
+
<div className="flex min-h-0 flex-1">
|
|
39
|
+
<EditorLayerPanel hideSlideList />
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{/* リサイズハンドル(全高。上下どちらの段からでも掴める) */}
|
|
43
|
+
<div {...resizeHandleProps} />
|
|
44
|
+
{isDragging && <div className="fixed inset-0 z-50 cursor-col-resize" />}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|