Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions demo/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Optional. A projectId from https://console.unlayer.com/ with the AI
# Assistant feature enabled. The demo hides the AI controls when this is
# unset, so the rest of the demo works without an account.
VITE_UNLAYER_PROJECT_ID=
55 changes: 49 additions & 6 deletions demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ const SAMPLE_IMAGES = [

const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;

// The AI Assistant needs a projectId with the feature enabled, so the demo
// only offers it when one is configured. See demo/.env.example.
const PROJECT_ID = Number(import.meta.env.VITE_UNLAYER_PROJECT_ID) || undefined;

// Demonstrates features.imageEditor.tools.<tool>.icon. Raw <svg> markup is
// the form that works today; see #64 for the Font Awesome name form.
const CUSTOM_TEXT_ICON =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7V5h16v2M12 5v14M9 19h6" /></svg>';

const allToolsEnabled = () =>
Object.fromEntries(TOOL_NAMES.map((tool) => [tool, true])) as Record<
ToolName,
Expand All @@ -31,6 +40,8 @@ export default function App() {
const [tools, setTools] =
useState<Record<ToolName, boolean>>(allToolsEnabled);
const [sidebarOpen, setSidebarOpen] = useState(true);
const [dock, setDock] = useState<'left' | 'right'>('right');
const [ai, setAi] = useState(false);
const [saved, setSaved] = useState<ImageEditorSaveResult | null>(null);
const [status, setStatus] = useState('Loading editor…');

Expand Down Expand Up @@ -80,12 +91,14 @@ export default function App() {
setStatus(editor.hasChanges() ? 'Unsaved changes' : 'No unsaved changes');
};

const snapshot = () => {
const snapshot = async () => {
const dataUrl = editorRef.current?.editor?.getImage();
if (dataUrl) {
setSaved({ dataUrl, blob: new Blob() });
setStatus('Snapshot taken via getImage()');
}
if (!dataUrl) return;
// Build the real blob rather than an empty one: this object is shaped
// like an ImageEditorSaveResult, so it should not lie about `blob`.
const blob = await (await fetch(dataUrl)).blob();
setSaved({ dataUrl, blob });
setStatus(`Snapshot taken via getImage() (${blob.size} bytes)`);
};

const toggleTool = (tool: ToolName) => {
Expand Down Expand Up @@ -117,6 +130,19 @@ export default function App() {
onLocaleChange={setLocale}
tools={tools}
onToolToggle={toggleTool}
dock={dock}
onDockChange={(next) => {
setDock(next);
setStatus('Tool rail moved (remount)');
}}
aiAvailable={PROJECT_ID !== undefined}
ai={ai}
onAiChange={(enabled) => {
setAi(enabled);
setStatus(
`AI Assistant ${enabled ? 'enabled' : 'disabled'} (remount)`
);
}}
onChangeImage={nextImage}
onUploadImage={uploadImage}
onCheckChanges={checkChanges}
Expand All @@ -131,7 +157,24 @@ export default function App() {
options={{
theme,
locale,
features: { imageEditor: { tools } },
projectId: PROJECT_ID,
features: {
ai: { enabled: ai, assistant: ai },
imageEditor: {
dock,
tools: {
...tools,
// A tool entry can also be an object, which is how a
// custom icon is supplied. Raw <svg> markup, not a Font
// Awesome name — the name form is silently ignored
// (see #64).
text: {
enabled: tools.text,
icon: CUSTOM_TEXT_ICON,
},
},
},
},
}}
onLoad={() => setStatus('Editor ready')}
onSave={(result) => {
Expand Down
43 changes: 43 additions & 0 deletions demo/src/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import type { UnlayerLocale } from '@unlayer/types';

export const TOOL_NAMES = [
'crop',
// Configured through features.imageEditor.tools like any other tool, but
// it is the rounded-corners control inside Crop rather than its own tab.
'corners',
'resize',
'filter',
'draw',
Expand All @@ -28,6 +31,11 @@ interface SidebarProps {
onThemeChange(theme: 'light' | 'dark'): void;
locale: UnlayerLocale;
onLocaleChange(locale: UnlayerLocale): void;
dock: 'left' | 'right';
onDockChange(dock: 'left' | 'right'): void;
aiAvailable: boolean;
ai: boolean;
onAiChange(enabled: boolean): void;
tools: Record<ToolName, boolean>;
onToolToggle(tool: ToolName): void;
onChangeImage(): void;
Expand Down Expand Up @@ -95,6 +103,41 @@ export default function Sidebar(props: SidebarProps) {
</label>
</section>

<section className="section">
<h2 className="section-label">Layout (remounts editor)</h2>
<label className="field">
Tool rail
<select
value={props.dock}
onChange={(event) =>
props.onDockChange(event.target.value as 'left' | 'right')
}
>
<option value="left">Left</option>
<option value="right">Right</option>
</select>
</label>
</section>

<section className="section">
<h2 className="section-label">AI Assistant</h2>
{props.aiAvailable ? (
<label className="tool-toggle">
<input
type="checkbox"
checked={props.ai}
onChange={(event) => props.onAiChange(event.target.checked)}
/>
Enable AI Assistant
</label>
) : (
<p className="hint">
Set <code>VITE_UNLAYER_PROJECT_ID</code> in <code>demo/.env</code>{' '}
to try the AI Assistant. See <code>demo/.env.example</code>.
</p>
)}
</section>

<section className="section">
<h2 className="section-label">Tools (remounts editor)</h2>
<div className="tool-grid">
Expand Down
14 changes: 14 additions & 0 deletions demo/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,17 @@ body,
margin-top: 8px;
border-radius: 4px;
}

.hint {
margin: 0;
font-size: 12px;
line-height: 1.5;
color: #8f96a3;
}

.hint code {
font-size: 11px;
padding: 1px 4px;
border-radius: 4px;
background: #232732;
}
9 changes: 9 additions & 0 deletions demo/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
/// <reference types="vite/client" />

interface ImportMetaEnv {
/** Optional Unlayer projectId; enables the AI Assistant controls. */
readonly VITE_UNLAYER_PROJECT_ID?: string;
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}