@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,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ページ設定・AI機能などが使う汎用のAPI呼び出し。
|
|
3
|
+
*
|
|
4
|
+
* 実体は利用側が渡す io.apiFetch。渡されていなければ「未提供」として
|
|
5
|
+
* 404相当のエラーを投げる。呼び出し側は 404 を「まだ無い」として
|
|
6
|
+
* 静かに既定値へ落ちる作りになっている。
|
|
7
|
+
*/
|
|
8
|
+
import { io } from "../../io";
|
|
9
|
+
|
|
10
|
+
export class AuthFetchError extends Error {
|
|
11
|
+
status: number;
|
|
12
|
+
constructor(message: string, status: number) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "AuthFetchError";
|
|
15
|
+
this.status = status;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type AuthFetchOptions = RequestInit;
|
|
20
|
+
|
|
21
|
+
type Api = {
|
|
22
|
+
get: (path: string) => Promise<any>;
|
|
23
|
+
post: (path: string, body?: unknown) => Promise<any>;
|
|
24
|
+
patch: (path: string, body?: unknown) => Promise<any>;
|
|
25
|
+
put: (path: string, body?: unknown) => Promise<any>;
|
|
26
|
+
delete: (path: string) => Promise<any>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const send = async (path: string, init?: RequestInit) => {
|
|
30
|
+
const fn = io().apiFetch;
|
|
31
|
+
if (!fn) {
|
|
32
|
+
// 利用側がこのAPIを持っていない。呼び出し側は 404 を「無い」として扱う
|
|
33
|
+
throw new AuthFetchError(`HTTP 404: Not Found (apiFetch 未提供): ${path}`, 404);
|
|
34
|
+
}
|
|
35
|
+
return fn(path, init);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const withBody = (method: string) => (path: string, body?: unknown) =>
|
|
39
|
+
send(path, {
|
|
40
|
+
method,
|
|
41
|
+
headers: { "Content-Type": "application/json" },
|
|
42
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** 呼び出し側の形を変えないため、引数(getIdToken)は受け取るだけで使わない */
|
|
46
|
+
export async function createAuthApi(_getIdToken?: () => Promise<string | null>): Promise<Api> {
|
|
47
|
+
return {
|
|
48
|
+
get: (path) => send(path),
|
|
49
|
+
post: withBody("POST"),
|
|
50
|
+
patch: withBody("PATCH"),
|
|
51
|
+
put: withBody("PUT"),
|
|
52
|
+
delete: (path) => send(path, { method: "DELETE" }),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function createAuthFetch(_getIdToken?: () => Promise<string | null>) {
|
|
57
|
+
return (path: string, init?: RequestInit) => send(path, init);
|
|
58
|
+
}
|
package/src/lib/deck.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一覧(デッキ)とコメントのクライアントAPI。
|
|
3
|
+
*
|
|
4
|
+
* 実体は利用側が渡す io。ここは呼び出し側の形を変えないための薄い層。
|
|
5
|
+
* 未提供の操作は理由を明示して失敗させる(黙って何も起きないのを避ける)。
|
|
6
|
+
*/
|
|
7
|
+
import { io, notProvided, EMPTY_DECK } from "../io";
|
|
8
|
+
import type { EditorComment, EditorContent, EditorDeck, CommentAction } from "../io";
|
|
9
|
+
|
|
10
|
+
export type SlideComment = EditorComment;
|
|
11
|
+
export type DeckEntry = EditorContent;
|
|
12
|
+
export type Deck = EditorDeck;
|
|
13
|
+
|
|
14
|
+
export async function getDeck(): Promise<Deck> {
|
|
15
|
+
const load = io().loadDeck;
|
|
16
|
+
return load ? load() : EMPTY_DECK;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function commentAction(page: number, action: CommentAction): Promise<Deck> {
|
|
20
|
+
const fn = io().commentAction;
|
|
21
|
+
if (!fn) throw notProvided("commentAction");
|
|
22
|
+
return fn(page, action);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ops = () => io().deckOps ?? {};
|
|
26
|
+
|
|
27
|
+
export async function moveSlide(from: number, to: number): Promise<Deck> {
|
|
28
|
+
const fn = ops().move;
|
|
29
|
+
if (!fn) throw notProvided("deckOps.move");
|
|
30
|
+
return fn(from, to);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function duplicateSlide(page: number): Promise<Deck> {
|
|
34
|
+
const fn = ops().duplicate;
|
|
35
|
+
if (!fn) throw notProvided("deckOps.duplicate");
|
|
36
|
+
return fn(page);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function deleteSlide(page: number): Promise<Deck> {
|
|
40
|
+
const fn = ops().remove;
|
|
41
|
+
if (!fn) throw notProvided("deckOps.remove");
|
|
42
|
+
return fn(page);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function updateSlideMeta(
|
|
46
|
+
page: number,
|
|
47
|
+
patch: { hidden?: boolean; transition?: "fade" | "push" | "zoom" | null },
|
|
48
|
+
): Promise<Deck> {
|
|
49
|
+
const fn = ops().updateMeta;
|
|
50
|
+
if (!fn) throw notProvided("deckOps.updateMeta");
|
|
51
|
+
return fn(page, patch);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function insertSlide(template: string, at: number): Promise<Deck> {
|
|
55
|
+
const fn = ops().insert;
|
|
56
|
+
if (!fn) throw notProvided("deckOps.insert");
|
|
57
|
+
return fn(template, at);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function resetSlide(page: number): Promise<Deck> {
|
|
61
|
+
const fn = ops().reset;
|
|
62
|
+
if (!fn) throw notProvided("deckOps.reset");
|
|
63
|
+
return fn(page);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function getTemplates(): Promise<{ id: string; title: string }[]> {
|
|
67
|
+
const fn = ops().templates;
|
|
68
|
+
return fn ? fn() : [];
|
|
69
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 書き出し(PDF / PPTX)のクライアントAPI。
|
|
3
|
+
*
|
|
4
|
+
* 実体は利用側の io.exportDeck。渡されていなければ書き出しUIを出さないので、
|
|
5
|
+
* ここに来ることは無い(来たら理由を明示して失敗させる)。
|
|
6
|
+
*/
|
|
7
|
+
import { io, notProvided } from "../io";
|
|
8
|
+
import type { ExportFormat } from "../io";
|
|
9
|
+
|
|
10
|
+
export type { ExportFormat };
|
|
11
|
+
|
|
12
|
+
export type ExportStatus = {
|
|
13
|
+
state: "running" | "done" | "error";
|
|
14
|
+
done: number;
|
|
15
|
+
total: number;
|
|
16
|
+
message?: string;
|
|
17
|
+
file?: string;
|
|
18
|
+
downloadUrl?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** 利用側の実装に処理ごと委ねる。進捗の見せ方も利用側が持つ */
|
|
22
|
+
export async function startExport(format: ExportFormat, pages?: number[]): Promise<string> {
|
|
23
|
+
const fn = io().exportDeck;
|
|
24
|
+
if (!fn) throw notProvided("exportDeck");
|
|
25
|
+
await fn(format, pages);
|
|
26
|
+
return "done";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function getExportStatus(_jobId?: string): Promise<ExportStatus> {
|
|
30
|
+
return { state: "done", done: 1, total: 1 };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 進捗の待ち合わせ。
|
|
35
|
+
* 実処理は io.exportDeck の中で完了しているので、ここは即座に「完了」を返す。
|
|
36
|
+
* 引数は呼び出し側の形を変えないために受け取るだけ。
|
|
37
|
+
*/
|
|
38
|
+
export async function waitForExport(
|
|
39
|
+
_jobId?: string,
|
|
40
|
+
_onProgress?: (s: ExportStatus) => void,
|
|
41
|
+
): Promise<ExportStatus> {
|
|
42
|
+
return { state: "done", done: 1, total: 1 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function downloadExport(status: ExportStatus): void {
|
|
46
|
+
if (status.downloadUrl) window.open(status.downloadUrl, "_blank");
|
|
47
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ローカル永続化スタブ。オリジナルは Firebase 実装。
|
|
3
|
+
*
|
|
4
|
+
* このプロジェクトにはバックエンドが存在しないため、Firebase App / Auth /
|
|
5
|
+
* Firestore / Storage / Functions のインスタンスはすべてダミーオブジェクトを返す。
|
|
6
|
+
* `firebase/*` パッケージからの import は一切行わない(バンドルに混入させないため)。
|
|
7
|
+
*
|
|
8
|
+
* 実際の永続化は同ディレクトリの css-variables.ts / editor-components.ts /
|
|
9
|
+
* storage.ts が localStorage・data URL で代替している。
|
|
10
|
+
* ここから export されるオブジェクトは「import してもクラッシュしない」ことだけを
|
|
11
|
+
* 保証するプレースホルダであり、メソッド呼び出しは想定していない。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Firebase App のダミー */
|
|
15
|
+
export const app = {} as any;
|
|
16
|
+
|
|
17
|
+
/** Firebase Auth のダミー */
|
|
18
|
+
export const auth = {} as any;
|
|
19
|
+
|
|
20
|
+
/** Cloud Firestore のダミー */
|
|
21
|
+
export const db = {} as any;
|
|
22
|
+
|
|
23
|
+
/** Firebase Storage のダミー */
|
|
24
|
+
export const storage = {} as any;
|
|
25
|
+
|
|
26
|
+
/** Cloud Functions のダミー(asia-northeast1 相当) */
|
|
27
|
+
export const functions = {} as any;
|
|
28
|
+
|
|
29
|
+
export default app;
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ローカル永続化スタブ。オリジナルは Firebase 実装(Firestore)。
|
|
3
|
+
*
|
|
4
|
+
* オリジナルは /websites/{websiteId}/cssVariables/current または
|
|
5
|
+
* /presentations/{presentationId}/cssVariables/current に保存していたが、
|
|
6
|
+
* ここではバックエンドを持たないため localStorage に JSON で保存する。
|
|
7
|
+
*
|
|
8
|
+
* localStorage キー:
|
|
9
|
+
* - website スコープ: `gg-editor:cssVariables:<resourceId>`
|
|
10
|
+
* - presentation スコープ: `gg-editor:cssVariables:presentation:<resourceId>`
|
|
11
|
+
*
|
|
12
|
+
* export 名・引数・戻り値型はオリジナルと同一(呼び出し側を変更しないため)。
|
|
13
|
+
* Firestore Timestamp の代わりに ISO 文字列で保存し、読み出し時に Date へ変換する。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
CSSVariableDefinition,
|
|
18
|
+
CSSVariableCollection,
|
|
19
|
+
} from '../../types/css-variables';
|
|
20
|
+
|
|
21
|
+
// ============================================
|
|
22
|
+
// スコープタイプ定義
|
|
23
|
+
// ============================================
|
|
24
|
+
|
|
25
|
+
export type CSSVariableScope = 'website' | 'presentation';
|
|
26
|
+
|
|
27
|
+
// ============================================
|
|
28
|
+
// localStorage パス定義
|
|
29
|
+
// ============================================
|
|
30
|
+
|
|
31
|
+
const STORAGE_PREFIX = 'gg-editor:cssVariables';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* CSS変数コレクションの localStorage キーを取得
|
|
35
|
+
* (オリジナルの getCSSVariablesDocRef 相当)
|
|
36
|
+
* @param resourceId - website ID または presentation ID
|
|
37
|
+
* @param scope - 'website'(デフォルト)または 'presentation'
|
|
38
|
+
*/
|
|
39
|
+
function getCSSVariablesKey(resourceId: string, scope: CSSVariableScope = 'website'): string {
|
|
40
|
+
return scope === 'presentation'
|
|
41
|
+
? `${STORAGE_PREFIX}:presentation:${resourceId}`
|
|
42
|
+
: `${STORAGE_PREFIX}:${resourceId}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* localStorage が利用可能かどうか(SSR / 制限環境対策)
|
|
47
|
+
*/
|
|
48
|
+
function getStore(): Storage | null {
|
|
49
|
+
try {
|
|
50
|
+
if (typeof window === 'undefined' || !window.localStorage) return null;
|
|
51
|
+
return window.localStorage;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 保存済みドキュメントを読み出す(存在しなければ null)
|
|
59
|
+
*/
|
|
60
|
+
function readDoc(key: string): Record<string, unknown> | null {
|
|
61
|
+
const store = getStore();
|
|
62
|
+
if (!store) return null;
|
|
63
|
+
|
|
64
|
+
const raw = store.getItem(key);
|
|
65
|
+
if (!raw) return null;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
70
|
+
return parsed as Record<string, unknown>;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('[CSSVariables] Failed to parse stored data:', error);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* ドキュメントを書き込む
|
|
79
|
+
*/
|
|
80
|
+
function writeDoc(key: string, data: Record<string, unknown>): void {
|
|
81
|
+
const store = getStore();
|
|
82
|
+
if (!store) return;
|
|
83
|
+
store.setItem(key, JSON.stringify(data));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================
|
|
87
|
+
// 型変換ヘルパー
|
|
88
|
+
// ============================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 保存データを型付きオブジェクトに変換
|
|
92
|
+
*/
|
|
93
|
+
function convertToCollection(
|
|
94
|
+
docData: Record<string, unknown>,
|
|
95
|
+
websiteId: string
|
|
96
|
+
): CSSVariableCollection {
|
|
97
|
+
const variables = ((docData.variables as Array<Record<string, unknown>>) || []).map(v => ({
|
|
98
|
+
id: v.id as string,
|
|
99
|
+
name: v.name as string,
|
|
100
|
+
cssName: v.cssName as string,
|
|
101
|
+
value: v.value as string,
|
|
102
|
+
category: v.category as CSSVariableDefinition['category'],
|
|
103
|
+
description: v.description as string | undefined,
|
|
104
|
+
numericMeta: v.numericMeta as CSSVariableDefinition['numericMeta'],
|
|
105
|
+
breakpointValues: v.breakpointValues as CSSVariableDefinition['breakpointValues'],
|
|
106
|
+
createdAt: convertTimestamp(v.createdAt),
|
|
107
|
+
updatedAt: convertTimestamp(v.updatedAt),
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
id: 'current',
|
|
112
|
+
scope: 'project',
|
|
113
|
+
scopeId: websiteId,
|
|
114
|
+
variables,
|
|
115
|
+
syncedFromTheme: docData.syncedFromTheme as boolean | undefined,
|
|
116
|
+
lastSyncedAt: convertTimestamp(docData.lastSyncedAt),
|
|
117
|
+
createdAt: convertTimestamp(docData.createdAt) ?? new Date(),
|
|
118
|
+
updatedAt: convertTimestamp(docData.updatedAt) ?? new Date(),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 保存値(ISO文字列 / Date / Timestamp 互換オブジェクト)を Date に変換
|
|
124
|
+
*/
|
|
125
|
+
function convertTimestamp(value: unknown): Date | undefined {
|
|
126
|
+
if (!value) return undefined;
|
|
127
|
+
if (value instanceof Date) return value;
|
|
128
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
129
|
+
const date = new Date(value);
|
|
130
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
131
|
+
}
|
|
132
|
+
if (typeof value === 'object' && value !== null && 'toDate' in value) {
|
|
133
|
+
try {
|
|
134
|
+
return (value as { toDate: () => Date }).toDate();
|
|
135
|
+
} catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 保存用に日時を ISO 文字列へ変換
|
|
144
|
+
*/
|
|
145
|
+
function toISO(value: unknown): string {
|
|
146
|
+
const date = convertTimestamp(value);
|
|
147
|
+
return (date ?? new Date()).toISOString();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 保存用に変数リストをシリアライズ可能な形へ変換
|
|
152
|
+
*/
|
|
153
|
+
function prepareForSave(variables: CSSVariableDefinition[]): Array<Record<string, unknown>> {
|
|
154
|
+
const now = new Date().toISOString();
|
|
155
|
+
return variables.map(v => ({
|
|
156
|
+
id: v.id,
|
|
157
|
+
name: v.name,
|
|
158
|
+
cssName: v.cssName,
|
|
159
|
+
value: v.value,
|
|
160
|
+
category: v.category,
|
|
161
|
+
description: v.description ?? null,
|
|
162
|
+
numericMeta: v.numericMeta ?? null,
|
|
163
|
+
breakpointValues: v.breakpointValues ?? null,
|
|
164
|
+
createdAt: v.createdAt ? toISO(v.createdAt) : now,
|
|
165
|
+
updatedAt: now,
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ============================================
|
|
170
|
+
// CRUD 操作
|
|
171
|
+
// ============================================
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* CSS変数コレクションを取得
|
|
175
|
+
* @param resourceId - website ID または presentation ID
|
|
176
|
+
* @param scope - 'website'(デフォルト)または 'presentation'
|
|
177
|
+
*/
|
|
178
|
+
export async function getCSSVariables(
|
|
179
|
+
resourceId: string,
|
|
180
|
+
scope: CSSVariableScope = 'website'
|
|
181
|
+
): Promise<CSSVariableCollection | null> {
|
|
182
|
+
try {
|
|
183
|
+
const key = getCSSVariablesKey(resourceId, scope);
|
|
184
|
+
const data = readDoc(key);
|
|
185
|
+
|
|
186
|
+
if (!data) {
|
|
187
|
+
console.log(`[CSSVariables] No variables found for ${scope}:`, resourceId);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return convertToCollection(data, resourceId);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error('[CSSVariables] Error getting variables:', error);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* CSS変数リストのみを取得
|
|
200
|
+
* @param resourceId - website ID または presentation ID
|
|
201
|
+
* @param scope - 'website'(デフォルト)または 'presentation'
|
|
202
|
+
*/
|
|
203
|
+
export async function getCSSVariablesList(
|
|
204
|
+
resourceId: string,
|
|
205
|
+
scope: CSSVariableScope = 'website'
|
|
206
|
+
): Promise<CSSVariableDefinition[]> {
|
|
207
|
+
const collection = await getCSSVariables(resourceId, scope);
|
|
208
|
+
return collection?.variables ?? [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* CSS変数コレクションを保存(全体上書き)
|
|
213
|
+
* @param resourceId - website ID または presentation ID
|
|
214
|
+
* @param variables - CSS変数リスト
|
|
215
|
+
* @param options - オプション(syncedFromTheme, scope)
|
|
216
|
+
*/
|
|
217
|
+
export async function saveCSSVariables(
|
|
218
|
+
resourceId: string,
|
|
219
|
+
variables: CSSVariableDefinition[],
|
|
220
|
+
options?: {
|
|
221
|
+
syncedFromTheme?: boolean;
|
|
222
|
+
scope?: CSSVariableScope;
|
|
223
|
+
}
|
|
224
|
+
): Promise<void> {
|
|
225
|
+
try {
|
|
226
|
+
const scope = options?.scope ?? 'website';
|
|
227
|
+
const key = getCSSVariablesKey(resourceId, scope);
|
|
228
|
+
const now = new Date().toISOString();
|
|
229
|
+
|
|
230
|
+
// 既存ドキュメントをチェック(createdAt を維持)
|
|
231
|
+
const existing = readDoc(key);
|
|
232
|
+
const createdAt = existing?.createdAt ? toISO(existing.createdAt) : now;
|
|
233
|
+
|
|
234
|
+
const data = {
|
|
235
|
+
variables: prepareForSave(variables),
|
|
236
|
+
syncedFromTheme: options?.syncedFromTheme ?? false,
|
|
237
|
+
lastSyncedAt: options?.syncedFromTheme ? now : null,
|
|
238
|
+
createdAt,
|
|
239
|
+
updatedAt: now,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
writeDoc(key, data);
|
|
243
|
+
console.log(`[CSSVariables] Saved variables for ${scope}:`, resourceId, 'count:', variables.length);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error('[CSSVariables] Error saving variables:', error);
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* CSS変数を追加(既存に追加)
|
|
252
|
+
*/
|
|
253
|
+
export async function addCSSVariable(
|
|
254
|
+
websiteId: string,
|
|
255
|
+
variable: CSSVariableDefinition
|
|
256
|
+
): Promise<void> {
|
|
257
|
+
const existing = await getCSSVariablesList(websiteId);
|
|
258
|
+
await saveCSSVariables(websiteId, [...existing, variable]);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* CSS変数を更新
|
|
263
|
+
*/
|
|
264
|
+
export async function updateCSSVariable(
|
|
265
|
+
websiteId: string,
|
|
266
|
+
variableId: string,
|
|
267
|
+
updates: Partial<Omit<CSSVariableDefinition, 'id'>>
|
|
268
|
+
): Promise<void> {
|
|
269
|
+
const existing = await getCSSVariablesList(websiteId);
|
|
270
|
+
const updatedVariables = existing.map(v => {
|
|
271
|
+
if (v.id !== variableId) return v;
|
|
272
|
+
return {
|
|
273
|
+
...v,
|
|
274
|
+
...updates,
|
|
275
|
+
updatedAt: new Date(),
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
await saveCSSVariables(websiteId, updatedVariables);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* CSS変数を削除
|
|
283
|
+
*/
|
|
284
|
+
export async function deleteCSSVariable(
|
|
285
|
+
websiteId: string,
|
|
286
|
+
variableId: string
|
|
287
|
+
): Promise<void> {
|
|
288
|
+
const existing = await getCSSVariablesList(websiteId);
|
|
289
|
+
const filteredVariables = existing.filter(v => v.id !== variableId);
|
|
290
|
+
await saveCSSVariables(websiteId, filteredVariables);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* CSS変数コレクションを削除
|
|
295
|
+
*/
|
|
296
|
+
export async function deleteCSSVariablesCollection(websiteId: string): Promise<void> {
|
|
297
|
+
try {
|
|
298
|
+
const key = getCSSVariablesKey(websiteId);
|
|
299
|
+
const store = getStore();
|
|
300
|
+
if (store) store.removeItem(key);
|
|
301
|
+
console.log('[CSSVariables] Deleted variables for website:', websiteId);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error('[CSSVariables] Error deleting variables:', error);
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ============================================
|
|
309
|
+
// バルク操作
|
|
310
|
+
// ============================================
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 複数の変数を一括追加
|
|
314
|
+
*/
|
|
315
|
+
export async function addCSSVariablesBulk(
|
|
316
|
+
websiteId: string,
|
|
317
|
+
variables: CSSVariableDefinition[]
|
|
318
|
+
): Promise<void> {
|
|
319
|
+
const existing = await getCSSVariablesList(websiteId);
|
|
320
|
+
await saveCSSVariables(websiteId, [...existing, ...variables]);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 変数をマージ(同じcssNameは上書き、新規は追加)
|
|
325
|
+
*/
|
|
326
|
+
export async function mergeCSSVariables(
|
|
327
|
+
websiteId: string,
|
|
328
|
+
variables: CSSVariableDefinition[],
|
|
329
|
+
options?: { syncedFromTheme?: boolean }
|
|
330
|
+
): Promise<void> {
|
|
331
|
+
const existing = await getCSSVariablesList(websiteId);
|
|
332
|
+
|
|
333
|
+
// cssNameでマップを作成
|
|
334
|
+
const existingMap = new Map(existing.map(v => [v.cssName, v]));
|
|
335
|
+
|
|
336
|
+
// マージ
|
|
337
|
+
for (const newVar of variables) {
|
|
338
|
+
const existingVar = existingMap.get(newVar.cssName);
|
|
339
|
+
if (existingVar) {
|
|
340
|
+
// 既存変数を更新(IDは維持)
|
|
341
|
+
existingMap.set(newVar.cssName, {
|
|
342
|
+
...existingVar,
|
|
343
|
+
...newVar,
|
|
344
|
+
id: existingVar.id, // 既存IDを維持
|
|
345
|
+
updatedAt: new Date(),
|
|
346
|
+
});
|
|
347
|
+
} else {
|
|
348
|
+
// 新規追加
|
|
349
|
+
existingMap.set(newVar.cssName, newVar);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
await saveCSSVariables(websiteId, Array.from(existingMap.values()), options);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ============================================
|
|
357
|
+
// 検証・ユーティリティ
|
|
358
|
+
// ============================================
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* 変数名の重複チェック
|
|
362
|
+
*/
|
|
363
|
+
export async function isVariableNameTaken(
|
|
364
|
+
websiteId: string,
|
|
365
|
+
cssName: string,
|
|
366
|
+
excludeId?: string
|
|
367
|
+
): Promise<boolean> {
|
|
368
|
+
const existing = await getCSSVariablesList(websiteId);
|
|
369
|
+
return existing.some(v => v.cssName === cssName && v.id !== excludeId);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* CSS変数が存在するかチェック
|
|
374
|
+
*/
|
|
375
|
+
export async function hasCSSVariables(websiteId: string): Promise<boolean> {
|
|
376
|
+
const collection = await getCSSVariables(websiteId);
|
|
377
|
+
return collection !== null && collection.variables.length > 0;
|
|
378
|
+
}
|