Skip to content

Commit 542e71a

Browse files
add presets
1 parent b78a0d2 commit 542e71a

17 files changed

Lines changed: 1226 additions & 24 deletions

examples/react-example/src/index.tsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import {
22
ImageKitEditor,
33
type ImageKitEditorProps,
44
type ImageKitEditorRef,
5+
type PresetLayerType,
6+
type PresetRecord,
7+
type PresetStorageProvider,
8+
type SavePresetInput,
59
type TemplateStorageProvider,
610
TRANSFORMATION_STATE_VERSION,
711
type Transformation,
@@ -112,6 +116,86 @@ function createLocalTemplateStorage(): TemplateStorageProvider {
112116
}
113117
}
114118

119+
const PRESET_STORAGE_KEY = "ik-editor:presets:v1"
120+
121+
function readAllPresets(): PresetRecord[] {
122+
const raw = localStorage.getItem(PRESET_STORAGE_KEY)
123+
if (!raw) return []
124+
try {
125+
const parsed = JSON.parse(raw)
126+
return Array.isArray(parsed) ? (parsed as PresetRecord[]) : []
127+
} catch {
128+
return []
129+
}
130+
}
131+
132+
function writeAllPresets(records: PresetRecord[]) {
133+
localStorage.setItem(PRESET_STORAGE_KEY, JSON.stringify(records))
134+
}
135+
136+
function createLocalPresetStorage(): PresetStorageProvider {
137+
const session = {
138+
userId: "demo-user",
139+
name: "Demo User",
140+
email: "demo@example.com",
141+
clientNumber: "demo-client",
142+
}
143+
144+
return {
145+
async listPresets(layerType?: PresetLayerType) {
146+
const all = readAllPresets().sort((a, b) => b.updatedAt - a.updatedAt)
147+
return layerType ? all.filter((p) => p.layerType === layerType) : all
148+
},
149+
async getPreset(id: string) {
150+
return readAllPresets().find((p) => p.id === id) ?? null
151+
},
152+
async savePreset(input: SavePresetInput) {
153+
const now = Date.now()
154+
const all = readAllPresets()
155+
const existing = input.id
156+
? (all.find((p) => p.id === input.id) ?? null)
157+
: null
158+
159+
const id = existing?.id ?? crypto.randomUUID?.() ?? String(now)
160+
const record: PresetRecord = {
161+
id,
162+
clientNumber: input.clientNumber ?? existing?.clientNumber ?? "demo",
163+
isPrivate: input.isPrivate ?? existing?.isPrivate ?? false,
164+
name: input.name,
165+
layerType: input.layerType,
166+
fieldValues: input.fieldValues,
167+
params: input.params,
168+
createdBy: input.createdBy ??
169+
existing?.createdBy ?? {
170+
userId: session.userId,
171+
name: session.name,
172+
email: session.email,
173+
},
174+
updatedBy: input.updatedBy ?? {
175+
userId: session.userId,
176+
name: session.name,
177+
email: session.email,
178+
},
179+
createdAt: input.createdAt ?? existing?.createdAt ?? now,
180+
updatedAt: input.updatedAt ?? now,
181+
}
182+
183+
const next = [record, ...all.filter((p) => p.id !== id)]
184+
writeAllPresets(next)
185+
return record
186+
},
187+
async deletePreset(id: string) {
188+
writeAllPresets(readAllPresets().filter((p) => p.id !== id))
189+
},
190+
getProviderName() {
191+
return "localStorage"
192+
},
193+
getCurrentUserSession() {
194+
return session
195+
},
196+
}
197+
}
198+
115199
function App() {
116200
const [open, setOpen] = React.useState(true)
117201
const [editorProps, setEditorProps] =
@@ -256,6 +340,7 @@ function App() {
256340
return Promise.resolve(request.url)
257341
},
258342
templateStorage: createLocalTemplateStorage(),
343+
presetStorage: createLocalPresetStorage(),
259344
})
260345
}, [handleAddImage])
261346

packages/imagekit-editor-dev/src/ImageKitEditor.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import React, {
99
} from "react"
1010
import { EditorLayout, EditorWrapper } from "./components/editor"
1111
import type { HeaderProps } from "./components/header"
12+
import { PresetStorageContextProvider } from "./context/PresetStorageContext"
1213
import type { GetTemplatePermissions } from "./context/TemplatePermissionsContext"
1314
import { TemplatePermissionsContextProvider } from "./context/TemplatePermissionsContext"
1415
import { TemplateStorageContextProvider } from "./context/TemplateStorageContext"
1516
import {
1617
isTemplateAccessDeniedError,
18+
type PresetStorageProvider,
1719
type TemplateStorageProvider,
1820
} from "./storage"
1921
import {
@@ -99,6 +101,11 @@ interface EditorProps<Metadata extends RequiredMetadata = RequiredMetadata> {
99101
* Omit or pass `null` to disable template sync UI.
100102
*/
101103
templateStorage?: TemplateStorageProvider | null
104+
/**
105+
* Preset persistence (list/save/delete) for image/text layer presets.
106+
* Implemented by the host app. Omit or pass `null` to disable preset UI.
107+
*/
108+
presetStorage?: PresetStorageProvider | null
102109
/**
103110
* Host-controlled, per-template permissions for template management UI.
104111
* If omitted, the editor defaults to allowing all actions.
@@ -122,6 +129,7 @@ function ImageKitEditorImpl<M extends RequiredMetadata>(
122129
signer,
123130
focusObjects,
124131
templateStorage,
132+
presetStorage,
125133
getTemplatePermissions,
126134
canvasMode,
127135
imagekitId,
@@ -141,6 +149,11 @@ function ImageKitEditorImpl<M extends RequiredMetadata>(
141149
[templateStorage],
142150
)
143151

152+
const resolvedPresetProvider = useMemo<PresetStorageProvider | null>(
153+
() => presetStorage ?? null,
154+
[presetStorage],
155+
)
156+
144157
const saveTemplateImperative = useCallback(async () => {
145158
// Avoid importing hooks here; implement via store+provider with version gating.
146159
if (!resolvedProvider) return
@@ -252,13 +265,15 @@ function ImageKitEditorImpl<M extends RequiredMetadata>(
252265
getTemplatePermissions={getTemplatePermissions}
253266
>
254267
<TemplateStorageContextProvider provider={resolvedProvider}>
255-
<EditorWrapper>
256-
<EditorLayout
257-
onAddImage={props.onAddImage}
258-
onClose={handleOnClose}
259-
exportOptions={props.exportOptions}
260-
/>
261-
</EditorWrapper>
268+
<PresetStorageContextProvider provider={resolvedPresetProvider}>
269+
<EditorWrapper>
270+
<EditorLayout
271+
onAddImage={props.onAddImage}
272+
onClose={handleOnClose}
273+
exportOptions={props.exportOptions}
274+
/>
275+
</EditorWrapper>
276+
</PresetStorageContextProvider>
262277
</TemplateStorageContextProvider>
263278
</TemplatePermissionsContextProvider>
264279
</ChakraProvider>

packages/imagekit-editor-dev/src/components/editor/layout.tsx

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { Box, Flex } from "@chakra-ui/react"
2-
import { useEffect, useState } from "react"
2+
import { useEffect, useMemo, useState } from "react"
3+
import { PresetsLibraryToggleContext } from "../../context/PresetsLibraryToggleContext"
34
import { useAutoSaveTemplate } from "../../hooks/useAutoSaveTemplate"
45
import { useSaveTemplate } from "../../hooks/useSaveTemplate"
56
import { useEditorStore } from "../../store"
67
import { Header, type HeaderProps } from "../header"
8+
import { PresetsLibraryView } from "../presets/PresetsLibraryView"
79
import { Sidebar } from "../sidebar"
810
import { TemplatesLibraryView } from "../templates/TemplatesLibraryView"
911
import { ActionBar } from "./ActionBar"
@@ -22,6 +24,7 @@ export function EditorLayout({ onAddImage, onClose, exportOptions }: Props) {
2224
const [viewMode, setViewMode] = useState<"list" | "grid">("list")
2325
const [gridImageSize, setGridImageSize] = useState<number>(300)
2426
const [isTemplatesOpen, setIsTemplatesOpen] = useState(false)
27+
const [isPresetsOpen, setIsPresetsOpen] = useState(false)
2528

2629
// Close templates modal on Escape while it's open
2730
useEffect(() => {
@@ -38,17 +41,38 @@ export function EditorLayout({ onAddImage, onClose, exportOptions }: Props) {
3841
}
3942
}, [isTemplatesOpen])
4043

44+
// Close presets modal on Escape while it's open
45+
useEffect(() => {
46+
if (!isPresetsOpen) return
47+
const handleKeyDown = (event: KeyboardEvent) => {
48+
if (event.key === "Escape") {
49+
event.stopPropagation()
50+
setIsPresetsOpen(false)
51+
}
52+
}
53+
window.addEventListener("keydown", handleKeyDown)
54+
return () => {
55+
window.removeEventListener("keydown", handleKeyDown)
56+
}
57+
}, [isPresetsOpen])
58+
4159
useAutoSaveTemplate()
4260
useSaveTemplate()
4361

4462
const closeTemplatesLibrary = () => setIsTemplatesOpen(false)
63+
const closePresetsLibrary = () => setIsPresetsOpen(false)
64+
const presetsToggle = useMemo(
65+
() => ({ open: () => setIsPresetsOpen(true) }),
66+
[],
67+
)
4568

4669
return (
47-
<>
70+
<PresetsLibraryToggleContext.Provider value={presetsToggle}>
4871
<Header
4972
onClose={onClose}
5073
exportOptions={exportOptions}
5174
onViewAllTemplates={() => setIsTemplatesOpen(true)}
75+
onViewAllPresets={() => setIsPresetsOpen(true)}
5276
/>
5377
<Flex flexDirection="row" width="full" height="full" flexGrow={0}>
5478
<Sidebar />
@@ -107,6 +131,35 @@ export function EditorLayout({ onAddImage, onClose, exportOptions }: Props) {
107131
</Box>
108132
</Box>
109133
) : null}
110-
</>
134+
{isPresetsOpen ? (
135+
<Box
136+
position="fixed"
137+
inset={0}
138+
bg="blackAlpha.400"
139+
display="flex"
140+
alignItems="center"
141+
justifyContent="center"
142+
zIndex={1400}
143+
onClick={closePresetsLibrary}
144+
>
145+
<Box
146+
w="60vw"
147+
h="70vh"
148+
maxW="800px"
149+
maxH="80vh"
150+
bg="white"
151+
borderRadius="xl"
152+
overflow="hidden"
153+
boxShadow="xl"
154+
display="flex"
155+
flexDirection="column"
156+
position="relative"
157+
onClick={(e) => e.stopPropagation()}
158+
>
159+
<PresetsLibraryView onClose={closePresetsLibrary} />
160+
</Box>
161+
</Box>
162+
) : null}
163+
</PresetsLibraryToggleContext.Provider>
111164
)
112165
}

packages/imagekit-editor-dev/src/components/header/index.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import {
88
MenuList,
99
Spacer,
1010
} from "@chakra-ui/react"
11+
import { PiBookmarksSimple } from "@react-icons/all-files/pi/PiBookmarksSimple"
1112
import { PiGear } from "@react-icons/all-files/pi/PiGear"
1213
import { PiGlobe } from "@react-icons/all-files/pi/PiGlobe"
1314
import { PiLock } from "@react-icons/all-files/pi/PiLock"
1415
import { PiX } from "@react-icons/all-files/pi/PiX"
1516
import React, { useEffect, useState } from "react"
17+
import { usePresetStorage } from "../../context/PresetStorageContext"
1618
import { useTemplateStorage } from "../../context/TemplateStorageContext"
1719
import { applyTemplateStorageAccessFailure } from "../../storage/templateAccessError"
1820
import type { TemplateRecord } from "../../storage/types"
@@ -59,12 +61,14 @@ export interface HeaderProps<
5961
ExportOptionButton<Metadata> | ExportOptionMenu<Metadata>
6062
>
6163
onViewAllTemplates?: () => void
64+
onViewAllPresets?: () => void
6265
}
6366

6467
export const Header = ({
6568
onClose,
6669
exportOptions,
6770
onViewAllTemplates,
71+
onViewAllPresets,
6872
}: HeaderProps): React.ReactElement => {
6973
const FlexAny = chakraAny(Flex)
7074
const DividerAny = chakraAny(Divider)
@@ -77,6 +81,7 @@ export const Header = ({
7781
const templateIsPrivate = useEditorStore((s) => s.templateIsPrivate)
7882
const syncStatus = useEditorStore((s) => s.syncStatus)
7983
const provider = useTemplateStorage()
84+
const presetProvider = usePresetStorage()
8085

8186
const [activeRecord, setActiveRecord] = useState<TemplateRecord | null>(null)
8287
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
@@ -179,6 +184,20 @@ export const Header = ({
179184
<FlexAny alignItems="center">
180185
<TemplatesDropdown onViewAllTemplates={onViewAllTemplates} />
181186
</FlexAny>
187+
{presetProvider && onViewAllPresets ? (
188+
<>
189+
<DividerAny
190+
orientation="vertical"
191+
borderColor="editorBattleshipGrey.200"
192+
height="40%"
193+
/>
194+
<NavbarItem
195+
label="Presets"
196+
icon={<PiBookmarksSimple />}
197+
onClick={onViewAllPresets}
198+
/>
199+
</>
200+
) : null}
182201
<DividerAny
183202
orientation="vertical"
184203
borderColor="editorBattleshipGrey.200"

0 commit comments

Comments
 (0)