@bygga.dev/editor 0.1.0 → 0.3.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/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.