@the-portland-company/canvas-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/CHANGELOG.md +24 -0
- package/README.md +107 -0
- package/dist/adapters-C5K7Ttha.d.ts +131 -0
- package/dist/adapters-DvBAf0cj.d.cts +131 -0
- package/dist/canvas.css +67 -0
- package/dist/chunk-ECSVJSOE.js +248 -0
- package/dist/chunk-ECSVJSOE.js.map +1 -0
- package/dist/chunk-NGI7AKL3.js +263 -0
- package/dist/chunk-NGI7AKL3.js.map +1 -0
- package/dist/chunk-RQHKLW3X.js +211 -0
- package/dist/chunk-RQHKLW3X.js.map +1 -0
- package/dist/chunk-WOJJTIVK.js +18 -0
- package/dist/chunk-WOJJTIVK.js.map +1 -0
- package/dist/editor.cjs +1972 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.d.cts +60 -0
- package/dist/editor.d.ts +60 -0
- package/dist/editor.js +1585 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.cjs +744 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +171 -0
- package/dist/index.d.ts +171 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/renderer.cjs +426 -0
- package/dist/renderer.cjs.map +1 -0
- package/dist/renderer.d.cts +22 -0
- package/dist/renderer.d.ts +22 -0
- package/dist/renderer.js +4 -0
- package/dist/renderer.js.map +1 -0
- package/dist/skeletons.cjs +21 -0
- package/dist/skeletons.cjs.map +1 -0
- package/dist/skeletons.d.cts +1747 -0
- package/dist/skeletons.d.ts +1747 -0
- package/dist/skeletons.js +3 -0
- package/dist/skeletons.js.map +1 -0
- package/dist/types-DJm_CBQt.d.cts +38 -0
- package/dist/types-DJm_CBQt.d.ts +38 -0
- package/package.json +85 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
Initial extraction of the shared VRM Canvas Editor substrate from the V1 email
|
|
6
|
+
canvas (`politogy-email`), per Ben Edtl's canvas-editor handoff (2026-09-10).
|
|
7
|
+
|
|
8
|
+
- **Document model** (`CanvasDocument`, `Block`, `BlockType`, `EmailSettings`)
|
|
9
|
+
and block registry (`BLOCK_LIBRARY`, `BlockDefinition`) — extracted verbatim.
|
|
10
|
+
- **Renderer** (`renderEmailHtml` / `renderEmailText` / `pinBlockSchemaVersion`)
|
|
11
|
+
on the server-safe root and `/renderer` subpaths.
|
|
12
|
+
- **Editor surface** (`<CanvasEditor>`) on the `"use client"` `/editor` subpath:
|
|
13
|
+
palette, canvas, selection, inline text toolbar, hover-`+` insert, per-block
|
|
14
|
+
properties with padding + mobile overrides, Desktop/Mobile/Dark preview,
|
|
15
|
+
media library, template picker, autosave with conflict detection, footer
|
|
16
|
+
(CAN-SPAM) block.
|
|
17
|
+
- **Adapter boundary** (`CanvasHostAdapter`): persistence, preview, send-test,
|
|
18
|
+
templates, media, personalization tokens, brand — injected by the host. The
|
|
19
|
+
package imports no `next/*`, `@supabase/*`, or app code.
|
|
20
|
+
- Host-specific metadata (email subject/preheader + deliverability panel) moved
|
|
21
|
+
out of the package into a host `metaPanel` slot; carried opaquely in the
|
|
22
|
+
document `meta` bag.
|
|
23
|
+
- React 18 || 19 peer; `@dnd-kit` and `vrm-builder` (skeletons) as peers.
|
|
24
|
+
- 66 CORE parity unit tests ported from the email app — all passing.
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @the-portland-company/canvas-editor
|
|
2
|
+
|
|
3
|
+
The shared **VRM Canvas Editor** substrate for Politogy VRM apps: one block/layout
|
|
4
|
+
document model, one block registry, one HTML renderer, and one drag-and-drop
|
|
5
|
+
editor surface. Every module that needs canvas editing (Email, and later Forms,
|
|
6
|
+
Surveys, CTAs, landing pages) binds to this package and registers its own blocks
|
|
7
|
+
— nobody rebuilds the editor.
|
|
8
|
+
|
|
9
|
+
Framework-agnostic by construction: the package **never imports `next/*`,
|
|
10
|
+
`@supabase/*`, `@the-portland-company/shell`, or any app code**. All persistence,
|
|
11
|
+
media, templates, brand tokens, and merge-field resolution are injected by the
|
|
12
|
+
host through a single `CanvasHostAdapter`. It runs in a Next.js (React 19) app
|
|
13
|
+
and a Vite (React 18) app alike.
|
|
14
|
+
|
|
15
|
+
> Extracted verbatim from the V1 email canvas (`politogy-email`) so behavior is
|
|
16
|
+
> identical. See the audit in `politogy-email/CANVAS_EDITOR_AUDIT.md`.
|
|
17
|
+
|
|
18
|
+
## Entry points
|
|
19
|
+
|
|
20
|
+
| Import | Contents | Environment |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| `@the-portland-company/canvas-editor` | Document model, block registry, tree ops, HTML renderer, adapter **types** | **Server-safe** — pure logic, no React, no `@dnd-kit`. Import from Server Components / server actions / API routes. |
|
|
23
|
+
| `@the-portland-company/canvas-editor/editor` | `<CanvasEditor>` + hooks | **`"use client"`** — pulls `@dnd-kit`. Client only. |
|
|
24
|
+
| `@the-portland-company/canvas-editor/renderer` | `renderEmailHtml` / `renderEmailText` / `pinBlockSchemaVersion` | Server-safe. |
|
|
25
|
+
| `@the-portland-company/canvas-editor/skeletons` | `Skeleton` / `SkeletonBar` (self-contained, token-driven) | Server-renderable. |
|
|
26
|
+
| `@the-portland-company/canvas-editor/css` | Primitive component classes (`.card`/`.input`/`.btn`/`.skeleton`…) | Import once in the app. |
|
|
27
|
+
|
|
28
|
+
**Never import `/editor` from a server module** — `@dnd-kit` evaluates
|
|
29
|
+
`React.createContext` at module load and will crash in an RSC / server-action
|
|
30
|
+
context. Server code that needs block logic imports the root (`.`) only.
|
|
31
|
+
|
|
32
|
+
## Consuming it
|
|
33
|
+
|
|
34
|
+
1. **Install** from the registry (never `npm link`; use `npm pack` tarballs for
|
|
35
|
+
local dev against an unpublished change):
|
|
36
|
+
```
|
|
37
|
+
npm i @the-portland-company/canvas-editor
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
2. **Tailwind `@source`** — the editor ships as Tailwind utility classes in
|
|
41
|
+
`dist`, and Tailwind v4 does not scan `node_modules`. Add to your
|
|
42
|
+
`app/globals.css` or it renders unstyled (silent — builds green, diagnose
|
|
43
|
+
against the live DOM):
|
|
44
|
+
```css
|
|
45
|
+
@source "../node_modules/@the-portland-company/canvas-editor/dist";
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
3. **CSS + brand tokens** — import the primitive CSS, and make sure your app
|
|
49
|
+
already loads the runtime brand-tokens `<link>` (shell requirement). The
|
|
50
|
+
package paints only through `var(--token, fallback)`; colors and fonts come
|
|
51
|
+
from the app's runtime tokens, and dark mode flips automatically under your
|
|
52
|
+
app's `.dark` ancestor. The package defines **no** tokens.
|
|
53
|
+
```ts
|
|
54
|
+
import "@the-portland-company/canvas-editor/css";
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
4. **Implement the adapter** and render the editor:
|
|
58
|
+
```tsx
|
|
59
|
+
"use client";
|
|
60
|
+
import { CanvasEditor } from "@the-portland-company/canvas-editor/editor";
|
|
61
|
+
import type { CanvasHostAdapter } from "@the-portland-company/canvas-editor";
|
|
62
|
+
|
|
63
|
+
const adapter: CanvasHostAdapter = {
|
|
64
|
+
save: (id, doc, expectedUpdatedAt) => myServerActions.save(id, doc, expectedUpdatedAt),
|
|
65
|
+
renderPreview: (id, doc, opts) => myServerActions.renderPreview(id, doc, opts),
|
|
66
|
+
sendTest: (id, addrs) => myServerActions.sendTest(id, addrs),
|
|
67
|
+
templates: { list, loadInto, saveAs, resetToBlank },
|
|
68
|
+
media: { list, upload, updateAltText, delete: del, usageCount, trackUsage },
|
|
69
|
+
personalizationTokens: () => myServerActions.tokens(),
|
|
70
|
+
brand: () => myServerActions.brand(),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
<CanvasEditor
|
|
74
|
+
adapter={adapter}
|
|
75
|
+
documentId={id}
|
|
76
|
+
initialDocument={{ name, blocks, settings, meta: { subject, preheader } }}
|
|
77
|
+
initialUpdatedAt={updatedAt}
|
|
78
|
+
nextAction={{ label: "Next: Sender →", href: senderHref }}
|
|
79
|
+
metaPanel={({ doc, setMeta }) => <MyEmailMetaPanel doc={doc} setMeta={setMeta} />}
|
|
80
|
+
/>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## The boundary
|
|
84
|
+
|
|
85
|
+
- **The package owns:** the document model (`CanvasDocument` = `name` + `blocks`
|
|
86
|
+
+ `settings` + opaque `meta`), the block registry (core blocks pre-registered),
|
|
87
|
+
the editor UI (palette, canvas, selection, inline toolbar, insert model,
|
|
88
|
+
properties panel, mobile overrides, preview modes, autosave), and the renderer.
|
|
89
|
+
- **The host supplies** (via `CanvasHostAdapter`): persistence, server-rendered
|
|
90
|
+
preview, send-test, templates, media library, personalization tokens, and
|
|
91
|
+
brand info. Host-specific fields (e.g. an email's subject/preheader and its
|
|
92
|
+
deliverability panel) live in the host and mount into the editor through the
|
|
93
|
+
`metaPanel` slot — they are carried in the document's opaque `meta` bag, which
|
|
94
|
+
the package never interprets.
|
|
95
|
+
- **Merge-field / UVP resolution stays in the host adapter** — provenance and
|
|
96
|
+
exposure are enforced at the host's data layer, never in this package.
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
npm run type-check # tsc --noEmit
|
|
102
|
+
npm test # vitest (CORE parity tests ported from the email app)
|
|
103
|
+
npm run build # tsup dual-build + re-assert "use client" on the editor entry
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Publishing is a tag-driven CI lane (`v*` tag → npm). `prepublishOnly` runs
|
|
107
|
+
type-check + `npm audit --audit-level=high` + tests + build.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { C as CanvasDocument, B as Block } from './types-DJm_CBQt.js';
|
|
2
|
+
|
|
3
|
+
/** A media asset the editor can place. Mirrors the host's media row, trimmed to
|
|
4
|
+
* what the editor needs. */
|
|
5
|
+
type MediaAsset = {
|
|
6
|
+
id: string;
|
|
7
|
+
url: string;
|
|
8
|
+
content_type: string;
|
|
9
|
+
filename: string;
|
|
10
|
+
size_bytes: number;
|
|
11
|
+
alt_text: string | null;
|
|
12
|
+
};
|
|
13
|
+
/** A template the host offers in the Load Template picker. Carries `blocks` so
|
|
14
|
+
* the picker can render a live thumbnail, and `created_at` for the date label. */
|
|
15
|
+
type TemplateSummary = {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
category: string | null;
|
|
19
|
+
is_curated: boolean;
|
|
20
|
+
blocks: Block[];
|
|
21
|
+
created_at: string;
|
|
22
|
+
};
|
|
23
|
+
/** Media list + the account's storage quota (host-specific), for the readout. */
|
|
24
|
+
type MediaListResult = {
|
|
25
|
+
assets: MediaAsset[];
|
|
26
|
+
quota: {
|
|
27
|
+
usedBytes: number;
|
|
28
|
+
quotaBytes: number;
|
|
29
|
+
} | null;
|
|
30
|
+
};
|
|
31
|
+
/** A personalization/merge token the host exposes. The host decides which tokens
|
|
32
|
+
* exist and is solely responsible for resolving them against its data at
|
|
33
|
+
* send/preview time (provenance + exposure enforced in the host, never here). */
|
|
34
|
+
type PersonalizationToken = {
|
|
35
|
+
token: string;
|
|
36
|
+
label: string;
|
|
37
|
+
};
|
|
38
|
+
/** A brand swatch surfaced in color pickers. */
|
|
39
|
+
type PaletteSwatch = {
|
|
40
|
+
label: string;
|
|
41
|
+
hex: string;
|
|
42
|
+
};
|
|
43
|
+
/** Brand context the editor reflects (colors + body font). `available:false`
|
|
44
|
+
* lets the UI show an honest "brand not loaded" note instead of fabricating. */
|
|
45
|
+
type BrandInfo = {
|
|
46
|
+
swatches: PaletteSwatch[];
|
|
47
|
+
fontFamily: string | null;
|
|
48
|
+
available: boolean;
|
|
49
|
+
};
|
|
50
|
+
type SaveResult = {
|
|
51
|
+
ok: true;
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
document: CanvasDocument;
|
|
54
|
+
} | {
|
|
55
|
+
ok: false;
|
|
56
|
+
conflict: true;
|
|
57
|
+
current: CanvasDocument;
|
|
58
|
+
updatedAt: string;
|
|
59
|
+
} | {
|
|
60
|
+
ok: false;
|
|
61
|
+
conflict: false;
|
|
62
|
+
error: string;
|
|
63
|
+
};
|
|
64
|
+
type SendTestResult = {
|
|
65
|
+
ok: true;
|
|
66
|
+
} | {
|
|
67
|
+
ok: false;
|
|
68
|
+
unavailable: true;
|
|
69
|
+
error: string;
|
|
70
|
+
} | {
|
|
71
|
+
ok: false;
|
|
72
|
+
unavailable: false;
|
|
73
|
+
error: string;
|
|
74
|
+
};
|
|
75
|
+
type UploadMediaResult = {
|
|
76
|
+
ok: true;
|
|
77
|
+
assetId: string;
|
|
78
|
+
url: string;
|
|
79
|
+
contentType: string;
|
|
80
|
+
filename: string;
|
|
81
|
+
} | {
|
|
82
|
+
ok: false;
|
|
83
|
+
error: string;
|
|
84
|
+
};
|
|
85
|
+
type SimpleResult = {
|
|
86
|
+
ok: true;
|
|
87
|
+
} | {
|
|
88
|
+
ok: false;
|
|
89
|
+
error: string;
|
|
90
|
+
};
|
|
91
|
+
type RenderedPreview = {
|
|
92
|
+
html: string;
|
|
93
|
+
text: string;
|
|
94
|
+
};
|
|
95
|
+
interface CanvasHostAdapter {
|
|
96
|
+
/** Persist the document. The editor autosaves on a debounce and passes the
|
|
97
|
+
* `updated_at` it last saw for conflict-safety (never a hard overwrite). */
|
|
98
|
+
save(documentId: string, doc: CanvasDocument, expectedUpdatedAt: string): Promise<SaveResult>;
|
|
99
|
+
/** Server-rendered preview HTML/text (host injects account context: physical
|
|
100
|
+
* address, unsubscribe URL, brand font). Keeps merge-field/UVP resolution in
|
|
101
|
+
* the host. */
|
|
102
|
+
renderPreview(documentId: string, doc: CanvasDocument, opts?: {
|
|
103
|
+
darkMode?: boolean;
|
|
104
|
+
}): Promise<RenderedPreview>;
|
|
105
|
+
/** Send a test to the given addresses. `unavailable:true` distinguishes a
|
|
106
|
+
* provider/credits outage from a validation error. */
|
|
107
|
+
sendTest(documentId: string, addresses: string[]): Promise<SendTestResult>;
|
|
108
|
+
templates: {
|
|
109
|
+
list(): Promise<TemplateSummary[]>;
|
|
110
|
+
loadInto(documentId: string, templateId: string): Promise<SaveResult>;
|
|
111
|
+
saveAs(documentId: string, name: string): Promise<SimpleResult>;
|
|
112
|
+
resetToBlank(documentId: string): Promise<SaveResult>;
|
|
113
|
+
};
|
|
114
|
+
media: {
|
|
115
|
+
list(search?: string): Promise<MediaListResult>;
|
|
116
|
+
upload(file: File): Promise<UploadMediaResult>;
|
|
117
|
+
updateAltText(assetId: string, altText: string): Promise<SimpleResult>;
|
|
118
|
+
delete(assetId: string): Promise<SimpleResult>;
|
|
119
|
+
usageCount(assetId: string): Promise<number>;
|
|
120
|
+
trackUsage(assetId: string, documentId: string): Promise<void>;
|
|
121
|
+
};
|
|
122
|
+
/** Tokens the host exposes for insertion; host resolves them at render time. */
|
|
123
|
+
personalizationTokens(): Promise<PersonalizationToken[]>;
|
|
124
|
+
/** Brand colors + body font for the active org, consumed live. */
|
|
125
|
+
brand(): Promise<BrandInfo>;
|
|
126
|
+
}
|
|
127
|
+
/** Default personalization tokens (the V1 email set) — a host may return these
|
|
128
|
+
* verbatim from `personalizationTokens()` or supply its own. */
|
|
129
|
+
declare const DEFAULT_PERSONALIZATION_TOKENS: PersonalizationToken[];
|
|
130
|
+
|
|
131
|
+
export { type BrandInfo as B, type CanvasHostAdapter as C, DEFAULT_PERSONALIZATION_TOKENS as D, type MediaAsset as M, type PersonalizationToken as P, type RenderedPreview as R, type SaveResult as S, type TemplateSummary as T, type UploadMediaResult as U, type MediaListResult as a, type PaletteSwatch as b, type SendTestResult as c, type SimpleResult as d };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { C as CanvasDocument, B as Block } from './types-DJm_CBQt.cjs';
|
|
2
|
+
|
|
3
|
+
/** A media asset the editor can place. Mirrors the host's media row, trimmed to
|
|
4
|
+
* what the editor needs. */
|
|
5
|
+
type MediaAsset = {
|
|
6
|
+
id: string;
|
|
7
|
+
url: string;
|
|
8
|
+
content_type: string;
|
|
9
|
+
filename: string;
|
|
10
|
+
size_bytes: number;
|
|
11
|
+
alt_text: string | null;
|
|
12
|
+
};
|
|
13
|
+
/** A template the host offers in the Load Template picker. Carries `blocks` so
|
|
14
|
+
* the picker can render a live thumbnail, and `created_at` for the date label. */
|
|
15
|
+
type TemplateSummary = {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
category: string | null;
|
|
19
|
+
is_curated: boolean;
|
|
20
|
+
blocks: Block[];
|
|
21
|
+
created_at: string;
|
|
22
|
+
};
|
|
23
|
+
/** Media list + the account's storage quota (host-specific), for the readout. */
|
|
24
|
+
type MediaListResult = {
|
|
25
|
+
assets: MediaAsset[];
|
|
26
|
+
quota: {
|
|
27
|
+
usedBytes: number;
|
|
28
|
+
quotaBytes: number;
|
|
29
|
+
} | null;
|
|
30
|
+
};
|
|
31
|
+
/** A personalization/merge token the host exposes. The host decides which tokens
|
|
32
|
+
* exist and is solely responsible for resolving them against its data at
|
|
33
|
+
* send/preview time (provenance + exposure enforced in the host, never here). */
|
|
34
|
+
type PersonalizationToken = {
|
|
35
|
+
token: string;
|
|
36
|
+
label: string;
|
|
37
|
+
};
|
|
38
|
+
/** A brand swatch surfaced in color pickers. */
|
|
39
|
+
type PaletteSwatch = {
|
|
40
|
+
label: string;
|
|
41
|
+
hex: string;
|
|
42
|
+
};
|
|
43
|
+
/** Brand context the editor reflects (colors + body font). `available:false`
|
|
44
|
+
* lets the UI show an honest "brand not loaded" note instead of fabricating. */
|
|
45
|
+
type BrandInfo = {
|
|
46
|
+
swatches: PaletteSwatch[];
|
|
47
|
+
fontFamily: string | null;
|
|
48
|
+
available: boolean;
|
|
49
|
+
};
|
|
50
|
+
type SaveResult = {
|
|
51
|
+
ok: true;
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
document: CanvasDocument;
|
|
54
|
+
} | {
|
|
55
|
+
ok: false;
|
|
56
|
+
conflict: true;
|
|
57
|
+
current: CanvasDocument;
|
|
58
|
+
updatedAt: string;
|
|
59
|
+
} | {
|
|
60
|
+
ok: false;
|
|
61
|
+
conflict: false;
|
|
62
|
+
error: string;
|
|
63
|
+
};
|
|
64
|
+
type SendTestResult = {
|
|
65
|
+
ok: true;
|
|
66
|
+
} | {
|
|
67
|
+
ok: false;
|
|
68
|
+
unavailable: true;
|
|
69
|
+
error: string;
|
|
70
|
+
} | {
|
|
71
|
+
ok: false;
|
|
72
|
+
unavailable: false;
|
|
73
|
+
error: string;
|
|
74
|
+
};
|
|
75
|
+
type UploadMediaResult = {
|
|
76
|
+
ok: true;
|
|
77
|
+
assetId: string;
|
|
78
|
+
url: string;
|
|
79
|
+
contentType: string;
|
|
80
|
+
filename: string;
|
|
81
|
+
} | {
|
|
82
|
+
ok: false;
|
|
83
|
+
error: string;
|
|
84
|
+
};
|
|
85
|
+
type SimpleResult = {
|
|
86
|
+
ok: true;
|
|
87
|
+
} | {
|
|
88
|
+
ok: false;
|
|
89
|
+
error: string;
|
|
90
|
+
};
|
|
91
|
+
type RenderedPreview = {
|
|
92
|
+
html: string;
|
|
93
|
+
text: string;
|
|
94
|
+
};
|
|
95
|
+
interface CanvasHostAdapter {
|
|
96
|
+
/** Persist the document. The editor autosaves on a debounce and passes the
|
|
97
|
+
* `updated_at` it last saw for conflict-safety (never a hard overwrite). */
|
|
98
|
+
save(documentId: string, doc: CanvasDocument, expectedUpdatedAt: string): Promise<SaveResult>;
|
|
99
|
+
/** Server-rendered preview HTML/text (host injects account context: physical
|
|
100
|
+
* address, unsubscribe URL, brand font). Keeps merge-field/UVP resolution in
|
|
101
|
+
* the host. */
|
|
102
|
+
renderPreview(documentId: string, doc: CanvasDocument, opts?: {
|
|
103
|
+
darkMode?: boolean;
|
|
104
|
+
}): Promise<RenderedPreview>;
|
|
105
|
+
/** Send a test to the given addresses. `unavailable:true` distinguishes a
|
|
106
|
+
* provider/credits outage from a validation error. */
|
|
107
|
+
sendTest(documentId: string, addresses: string[]): Promise<SendTestResult>;
|
|
108
|
+
templates: {
|
|
109
|
+
list(): Promise<TemplateSummary[]>;
|
|
110
|
+
loadInto(documentId: string, templateId: string): Promise<SaveResult>;
|
|
111
|
+
saveAs(documentId: string, name: string): Promise<SimpleResult>;
|
|
112
|
+
resetToBlank(documentId: string): Promise<SaveResult>;
|
|
113
|
+
};
|
|
114
|
+
media: {
|
|
115
|
+
list(search?: string): Promise<MediaListResult>;
|
|
116
|
+
upload(file: File): Promise<UploadMediaResult>;
|
|
117
|
+
updateAltText(assetId: string, altText: string): Promise<SimpleResult>;
|
|
118
|
+
delete(assetId: string): Promise<SimpleResult>;
|
|
119
|
+
usageCount(assetId: string): Promise<number>;
|
|
120
|
+
trackUsage(assetId: string, documentId: string): Promise<void>;
|
|
121
|
+
};
|
|
122
|
+
/** Tokens the host exposes for insertion; host resolves them at render time. */
|
|
123
|
+
personalizationTokens(): Promise<PersonalizationToken[]>;
|
|
124
|
+
/** Brand colors + body font for the active org, consumed live. */
|
|
125
|
+
brand(): Promise<BrandInfo>;
|
|
126
|
+
}
|
|
127
|
+
/** Default personalization tokens (the V1 email set) — a host may return these
|
|
128
|
+
* verbatim from `personalizationTokens()` or supply its own. */
|
|
129
|
+
declare const DEFAULT_PERSONALIZATION_TOKENS: PersonalizationToken[];
|
|
130
|
+
|
|
131
|
+
export { type BrandInfo as B, type CanvasHostAdapter as C, DEFAULT_PERSONALIZATION_TOKENS as D, type MediaAsset as M, type PersonalizationToken as P, type RenderedPreview as R, type SaveResult as S, type TemplateSummary as T, type UploadMediaResult as U, type MediaListResult as a, type PaletteSwatch as b, type SendTestResult as c, type SimpleResult as d };
|
package/dist/canvas.css
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* Canvas Editor component classes — token-driven primitives extracted from V1 email app.
|
|
2
|
+
These classes are the base design-system components used by canvas-editor components.
|
|
3
|
+
All colors/fonts reference CSS custom properties (--color-*, --font-family) provided
|
|
4
|
+
at runtime by the consuming app's brand tokens. Dark mode is inherited from the app's
|
|
5
|
+
.dark ancestor class (not a media query), so dark theming "just works" when the app's
|
|
6
|
+
token values flip. */
|
|
7
|
+
|
|
8
|
+
@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
|
|
9
|
+
.skeleton {
|
|
10
|
+
display: inline-block;
|
|
11
|
+
border-radius: 4px;
|
|
12
|
+
background: linear-gradient(90deg, var(--color-surface-2) 25%, var(--color-border) 37%, var(--color-surface-2) 63%);
|
|
13
|
+
background-size: 200% 100%;
|
|
14
|
+
animation: shimmer 1.4s ease-in-out infinite;
|
|
15
|
+
color: transparent !important;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.card {
|
|
19
|
+
background: var(--color-surface);
|
|
20
|
+
border: 1px solid var(--color-border);
|
|
21
|
+
border-radius: var(--radius);
|
|
22
|
+
box-shadow: var(--shadow-sm);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.btn {
|
|
26
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 0.375rem;
|
|
27
|
+
height: 2.25rem; padding: 0 0.85rem; border-radius: 0.375rem;
|
|
28
|
+
font-size: 0.85rem; font-weight: 500; cursor: pointer;
|
|
29
|
+
border: 1px solid var(--color-border); background: var(--color-surface); color: var(--color-text);
|
|
30
|
+
transition: background 0.12s, border-color 0.12s;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.btn:hover { background: var(--color-surface-2); }
|
|
34
|
+
|
|
35
|
+
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
36
|
+
|
|
37
|
+
.btn-primary { background: var(--color-accent); border-color: var(--color-accent); color: #fff; }
|
|
38
|
+
|
|
39
|
+
.btn-primary:hover { background: var(--color-accent-hover); }
|
|
40
|
+
|
|
41
|
+
.btn-danger { background: color-mix(in srgb, var(--color-red) 90%, black); border-color: var(--color-red); color: #fff; }
|
|
42
|
+
|
|
43
|
+
.btn-sm { height: 1.85rem; padding: 0 0.6rem; font-size: 0.78rem; }
|
|
44
|
+
|
|
45
|
+
.input, .textarea, .select {
|
|
46
|
+
width: 100%; background: var(--color-surface); color: var(--color-text);
|
|
47
|
+
border: 1px solid var(--color-border); border-radius: 0.375rem;
|
|
48
|
+
padding: 0.5rem 0.65rem; font-size: 0.875rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.textarea { resize: vertical; min-height: 6rem; line-height: 1.5; }
|
|
52
|
+
|
|
53
|
+
.input:focus, .textarea:focus, .select:focus { outline: 2px solid color-mix(in srgb, var(--color-accent) 40%, transparent); outline-offset: 0; border-color: var(--color-accent); }
|
|
54
|
+
|
|
55
|
+
.label { font-size: 0.78rem; font-weight: 600; color: var(--color-text-muted); margin-bottom: 0.25rem; display: block; }
|
|
56
|
+
|
|
57
|
+
.muted { color: var(--color-text-muted); }
|
|
58
|
+
|
|
59
|
+
.pill { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.72rem; font-weight: 600; padding: 0.12rem 0.5rem; border-radius: 999px; white-space: nowrap; }
|
|
60
|
+
|
|
61
|
+
.chip { display: inline-flex; align-items: center; gap: 0.25rem; font-size: 0.7rem; font-weight: 600; padding: 0.1rem 0.4rem; border-radius: 4px; background: var(--color-surface-2); color: var(--color-text-muted); }
|
|
62
|
+
|
|
63
|
+
.phone-frame {
|
|
64
|
+
width: 100%; max-width: 20rem; margin: 0 auto;
|
|
65
|
+
border: 8px solid var(--color-border); border-radius: 2rem;
|
|
66
|
+
background: var(--color-surface-2); padding: 1rem 0.75rem; min-height: 12rem;
|
|
67
|
+
}
|