@bygga.dev/editor 0.1.0 → 0.3.1
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/README.md +176 -0
- package/{TextEditor-D7HNnS83.js → TextEditor-BLz3iwfa.js} +444 -437
- package/bygga-editor.js +4040 -3921
- package/element.d.ts +9 -2
- package/package.json +2 -2
- package/{urls-BUFldK09.js → urls-CVvXdmlR.js} +3912 -3747
- package/{vue.runtime.esm-bundler-f-r9JKaH.js → vue.runtime.esm-bundler-C8OyVIE3.js} +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# @bygga.dev/editor
|
|
2
|
+
|
|
3
|
+
`<bygga-editor>` — a visual builder for content **documents**, shipped as a native
|
|
4
|
+
[custom element](https://developer.mozilla.org/en-US/docs/Web/API/Web_components).
|
|
5
|
+
Email is the first document kind; landing pages are next. Built from Vue 3 and
|
|
6
|
+
bundled with everything it needs, it embeds directly in any web page — no framework,
|
|
7
|
+
no iframe — and renders into a shadow root, so host CSS can't leak in and editor
|
|
8
|
+
styles can't leak out.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install @bygga.dev/editor
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The runtime bundle is self-contained — there are no peer dependencies to install
|
|
17
|
+
(Vue is bundled in). Ships as ES modules with TypeScript declarations.
|
|
18
|
+
|
|
19
|
+
## You need a licence key
|
|
20
|
+
|
|
21
|
+
The editor **activates** before it mounts: on load it asks `https://api.bygga.dev` to attest the
|
|
22
|
+
origin it is embedded on, and it renders only once it holds a signed attestation it can verify.
|
|
23
|
+
Without a licence covering your domain it shows a short "Editor unavailable" panel instead.
|
|
24
|
+
|
|
25
|
+
Your licence key goes in the `license-key` attribute. It is **public** — it lives in your page, it
|
|
26
|
+
identifies which licence to check your domain against, and it proves nothing on its own, because the
|
|
27
|
+
`Origin` header your browser sets is what actually gets attested. The same key authenticates
|
|
28
|
+
compiling from your own backend, where there is no origin to attest — so treat it as public in a
|
|
29
|
+
page and as a credential everywhere else.
|
|
30
|
+
|
|
31
|
+
**`localhost` and `127.0.0.1` are licensed with no key at all**, so you can evaluate the editor with
|
|
32
|
+
nothing to paste. Only deploying needs a licence.
|
|
33
|
+
|
|
34
|
+
A licence covers a registrable domain and every subdomain of it, so one entry for `acme.com` covers
|
|
35
|
+
`app.acme.com` and `staging.acme.com`. If your page sends a restrictive CSP, it needs
|
|
36
|
+
`connect-src https://api.bygga.dev`.
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
Importing the package registers the `<bygga-editor>` element. Assign `config`
|
|
41
|
+
(with a required `uploadImage`) **before** the element connects to the DOM, then give
|
|
42
|
+
it a document:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import '@bygga.dev/editor' // registers <bygga-editor>
|
|
46
|
+
import { createEmptyDocument, parseDocument } from '@bygga.dev/editor'
|
|
47
|
+
|
|
48
|
+
const editor = document.createElement('bygga-editor')
|
|
49
|
+
|
|
50
|
+
// Your licence key. Omit it only on localhost, which is licensed without one.
|
|
51
|
+
editor.setAttribute('license-key', 'bk_live_…')
|
|
52
|
+
|
|
53
|
+
// Required. The editor calls this with an already-compressed image Blob and expects
|
|
54
|
+
// a public, permanent, unauthenticated URL back — it goes straight into the document
|
|
55
|
+
// JSON and, later, into a sent email.
|
|
56
|
+
editor.config = {
|
|
57
|
+
async uploadImage(blob) {
|
|
58
|
+
return await uploadToYourStorage(blob)
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
editor.locale = 'en'
|
|
63
|
+
editor.mergeTags = { firstName: { en: 'First name', sv: 'Förnamn' } }
|
|
64
|
+
editor.document = createEmptyDocument() // or parseDocument(savedJson)
|
|
65
|
+
|
|
66
|
+
editor.addEventListener('change', (event) => {
|
|
67
|
+
persist(event.detail[0]) // a detached JSON snapshot of the document
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
editor.style.height = '100vh' // it is display:block/height:100% — give it a sized container
|
|
71
|
+
document.body.append(editor) // config is set, so it's safe to connect
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Host contract
|
|
75
|
+
|
|
76
|
+
### `locale` attribute
|
|
77
|
+
`sv` (default) or `en`. Reactive, and also sets the shadow tree's `lang` for
|
|
78
|
+
assistive technology. Settable as the attribute or the `.locale` property.
|
|
79
|
+
|
|
80
|
+
### `config` property *(required)*
|
|
81
|
+
Host callbacks. Assign it **before** connecting the element (the editor throws at
|
|
82
|
+
mount if `uploadImage` is missing):
|
|
83
|
+
|
|
84
|
+
- **`uploadImage(blob: Blob): Promise<string>`** *(required)* — store the image and
|
|
85
|
+
resolve with a public URL. The Blob arrives already compressed and scaled, so don't
|
|
86
|
+
re-encode it; it may be called concurrently. The URL is written into the document
|
|
87
|
+
and then into sent email, so it must be absolute, unauthenticated, and permanent (a
|
|
88
|
+
presigned GET that expires will break in the inbox). Rejecting shows the user an
|
|
89
|
+
alert, keeps the block's previous image, and reports through `onImageError`.
|
|
90
|
+
- **`saveDocument(document: Document): Promise<void>`** *(optional)* — push-save hook
|
|
91
|
+
invoked by `save()`; on success the editor marks the document saved. Rejecting
|
|
92
|
+
leaves the document dirty.
|
|
93
|
+
- **`onImageError(error: unknown): void`** *(optional)* — called after the editor has
|
|
94
|
+
already told the user and recovered, for your own logging.
|
|
95
|
+
|
|
96
|
+
### `document` property
|
|
97
|
+
Assign a `Document` to load one; unset or `null` starts a blank document. It's a
|
|
98
|
+
property, not an attribute — objects can't pass through an HTML attribute.
|
|
99
|
+
|
|
100
|
+
### `mergeTags` property
|
|
101
|
+
The personalization tags this host offers, keyed by the id the document stores, with
|
|
102
|
+
per-locale labels (`{ firstName: { en: 'First name', sv: 'Förnamn' } }`). Populates
|
|
103
|
+
the merge-tag picker; your backend must later resolve every id it sees. A missing
|
|
104
|
+
locale label falls back to the default locale, then to the bare id.
|
|
105
|
+
|
|
106
|
+
### `license-key` attribute
|
|
107
|
+
Your licence key (see [You need a licence key](#you-need-a-licence-key)). Reactive: changing it
|
|
108
|
+
re-activates, so a host that fetches its key can set it after the element is connected. Omit it on
|
|
109
|
+
`localhost`, which is licensed without one.
|
|
110
|
+
|
|
111
|
+
### Events
|
|
112
|
+
- **`change`** — a `CustomEvent` fired whenever the document changes; `event.detail[0]`
|
|
113
|
+
is a detached JSON snapshot (persist it with `JSON.stringify`).
|
|
114
|
+
- **`dirtychange`** — a `CustomEvent` fired when the unsaved-changes state flips;
|
|
115
|
+
`event.detail[0]` is a boolean. Drives, e.g., a Save button's enabled state.
|
|
116
|
+
- **`licenseerror`** — a `CustomEvent` fired once when the editor refuses to open;
|
|
117
|
+
`event.detail[0]` is `{ reason }`, one of `key`, `domain`, `origin`, `malformed`,
|
|
118
|
+
`unreachable`, `invalid`, `insecure-context`. The panel and a fuller `console.error`
|
|
119
|
+
diagnostic are already handled — this is so you can route it into your own telemetry.
|
|
120
|
+
Events do not bubble, so listen on the element itself.
|
|
121
|
+
|
|
122
|
+
### Methods
|
|
123
|
+
- **`getDocument(): Document`** — the current document.
|
|
124
|
+
- **`isDirty(): boolean`** — the current unsaved-changes state.
|
|
125
|
+
- **`markSaved(): void`** — clear the unsaved-changes state after you persist.
|
|
126
|
+
- **`save(): Promise<void>`** — routes through `config.saveDocument`, then marks
|
|
127
|
+
saved. Overlapping calls share one in-flight save.
|
|
128
|
+
|
|
129
|
+
`getDocument()` throws and `save()` rejects while the editor is unlicensed, because there is no
|
|
130
|
+
document behind them that anyone has edited — the editor never mounted. That matters if you autosave
|
|
131
|
+
on a timer: without it, a licence outage would write a blank document over the stored one.
|
|
132
|
+
|
|
133
|
+
Two ways to save: **pull** (`getDocument()`, persist it yourself, then `markSaved()`)
|
|
134
|
+
or **push** (`save()`). `change` is a *notification*, not a request — a host that
|
|
135
|
+
echoes every `change` payload back into the `document` property is declaring each
|
|
136
|
+
keystroke saved and will never see the editor dirty.
|
|
137
|
+
|
|
138
|
+
### The `Document`
|
|
139
|
+
Opaque by design: persist it (it's plain JSON — `JSON.stringify` it) and hand it
|
|
140
|
+
back, but don't reach into it, since the shape is the editor's to evolve. Two ways to
|
|
141
|
+
get one:
|
|
142
|
+
|
|
143
|
+
- **`createEmptyDocument()`** — a fresh blank document.
|
|
144
|
+
- **`parseDocument(json)`** — the supported path from stored JSON back to a
|
|
145
|
+
`Document`. Throws if the editor can't open it, so a corrupt row fails at your load
|
|
146
|
+
boundary rather than deep inside the editor. Use what it returns, not what you
|
|
147
|
+
passed in.
|
|
148
|
+
|
|
149
|
+
### Theming
|
|
150
|
+
Design tokens are CSS custom properties (`--bygga-*`) read through the shadow boundary,
|
|
151
|
+
so a host retheme is just setting them on the element:
|
|
152
|
+
|
|
153
|
+
```css
|
|
154
|
+
bygga-editor { --bygga-color-accent: #b00; }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## TypeScript
|
|
158
|
+
|
|
159
|
+
The package augments `HTMLElementTagNameMap`, so
|
|
160
|
+
`document.querySelector('bygga-editor')` is typed as `ByggaEditorElement` — its
|
|
161
|
+
properties, methods, and a typed `addEventListener` for `change` / `dirtychange`
|
|
162
|
+
included.
|
|
163
|
+
|
|
164
|
+
- **Types:** `Document`, `EditorConfig`, `MergeTags`, `MergeTagLabels`, `Locale`,
|
|
165
|
+
`ByggaEditorElement`, `ByggaEditorEventMap`.
|
|
166
|
+
- **Values:** `createEmptyDocument`, `parseDocument`, `SUPPORTED_LOCALES`,
|
|
167
|
+
`DEFAULT_LOCALE`, `EDITOR_TAG`, and `ByggaEditor` (the element constructor,
|
|
168
|
+
already registered on import).
|
|
169
|
+
|
|
170
|
+
## Rendering to email
|
|
171
|
+
|
|
172
|
+
The editor only edits and previews. Turning a saved document into sendable,
|
|
173
|
+
email-client-safe HTML is done for you: the editor compiles while saving and hands it to
|
|
174
|
+
`config.saveDocument(document, compiled)` as one standalone HTML string with `{{tag}}`
|
|
175
|
+
placeholders. Substitute them with a find-and-replace at send time — escaping each value
|
|
176
|
+
yourself, since they are spliced in raw.
|