@lit-pigeon/editor 0.2.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.
@@ -0,0 +1,258 @@
1
+ import { LitElement as b, nothing as v, html as d, css as y } from "lit";
2
+ import { property as x, state as u, customElement as k } from "lit/decorators.js";
3
+ import { t as c } from "./templates.js";
4
+ const m = 24;
5
+ class g extends Error {
6
+ constructor(r) {
7
+ super(`Stock request failed: ${r}`), this.status = r, this.name = "StockError";
8
+ }
9
+ }
10
+ function w(e, r) {
11
+ const i = { Authorization: `Client-ID ${e}` }, s = `utm_source=${encodeURIComponent(r)}&utm_medium=referral`, o = (t) => t + (t.includes("?") ? "&" : "?") + s;
12
+ return {
13
+ id: "unsplash",
14
+ label: "Unsplash",
15
+ async search(t, n) {
16
+ const p = `https://api.unsplash.com/search/photos?query=${encodeURIComponent(t)}&page=${n}&per_page=${m}`, f = await fetch(p, { headers: i });
17
+ if (!f.ok) throw new g(f.status);
18
+ const _ = await f.json();
19
+ return { photos: _.results.map((l) => ({
20
+ id: `unsplash:${l.id}`,
21
+ provider: "unsplash",
22
+ thumbUrl: l.urls.small,
23
+ fullUrl: l.urls.regular,
24
+ width: l.width,
25
+ height: l.height,
26
+ alt: l.alt_description ?? l.description ?? "",
27
+ photographerName: l.user.name,
28
+ photographerUrl: o(l.user.links.html),
29
+ providerUrl: o(l.links.html),
30
+ downloadLocation: l.links.download_location
31
+ })), page: n, hasMore: n < _.total_pages };
32
+ },
33
+ async trackDownload(t) {
34
+ if (t.downloadLocation)
35
+ try {
36
+ await fetch(t.downloadLocation, { headers: i });
37
+ } catch {
38
+ }
39
+ }
40
+ };
41
+ }
42
+ function $(e) {
43
+ return {
44
+ id: "pexels",
45
+ label: "Pexels",
46
+ async search(r, i) {
47
+ const s = `https://api.pexels.com/v1/search?query=${encodeURIComponent(r)}&page=${i}&per_page=${m}`, o = await fetch(s, { headers: { Authorization: e } });
48
+ if (!o.ok) throw new g(o.status);
49
+ const t = await o.json();
50
+ return { photos: t.photos.map((p) => ({
51
+ id: `pexels:${p.id}`,
52
+ provider: "pexels",
53
+ thumbUrl: p.src.medium,
54
+ fullUrl: p.src.large,
55
+ width: p.width,
56
+ height: p.height,
57
+ alt: p.alt ?? "",
58
+ photographerName: p.photographer,
59
+ photographerUrl: p.photographer_url,
60
+ providerUrl: p.url
61
+ })), page: i, hasMore: !!t.next_page };
62
+ },
63
+ async trackDownload() {
64
+ }
65
+ };
66
+ }
67
+ function U(e) {
68
+ var s, o;
69
+ if (!e) return [];
70
+ const r = e.appName || "lit-pigeon", i = [];
71
+ return (s = e.unsplash) != null && s.accessKey && i.push(w(e.unsplash.accessKey, r)), (o = e.pexels) != null && o.apiKey && i.push($(e.pexels.apiKey)), i;
72
+ }
73
+ var P = Object.defineProperty, I = Object.getOwnPropertyDescriptor, h = (e, r, i, s) => {
74
+ for (var o = s > 1 ? void 0 : s ? I(r, i) : r, t = e.length - 1, n; t >= 0; t--)
75
+ (n = e[t]) && (o = (s ? n(r, i, o) : n(o)) || o);
76
+ return s && o && P(r, i, o), o;
77
+ };
78
+ const K = 250;
79
+ let a = class extends b {
80
+ constructor() {
81
+ super(...arguments), this.config = {}, this._providers = [], this._providerIndex = 0, this._query = "", this._photos = [], this._page = 1, this._hasMore = !1, this._loading = !1, this._errorKey = "", this._token = 0, this._timer = null;
82
+ }
83
+ willUpdate(e) {
84
+ e.has("config") && (this._providers = U(this.config), this._providerIndex >= this._providers.length && (this._providerIndex = 0));
85
+ }
86
+ get _provider() {
87
+ return this._providers[this._providerIndex];
88
+ }
89
+ render() {
90
+ return d`
91
+ <div class="controls">
92
+ ${this._providers.length > 1 ? d`<div class="provider-switch" role="group">
93
+ ${this._providers.map(
94
+ (e, r) => d`<button
95
+ class="provider-btn ${r === this._providerIndex ? "active" : ""}"
96
+ @click=${() => this._switchProvider(r)}
97
+ >${e.label}</button>`
98
+ )}
99
+ </div>` : v}
100
+ <input
101
+ type="search"
102
+ placeholder=${c("asset.stock.search")}
103
+ aria-label=${c("asset.stock.search")}
104
+ .value=${this._query}
105
+ @input=${this._onInput}
106
+ />
107
+ </div>
108
+ ${this._renderBody()}
109
+ `;
110
+ }
111
+ _renderBody() {
112
+ return this._errorKey ? d`<div class="error">${c(this._errorKey)}</div>` : this._loading && this._photos.length === 0 ? d`<div class="library-spinner">${c("asset.loading")}</div>` : this._query.trim() ? this._photos.length === 0 ? d`<div class="empty-library">${c("asset.stock.empty")}</div>` : d`
113
+ <div class="asset-grid">${this._photos.map((e) => this._renderCard(e))}</div>
114
+ ${this._hasMore ? d`<button class="load-more" @click=${this._loadMore}>${c("asset.stock.load-more")}</button>` : v}
115
+ ` : d`<div class="stock-idle">${c("asset.stock.idle")}</div>`;
116
+ }
117
+ _renderCard(e) {
118
+ var i;
119
+ const r = ((i = this._providers.find((s) => s.id === e.provider)) == null ? void 0 : i.label) ?? e.provider;
120
+ return d`<button class="asset-card" type="button" @click=${() => this._pick(e)}>
121
+ <img class="asset-thumb" src=${e.thumbUrl} alt=${e.alt} loading="lazy" />
122
+ <div class="stock-credit">
123
+ ${c("asset.stock.photo-by")}
124
+ <a href=${e.photographerUrl} target="_blank" rel="noopener"
125
+ @click=${(s) => s.stopPropagation()}>${e.photographerName}</a>
126
+ ${c("asset.stock.on")}
127
+ <a href=${e.providerUrl} target="_blank" rel="noopener"
128
+ @click=${(s) => s.stopPropagation()}>${r}</a>
129
+ </div>
130
+ </button>`;
131
+ }
132
+ _onInput(e) {
133
+ this._query = e.target.value, this._timer && clearTimeout(this._timer), this._timer = setTimeout(() => this._runSearch(1, !1), K);
134
+ }
135
+ async _runSearch(e, r) {
136
+ const i = this._provider, s = this._query.trim();
137
+ if (!i || !s) {
138
+ this._photos = [], this._hasMore = !1, this._errorKey = "";
139
+ return;
140
+ }
141
+ const o = ++this._token;
142
+ this._loading = !0, this._errorKey = "";
143
+ try {
144
+ const t = await i.search(s, e);
145
+ if (o !== this._token) return;
146
+ this._photos = r ? [...this._photos, ...t.photos] : t.photos, this._page = t.page, this._hasMore = t.hasMore;
147
+ } catch (t) {
148
+ if (o !== this._token) return;
149
+ const n = t instanceof g ? t.status : 0;
150
+ this._errorKey = n === 429 || n === 403 ? "asset.stock.rate-limited" : "asset.stock.error", r || (this._photos = []);
151
+ } finally {
152
+ o === this._token && (this._loading = !1);
153
+ }
154
+ }
155
+ _loadMore() {
156
+ this._runSearch(this._page + 1, !0);
157
+ }
158
+ _switchProvider(e) {
159
+ e !== this._providerIndex && (this._providerIndex = e, this._photos = [], this._errorKey = "", this._runSearch(1, !1));
160
+ }
161
+ _pick(e) {
162
+ var r;
163
+ (r = this._provider) == null || r.trackDownload(e), this.dispatchEvent(
164
+ new CustomEvent("stock-select", {
165
+ detail: { url: e.fullUrl },
166
+ bubbles: !0,
167
+ composed: !0
168
+ })
169
+ );
170
+ }
171
+ };
172
+ a.styles = y`
173
+ :host { display: block; }
174
+ .controls { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; }
175
+ input[type='search'] {
176
+ flex: 1; height: 32px; box-sizing: border-box;
177
+ border: 1px solid var(--pigeon-border, #e2e8f0);
178
+ border-radius: var(--pigeon-radius-sm, 4px);
179
+ padding: 0 10px; font-family: var(--pigeon-font); font-size: 13px;
180
+ color: var(--pigeon-text, #1e293b); background: var(--pigeon-bg, #ffffff); outline: none;
181
+ }
182
+ input[type='search']:focus { border-color: var(--pigeon-border-focus, #3b82f6); }
183
+ .provider-switch { display: flex; gap: 4px; }
184
+ .provider-btn {
185
+ height: 32px; padding: 0 10px; cursor: pointer; font-size: 12px; font-weight: 500;
186
+ font-family: var(--pigeon-font); color: var(--pigeon-text-secondary, #64748b);
187
+ background: var(--pigeon-surface, #f1f5f9);
188
+ border: 1px solid var(--pigeon-border, #e2e8f0); border-radius: var(--pigeon-radius-sm, 4px);
189
+ }
190
+ .provider-btn.active {
191
+ background: var(--pigeon-primary, #3b82f6); color: var(--pigeon-primary-foreground, #fff);
192
+ border-color: var(--pigeon-primary, #3b82f6);
193
+ }
194
+ .asset-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
195
+ .asset-card {
196
+ cursor: pointer; border: 1px solid var(--pigeon-border, #e2e8f0);
197
+ border-radius: var(--pigeon-radius-sm, 4px); overflow: hidden; padding: 0;
198
+ background: var(--pigeon-bg, #ffffff); display: flex; flex-direction: column;
199
+ text-align: left; font-family: inherit;
200
+ }
201
+ .asset-card:hover, .asset-card:focus-visible {
202
+ border-color: var(--pigeon-primary, #3b82f6); box-shadow: var(--pigeon-ring-shadow); outline: none;
203
+ }
204
+ .asset-thumb {
205
+ width: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block;
206
+ background: var(--pigeon-surface, #f1f5f9);
207
+ }
208
+ .stock-credit {
209
+ padding: 5px 8px; font-size: 11px; line-height: 1.3;
210
+ color: var(--pigeon-text-secondary, #64748b);
211
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
212
+ }
213
+ .stock-credit a { color: inherit; text-decoration: underline; }
214
+ .stock-idle, .empty-library, .library-spinner {
215
+ padding: 32px 16px; text-align: center; font-size: 13px;
216
+ color: var(--pigeon-text-secondary, #64748b);
217
+ }
218
+ .error { color: var(--pigeon-danger, #ef4444); font-size: 13px; margin: 8px 0; }
219
+ .load-more {
220
+ display: block; margin: 14px auto 0; height: 32px; padding: 0 16px; cursor: pointer;
221
+ font-family: var(--pigeon-font); font-size: 13px; font-weight: 500;
222
+ color: var(--pigeon-text, #1e293b); background: var(--pigeon-surface, #f1f5f9);
223
+ border: 1px solid var(--pigeon-border, #e2e8f0); border-radius: var(--pigeon-radius-sm, 4px);
224
+ }
225
+ `;
226
+ h([
227
+ x({ attribute: !1 })
228
+ ], a.prototype, "config", 2);
229
+ h([
230
+ u()
231
+ ], a.prototype, "_providers", 2);
232
+ h([
233
+ u()
234
+ ], a.prototype, "_providerIndex", 2);
235
+ h([
236
+ u()
237
+ ], a.prototype, "_query", 2);
238
+ h([
239
+ u()
240
+ ], a.prototype, "_photos", 2);
241
+ h([
242
+ u()
243
+ ], a.prototype, "_page", 2);
244
+ h([
245
+ u()
246
+ ], a.prototype, "_hasMore", 2);
247
+ h([
248
+ u()
249
+ ], a.prototype, "_loading", 2);
250
+ h([
251
+ u()
252
+ ], a.prototype, "_errorKey", 2);
253
+ a = h([
254
+ k("pigeon-stock-tab")
255
+ ], a);
256
+ export {
257
+ a as PigeonStockTab
258
+ };
package/dist/templates.js CHANGED
@@ -90,6 +90,16 @@ const m = {
90
90
  "asset.url.separator": "or enter URL",
91
91
  "asset.url.placeholder": "https://example.com/image.jpg",
92
92
  "asset.url.use": "Use URL",
93
+ // asset manager — stock tab
94
+ "asset.tab.stock": "Stock",
95
+ "asset.stock.search": "Search free photos…",
96
+ "asset.stock.idle": "Search Unsplash and Pexels for free photos.",
97
+ "asset.stock.empty": "No photos found. Try another search.",
98
+ "asset.stock.load-more": "Load more",
99
+ "asset.stock.photo-by": "Photo by",
100
+ "asset.stock.on": "on",
101
+ "asset.stock.error": "Couldn’t load photos. Check the API key or try again.",
102
+ "asset.stock.rate-limited": "Rate limit reached. Try again shortly.",
93
103
  // property panels — shared (common) labels
94
104
  "panel.common.backgroundColor": "Background Color",
95
105
  "panel.common.padding": "Padding",
@@ -236,13 +246,13 @@ const m = {
236
246
  "richtext.removeLink": "Remove link",
237
247
  "richtext.urlPlaceholder": "https://…"
238
248
  }, x = /* @__PURE__ */ new Set(["ar", "he", "fa", "ur", "ps", "sd", "dv", "yi"]);
239
- let b = "en", h = { en: m };
249
+ let h = "en", b = { en: m };
240
250
  function L(e, t) {
241
- b = e || "en", h = { en: m, ...t ?? {} };
251
+ h = e || "en", b = { en: m, ...t ?? {} };
242
252
  }
243
253
  function o(e) {
244
254
  var t;
245
- return ((t = h[b]) == null ? void 0 : t[e]) ?? m[e] ?? e;
255
+ return ((t = b[h]) == null ? void 0 : t[e]) ?? m[e] ?? e;
246
256
  }
247
257
  function T(e, t) {
248
258
  if (t) return t;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lit-pigeon/editor",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Lit Web Components UI for the Pigeon email editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,7 +13,9 @@
13
13
  }
14
14
  },
15
15
  "files": [
16
- "dist"
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
17
19
  ],
18
20
  "dependencies": {
19
21
  "@tiptap/core": "^2.10.0",
@@ -26,7 +28,7 @@
26
28
  "@tiptap/pm": "^2.10.0",
27
29
  "@tiptap/starter-kit": "^2.10.0",
28
30
  "lit": "^3.1.0",
29
- "@lit-pigeon/core": "^0.2.0"
31
+ "@lit-pigeon/core": "^0.3.1"
30
32
  },
31
33
  "devDependencies": {
32
34
  "happy-dom": "^20.9.0",
@@ -44,6 +46,21 @@
44
46
  "directory": "packages/editor"
45
47
  },
46
48
  "license": "MIT",
49
+ "homepage": "https://github.com/snxstudio/lit-pigeon/tree/main/packages/editor#readme",
50
+ "bugs": "https://github.com/snxstudio/lit-pigeon/issues",
51
+ "author": "Lit Pigeon Contributors",
52
+ "keywords": [
53
+ "email",
54
+ "email-template",
55
+ "email-editor",
56
+ "mjml",
57
+ "html-email",
58
+ "lit",
59
+ "web-components",
60
+ "drag-and-drop",
61
+ "no-code",
62
+ "wysiwyg"
63
+ ],
47
64
  "scripts": {
48
65
  "build": "vite build",
49
66
  "dev": "vite build --watch",