Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0e9b71c
chore: add yalc for local package linking and development workflow
ahnv May 6, 2026
27ee907
feat: add initialTemplateId prop to ImageKitEditor and implement temp…
manu4543 May 7, 2026
b1fd803
feat: introduce canvas mode and editor mode configuration; enhance te…
manu4543 May 7, 2026
71cfda4
feat: add CanvasSettingsPopover component for canvas configuration; i…
manu4543 May 7, 2026
3fbc042
feat: enhance CanvasSettingsPopover with background toggle and update…
manu4543 May 8, 2026
ce0b2e7
feat: introduce variable support in transformation editor
manu4543 May 8, 2026
a5cdd6d
feat: prevent infinite loop in VariableField by stabilizing onChange …
manu4543 May 9, 2026
f302f99
feat: add unique id handling for VariableField and TransformationFiel…
manu4543 May 9, 2026
0b4e56b
feat: add async image picker support to VariableField and Transformat…
manu4543 May 9, 2026
a077453
feat: update @imagekit/javascript to version 5.3.0 and add layer anch…
manu4543 May 9, 2026
ca7e185
feat: add rawTransformation support for text and image layers in tran…
manu4543 May 9, 2026
6169cfc
fix: update help text for transformation schema to remove unnecessary…
manu4543 May 9, 2026
e609206
feat: add support for nested layers
manu4543 May 9, 2026
9cd602c
feat: enhance UI interactions in SortableTransformationItem with impr…
manu4543 May 9, 2026
13488c0
feat: add tests for buildVariablesSchema and enhance variable resolut…
manu4543 May 10, 2026
9fa9e6e
feat: add sourceUrl to CanvasConfig and enforce its presence in canva…
manu4543 May 10, 2026
de801a8
feat: enhance variable resolution to include nested layer children in…
manu4543 May 10, 2026
7872b88
feat: improve default values handling and error filtering for transfo…
manu4543 May 10, 2026
8419b8c
feat: enhance variable handling and validation in transformation config
manu4543 May 14, 2026
76e2472
feat: add VariablesListPopover component and integrate variable displ…
manu4543 May 14, 2026
c86d617
feat: enhance transformation handling with nested layer support and c…
manu4543 May 14, 2026
894c7eb
feat: enhance file picker with tooltip and add compact clear indicato…
manu4543 May 14, 2026
5321587
Merge with main
manu4543 May 15, 2026
0c7ffde
Fix lint
manu4543 May 15, 2026
1978412
fix: update coverage include paths and lower branch threshold to acco…
manu4543 May 15, 2026
c69b548
feat: add colorize transformation with adjustable tint and intensity
manu4543 May 15, 2026
6ea22d3
feat: add new crop/resize modes and update visibility logic for no en…
manu4543 May 15, 2026
1e20135
fix: update transformation initialization to use transformation ID in…
manu4543 May 15, 2026
b018acc
fix: update resume session modal to remove close editor functionality…
manu4543 May 15, 2026
f2889cf
fix: synchronize internal state with external value prop changes in P…
manu4543 May 16, 2026
880346a
fix: synchronize internal state with external value prop changes in D…
manu4543 May 16, 2026
d0ae80b
feat: implement deduplication of variable markers in transformations …
manu4543 May 16, 2026
caec846
fix: ensure icon interactivity during notification window in Template…
manu4543 May 16, 2026
7de6592
refactor(header): replace TemplateStatus popover with inline Save button
manu4543 May 16, 2026
ad0ebed
fix(schema): clarify backgroundColor picker restrictions and update h…
manu4543 May 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/imagekit-editor-dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@hookform/resolvers": "^5.1.1",
"@imagekit/javascript": "^5.1.0",
"@imagekit/javascript": "^5.4.0",
"@react-icons/all-files": "https://github.com/react-icons/react-icons/releases/download/v5.4.0/react-icons-all-files-5.4.0.tgz",
"@tanstack/react-virtual": "^3.13.12",
"framer-motion": "6.5.1",
Expand Down
1 change: 0 additions & 1 deletion packages/imagekit-editor-dev/src/ImageKitEditor.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import "@testing-library/jest-dom/vitest"
import { render, screen, waitFor } from "@testing-library/react"
import React from "react"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { ImageKitEditor } from "./ImageKitEditor"
import {
Expand Down
124 changes: 117 additions & 7 deletions packages/imagekit-editor-dev/src/ImageKitEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,24 @@ import {
readEditorSessionFromLocalStorage,
} from "./persistence/editorSessionStorage"
import {
applyTemplateStorageAccessFailure,
isTemplateAccessDeniedError,
type TemplateStorageProvider,
} from "./storage"
import {
applyTemplateRecord,
type CanvasConfig,
type EditorMode,
type FocusObjects,
type InputFileElement,
type OnPickImage,
type RequiredMetadata,
type Signer,
type Transformation,
useEditorStore,
} from "./store"
import { themeOverrides } from "./theme"
import { dedupeVariableMarkersInList } from "./variables"

export interface ImageKitEditorRef {
/**
Expand Down Expand Up @@ -93,6 +99,16 @@ interface EditorProps<Metadata extends RequiredMetadata = RequiredMetadata> {
initialImages?: Array<string | InputFileElement<Metadata>>
signer?: Signer<Metadata>
onAddImage?: () => void
/**
* Optional async image picker. When provided, image-path fields (currently
* the image layer's `imageUrl`) render a small folder icon next to the
* input; clicking it invokes this callback. Resolve to a URL/path string to
* fill the field, or to `null`/`undefined` to leave the field unchanged.
*
* The host owns the picker UI and any backend calls; the editor never opens
* a media library itself.
*/
onPickImage?: OnPickImage
exportOptions?: HeaderProps<Metadata>["exportOptions"]
focusObjects?: ReadonlyArray<FocusObjects>
onClose: (args: { dirty: boolean; destroy: () => void }) => void
Expand All @@ -107,6 +123,26 @@ interface EditorProps<Metadata extends RequiredMetadata = RequiredMetadata> {
* If omitted, the editor defaults to allowing all actions.
*/
getTemplatePermissions?: GetTemplatePermissions
/**
* Open the editor with this template pre-loaded. The editor calls
* `templateStorage.getTemplate(initialTemplateId)` on mount and applies
* the result. Requires `templateStorage` to be configured.
*
* Failures (template not found, access denied, network error) are surfaced
* via the standard sync-status error UI; the editor still opens empty.
*/
initialTemplateId?: string
/**
* Editor authoring mode. Defaults to `"editing"` (regular media editing).
* Pass `"canvas"` to author a layer-only template against a sized blank
* canvas; `canvas` prop must also be provided in that case.
*
* If `initialTemplateId` is supplied and the loaded template has its own
* `mode`, that wins (the template carries its authoring context).
*/
mode?: EditorMode
/** Canvas dimensions and optional background. Required when `mode === "canvas"`. */
canvas?: CanvasConfig
}

function ImageKitEditorImpl<M extends RequiredMetadata>(
Expand All @@ -117,9 +153,13 @@ function ImageKitEditorImpl<M extends RequiredMetadata>(
theme,
initialImages,
signer,
onPickImage,
focusObjects,
templateStorage,
getTemplatePermissions,
initialTemplateId,
mode,
canvas,
} = props
const {
addImage,
Expand All @@ -140,6 +180,10 @@ function ImageKitEditorImpl<M extends RequiredMetadata>(
useState<PersistedEditorSession | null>(null)

React.useEffect(() => {
// Canvas-mode templates author a fixed-size blank canvas (often paired
// with a specific sourceUrl/dimensions) and aren't compatible with a
// generic resumed editing session — skip the prompt entirely.
if (mode === "canvas") return
const resumableSession = readEditorSessionFromLocalStorage(
EDITOR_SESSION_STORAGE_KEY,
)
Expand All @@ -150,7 +194,7 @@ function ImageKitEditorImpl<M extends RequiredMetadata>(
: !persisted.isPristine
if (!hasUnsavedChanges) return
setResumeSession(resumableSession)
}, [resolvedProvider])
}, [resolvedProvider, mode])

const saveTemplateImperative = useCallback(async () => {
// Avoid importing hooks here; implement via store+provider with version gating.
Expand All @@ -161,15 +205,23 @@ function ImageKitEditorImpl<M extends RequiredMetadata>(
const saveStartedAtVersion = state.localChangeVersion
state.setSyncStatus("saving")
try {
// Dedupe variable markers at the save boundary so the persisted JSON
// never contains two `$var` markers with the same name (see
// useTemplateSync for the matching call on the hook-driven path).
const safeTransformations = dedupeVariableMarkersInList(
state.transformations,
)
const saved = await resolvedProvider.saveTemplate({
id: state.templateId ?? undefined,
name: state.templateName,
transformations: state.transformations.map(
transformations: safeTransformations.map(
({ id: _id, ...rest }) => rest,
),
...(state.templateIsPrivate !== null
? { isPrivate: state.templateIsPrivate }
: {}),
mode: state.mode,
...(state.mode === "canvas" ? { canvas: state.canvas } : {}),
})
const after = useEditorStore.getState()
after.hydrateTemplateMetadata({
Expand Down Expand Up @@ -230,9 +282,68 @@ function ImageKitEditorImpl<M extends RequiredMetadata>(
initialize({
imageList: initialImages,
signer,
onPickImage,
focusObjects,
mode,
canvas,
})
}, [initialImages, signer, focusObjects, initialize])
}, [
initialImages,
signer,
onPickImage,
focusObjects,
initialize,
mode,
canvas,
])

// Load template by id from the configured storage provider when
// `initialTemplateId` is supplied. This runs after `initialize` so it can
// overwrite any reset metadata. Keyed on (provider, id) so switching either
// re-fetches.
React.useEffect(() => {
if (!initialTemplateId) return
if (!resolvedProvider) {
console.warn(
"ImageKitEditor: `initialTemplateId` was provided but no `templateStorage` is configured.",
)
return
}

let cancelled = false
const store = useEditorStore.getState()

resolvedProvider
.getTemplate(initialTemplateId)
.then((record) => {
if (cancelled) return
if (!record) {
useEditorStore
.getState()
.setSyncStatus("error", "Template not found.")
return
}
applyTemplateRecord(record)
})
.catch((err) => {
if (cancelled) return
const handled = applyTemplateStorageAccessFailure(err, {
denyTemplateStorageAccessAndReset:
store.denyTemplateStorageAccessAndReset,
})
if (handled) return
useEditorStore
.getState()
.setSyncStatus(
"error",
err instanceof Error ? err.message : "Failed to load template",
)
})

return () => {
cancelled = true
}
}, [resolvedProvider, initialTemplateId])

useImperativeHandle(
ref,
Expand Down Expand Up @@ -268,7 +379,9 @@ function ImageKitEditorImpl<M extends RequiredMetadata>(
onAddImage={props.onAddImage}
onClose={handleOnClose}
exportOptions={props.exportOptions}
pauseLocalSessionPersistence={Boolean(resumeSession)}
pauseLocalSessionPersistence={
Boolean(resumeSession) || mode === "canvas"
}
/>
{resumeSession ? (
<ResumeSessionModal
Expand All @@ -285,9 +398,6 @@ function ImageKitEditorImpl<M extends RequiredMetadata>(
useEditorStore.getState().resetToNewTemplate()
setResumeSession(null)
}}
onCloseEditor={() => {
handleOnClose()
}}
/>
) : null}
</EditorWrapper>
Expand Down
Loading
Loading