@lit-pigeon/editor 0.1.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.
@@ -0,0 +1,274 @@
1
+ import { LitElement as m, html as o, css as f } from "lit";
2
+ import { property as c, state as s, customElement as g } from "lit/decorators.js";
3
+ var h = Object.defineProperty, b = Object.getOwnPropertyDescriptor, r = (e, i, p, n) => {
4
+ for (var a = n > 1 ? void 0 : n ? b(i, p) : i, l = e.length - 1, d; l >= 0; l--)
5
+ (d = e[l]) && (a = (n ? d(i, p, a) : d(a)) || a);
6
+ return n && a && h(i, p, a), a;
7
+ };
8
+ const v = [
9
+ { v: "welcome", l: "Welcome" },
10
+ { v: "newsletter", l: "Newsletter" },
11
+ { v: "transactional", l: "Transactional" },
12
+ { v: "promo", l: "Promo" },
13
+ { v: "announcement", l: "Announcement" },
14
+ { v: "other", l: "Other" }
15
+ ];
16
+ let t = class extends m {
17
+ constructor() {
18
+ super(...arguments), this.open = !1, this._templates = [], this._name = "", this._description = "", this._category = "other", this._formError = "", this._close = () => {
19
+ this.open = !1, this._formError = "";
20
+ }, this._onSubmit = (e) => {
21
+ e.preventDefault();
22
+ const i = this._name.trim();
23
+ if (!i) {
24
+ this._formError = "Name is required";
25
+ return;
26
+ }
27
+ this._formError = "", this.dispatchEvent(
28
+ new CustomEvent("template-save", {
29
+ detail: {
30
+ name: i,
31
+ description: this._description.trim() || void 0,
32
+ category: this._category
33
+ },
34
+ bubbles: !0,
35
+ composed: !0
36
+ })
37
+ ), this._name = "", this._description = "", this._category = "other";
38
+ };
39
+ }
40
+ updated(e) {
41
+ (e.has("storage") || e.has("open") && this.open) && this.refresh();
42
+ }
43
+ /** Re-fetch templates from storage and re-render. */
44
+ async refresh() {
45
+ if (!this.storage) {
46
+ this._templates = [];
47
+ return;
48
+ }
49
+ try {
50
+ this._templates = await this.storage.list();
51
+ } catch {
52
+ this._templates = [];
53
+ }
54
+ }
55
+ render() {
56
+ return o`
57
+ <div class="overlay" @click=${this._close}></div>
58
+ <div class="modal">
59
+ <div class="header">
60
+ <h3>Templates</h3>
61
+ <button class="close-btn" title="Close" @click=${this._close}>&times;</button>
62
+ </div>
63
+ <div class="body">
64
+ <div>
65
+ <h4 class="section-title">Choose a template</h4>
66
+ ${this._templates.length === 0 ? o`<div class="empty">No templates available yet.</div>` : o`<div class="grid">${this._templates.map((e) => this._renderCard(e))}</div>`}
67
+ </div>
68
+
69
+ <form class="save-form" @submit=${this._onSubmit}>
70
+ <h4 class="section-title">Save current as template</h4>
71
+ <div class="save-row">
72
+ <div class="field">
73
+ <label for="template-name">Name</label>
74
+ <input
75
+ id="template-name"
76
+ name="template-name"
77
+ type="text"
78
+ placeholder="My template"
79
+ .value=${this._name}
80
+ @input=${(e) => this._name = e.target.value}
81
+ />
82
+ </div>
83
+ <div class="field">
84
+ <label for="template-category">Category</label>
85
+ <select
86
+ id="template-category"
87
+ name="template-category"
88
+ .value=${this._category}
89
+ @change=${(e) => this._category = e.target.value}
90
+ >
91
+ ${v.map(
92
+ (e) => o`<option value=${e.v} ?selected=${this._category === e.v}>${e.l}</option>`
93
+ )}
94
+ </select>
95
+ </div>
96
+ </div>
97
+ <div class="field">
98
+ <label for="template-description">Description (optional)</label>
99
+ <textarea
100
+ id="template-description"
101
+ name="template-description"
102
+ placeholder="What is this template for?"
103
+ .value=${this._description}
104
+ @input=${(e) => this._description = e.target.value}
105
+ ></textarea>
106
+ </div>
107
+ ${this._formError ? o`<div class="error">${this._formError}</div>` : ""}
108
+ <div class="form-actions">
109
+ <button type="button" class="secondary" @click=${this._close}>Cancel</button>
110
+ <button type="submit">Save template</button>
111
+ </div>
112
+ </form>
113
+ </div>
114
+ </div>
115
+ `;
116
+ }
117
+ _renderCard(e) {
118
+ return o`
119
+ <button class="template-card" @click=${() => this._onPick(e)} title="Load ${e.name}">
120
+ ${e.thumbnail ? o`<img class="thumb" src=${e.thumbnail} alt="" />` : o`<div class="thumb-placeholder">${e.category ?? "template"}</div>`}
121
+ <span class="name">${e.name}</span>
122
+ ${e.description ? o`<span class="description">${e.description}</span>` : ""}
123
+ </button>
124
+ `;
125
+ }
126
+ _onPick(e) {
127
+ this.dispatchEvent(
128
+ new CustomEvent("template-load", {
129
+ detail: { template: e },
130
+ bubbles: !0,
131
+ composed: !0
132
+ })
133
+ );
134
+ }
135
+ };
136
+ t.styles = f`
137
+ :host { display: none; }
138
+ :host([open]) { display: block; position: fixed; inset: 0; z-index: 10000; }
139
+ .overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.5); }
140
+ .modal {
141
+ position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
142
+ width: 720px; max-width: 92vw; max-height: 85vh;
143
+ background: var(--pigeon-bg, #fff);
144
+ border-radius: var(--pigeon-radius, 6px);
145
+ box-shadow: var(--pigeon-shadow-md);
146
+ display: flex; flex-direction: column;
147
+ font-family: var(--pigeon-font);
148
+ color: var(--pigeon-text, #1e293b);
149
+ }
150
+ .header {
151
+ display: flex; align-items: center; justify-content: space-between;
152
+ padding: 16px;
153
+ border-bottom: 1px solid var(--pigeon-border, #e2e8f0);
154
+ }
155
+ .header h3 { margin: 0; font-size: 16px; font-weight: 600; }
156
+ .close-btn {
157
+ background: none; border: none; cursor: pointer;
158
+ color: var(--pigeon-text-secondary, #64748b);
159
+ padding: 4px; font-size: 18px; line-height: 1;
160
+ }
161
+ .body { padding: 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; }
162
+ .section-title {
163
+ margin: 0 0 8px; font-size: 13px; font-weight: 600;
164
+ color: var(--pigeon-text-secondary, #64748b);
165
+ text-transform: uppercase; letter-spacing: 0.04em;
166
+ }
167
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
168
+ .template-card {
169
+ display: flex; flex-direction: column; gap: 6px;
170
+ border: 1px solid var(--pigeon-border, #e2e8f0);
171
+ border-radius: var(--pigeon-radius, 6px);
172
+ background: var(--pigeon-surface, #f8fafc);
173
+ padding: 10px; cursor: pointer; text-align: left;
174
+ font-family: var(--pigeon-font); color: inherit;
175
+ transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
176
+ }
177
+ .template-card:hover {
178
+ border-color: var(--pigeon-primary, #3b82f6);
179
+ background: var(--pigeon-bg, #fff);
180
+ box-shadow: var(--pigeon-shadow-sm);
181
+ }
182
+ .thumb, .thumb-placeholder {
183
+ width: 100%; height: 96px;
184
+ border-radius: var(--pigeon-radius-sm, 4px);
185
+ background: var(--pigeon-bg, #fff);
186
+ border: 1px solid var(--pigeon-border, #e2e8f0);
187
+ display: block;
188
+ }
189
+ .thumb { object-fit: cover; }
190
+ .thumb-placeholder {
191
+ border-style: dashed;
192
+ display: flex; align-items: center; justify-content: center;
193
+ color: var(--pigeon-text-secondary, #64748b);
194
+ font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em;
195
+ }
196
+ .name { font-size: 13px; font-weight: 600; color: var(--pigeon-text, #1e293b); }
197
+ .description { font-size: 12px; color: var(--pigeon-text-secondary, #64748b); line-height: 1.4; }
198
+ .empty {
199
+ padding: 24px; text-align: center;
200
+ color: var(--pigeon-text-secondary, #64748b);
201
+ font-size: 13px;
202
+ border: 1px dashed var(--pigeon-border, #e2e8f0);
203
+ border-radius: var(--pigeon-radius, 6px);
204
+ }
205
+ .save-form {
206
+ display: flex; flex-direction: column; gap: 8px;
207
+ padding-top: 16px;
208
+ border-top: 1px solid var(--pigeon-border, #e2e8f0);
209
+ }
210
+ .field { display: flex; flex-direction: column; gap: 4px; }
211
+ .field label {
212
+ font-size: 12px; font-weight: 500;
213
+ color: var(--pigeon-text-secondary, #64748b);
214
+ }
215
+ .field input, .field textarea, .field select {
216
+ height: 32px;
217
+ border: 1px solid var(--pigeon-border, #e2e8f0);
218
+ border-radius: var(--pigeon-radius-sm, 4px);
219
+ padding: 0 8px;
220
+ font-family: var(--pigeon-font); font-size: 13px;
221
+ color: var(--pigeon-text, #1e293b);
222
+ background: var(--pigeon-bg, #fff);
223
+ outline: none; box-sizing: border-box;
224
+ }
225
+ .field textarea { height: auto; min-height: 56px; padding: 8px; resize: vertical; }
226
+ .field input:focus, .field textarea:focus, .field select:focus {
227
+ border-color: var(--pigeon-border-focus, #3b82f6);
228
+ }
229
+ .save-row { display: grid; grid-template-columns: 1fr 160px; gap: 8px; }
230
+ .form-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
231
+ .form-actions button {
232
+ height: 32px; padding: 0 14px;
233
+ border: 1px solid transparent;
234
+ border-radius: var(--pigeon-radius-sm, 4px);
235
+ font-family: var(--pigeon-font); font-size: 13px; font-weight: 500;
236
+ cursor: pointer;
237
+ background: var(--pigeon-primary, #3b82f6); color: #fff;
238
+ }
239
+ .form-actions button:hover { background: var(--pigeon-primary-hover, #2563eb); }
240
+ .form-actions button.secondary {
241
+ background: transparent;
242
+ color: var(--pigeon-text, #1e293b);
243
+ border-color: var(--pigeon-border, #e2e8f0);
244
+ }
245
+ .form-actions button.secondary:hover { background: var(--pigeon-surface-hover, #f1f5f9); }
246
+ .error { color: var(--pigeon-danger, #ef4444); font-size: 12px; }
247
+ `;
248
+ r([
249
+ c({ attribute: !1 })
250
+ ], t.prototype, "storage", 2);
251
+ r([
252
+ c({ type: Boolean, reflect: !0 })
253
+ ], t.prototype, "open", 2);
254
+ r([
255
+ s()
256
+ ], t.prototype, "_templates", 2);
257
+ r([
258
+ s()
259
+ ], t.prototype, "_name", 2);
260
+ r([
261
+ s()
262
+ ], t.prototype, "_description", 2);
263
+ r([
264
+ s()
265
+ ], t.prototype, "_category", 2);
266
+ r([
267
+ s()
268
+ ], t.prototype, "_formError", 2);
269
+ t = r([
270
+ g("pigeon-template-picker")
271
+ ], t);
272
+ export {
273
+ t as PigeonTemplatePicker
274
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@lit-pigeon/editor",
3
+ "version": "0.1.1",
4
+ "description": "Lit Web Components UI for the Pigeon email editor",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@tiptap/core": "^2.10.0",
20
+ "@tiptap/extension-bubble-menu": "^2.10.0",
21
+ "@tiptap/extension-color": "^2.10.0",
22
+ "@tiptap/extension-font-family": "^2.10.0",
23
+ "@tiptap/extension-link": "^2.10.0",
24
+ "@tiptap/extension-text-style": "^2.10.0",
25
+ "@tiptap/extension-underline": "^2.10.0",
26
+ "@tiptap/pm": "^2.10.0",
27
+ "@tiptap/starter-kit": "^2.10.0",
28
+ "lit": "^3.1.0",
29
+ "@lit-pigeon/core": "^0.1.1"
30
+ },
31
+ "devDependencies": {
32
+ "happy-dom": "^20.9.0",
33
+ "typescript": "^5.5.0",
34
+ "vite": "^5.4.0",
35
+ "vite-plugin-dts": "^4.0.0",
36
+ "vitest": "^2.0.0"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/snxstudio/lit-pigeon.git",
44
+ "directory": "packages/editor"
45
+ },
46
+ "license": "MIT",
47
+ "scripts": {
48
+ "build": "vite build",
49
+ "dev": "vite build --watch",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest",
52
+ "clean": "rm -rf dist"
53
+ }
54
+ }