@quikturn/logos 0.2.1 → 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 CHANGED
@@ -2,11 +2,20 @@
2
2
 
3
3
  > TypeScript SDK for the Quikturn Logos API -- fetch company logos with type safety.
4
4
 
5
+ ## Packages
6
+
7
+ | Package | Description | Install |
8
+ |---------|-------------|---------|
9
+ | [`@quikturn/logos`](./README.md) | Core SDK -- URL builder, browser client, server client, web component | `pnpm add @quikturn/logos` |
10
+ | [`@quikturn/logos-react`](./packages/react/) | React components -- `<QuikturnLogo>`, `<QuikturnLogoCarousel>`, `<QuikturnLogoGrid>` | `pnpm add @quikturn/logos-react` |
11
+
5
12
  ## Features
6
13
 
7
14
  - **Zero-dependency URL builder** -- universal, works in any JavaScript runtime
8
15
  - **Browser client** -- blob URL management, retry/backoff, scrape polling, event emission
9
16
  - **Server client** -- Buffer output, ReadableStream streaming, concurrent batch operations
17
+ - **`<quikturn-logo>` web component** -- zero-effort attribution element with shadow DOM
18
+ - **React components** -- see [`@quikturn/logos-react`](./packages/react/) for `<QuikturnLogo>`, `<QuikturnLogoCarousel>`, and `<QuikturnLogoGrid>`
10
19
  - **Full TypeScript support** -- strict types, discriminated union error codes, generic response shapes
11
20
  - **Tree-shakeable** -- ESM and CJS dual builds; import only what you need
12
21
 
@@ -103,6 +112,59 @@ const stream = await client.getStream("github.com", { format: "png" });
103
112
  Readable.fromWeb(stream).pipe(createWriteStream("logo.png"));
104
113
  ```
105
114
 
115
+ ### Web Component
116
+
117
+ The `<quikturn-logo>` custom element renders a logo with built-in attribution. It uses shadow DOM to protect the attribution badge and requires no framework.
118
+
119
+ ```html
120
+ <script type="module">
121
+ import "@quikturn/logos/element";
122
+ </script>
123
+
124
+ <quikturn-logo
125
+ domain="github.com"
126
+ token="qt_abc123"
127
+ size="64"
128
+ format="webp"
129
+ theme="dark"
130
+ ></quikturn-logo>
131
+ ```
132
+
133
+ | Attribute | Type | Description |
134
+ |-----------|------|-------------|
135
+ | `domain` | `string` | Domain to fetch logo for (required for rendering) |
136
+ | `token` | `string` | Publishable API key |
137
+ | `size` | `string` | Image width in pixels |
138
+ | `format` | `string` | `"png"`, `"jpeg"`, `"webp"`, or `"avif"` |
139
+ | `greyscale` | presence | When present, applies greyscale transformation |
140
+ | `theme` | `string` | `"light"` or `"dark"` |
141
+
142
+ The element automatically registers as `quikturn-logo` on import and fires an attribution beacon on first render. Attribution styling uses `!important` rules inside the shadow DOM to prevent accidental removal.
143
+
144
+ ### React Components
145
+
146
+ For React applications, install the companion package:
147
+
148
+ ```bash
149
+ pnpm add @quikturn/logos-react
150
+ ```
151
+
152
+ ```tsx
153
+ import { QuikturnProvider, QuikturnLogo, QuikturnLogoCarousel } from "@quikturn/logos-react";
154
+
155
+ <QuikturnProvider token="qt_your_key">
156
+ <QuikturnLogo domain="github.com" size={64} />
157
+ <QuikturnLogoCarousel
158
+ domains={["github.com", "stripe.com", "vercel.com"]}
159
+ speed={120}
160
+ fadeOut
161
+ pauseOnHover
162
+ />
163
+ </QuikturnProvider>
164
+ ```
165
+
166
+ See the full API reference in [`@quikturn/logos-react` README](./packages/react/README.md).
167
+
106
168
  ## API Reference
107
169
 
108
170
  ### Universal (`@quikturn/logos`)
@@ -417,6 +479,18 @@ client.get("github.com", { format: "webp" });
417
479
 
418
480
  Rate limits and monthly quotas are enforced by the API server and vary by plan. The SDK automatically reads rate-limit headers to provide warnings via the event system and retries with backoff when limits are hit. See [Quikturn pricing](https://getquikturn.io/pricing) for details on your plan's limits.
419
481
 
482
+ ## Related Packages
483
+
484
+ ### [`@quikturn/logos-react`](./packages/react/)
485
+
486
+ Ready-made React components for displaying Quikturn logos. Includes an infinite scrolling carousel, responsive grid, single logo image, context provider for token propagation, and a `useLogoUrl()` hook. Zero CSS dependencies -- inline styles only.
487
+
488
+ ```bash
489
+ pnpm add @quikturn/logos-react @quikturn/logos
490
+ ```
491
+
492
+ See the full documentation at [`packages/react/README.md`](./packages/react/README.md).
493
+
420
494
  ## License
421
495
 
422
496
  MIT
@@ -517,6 +517,18 @@ async function handleScrapeResponse(response, originalUrl, fetchFn, options) {
517
517
  }
518
518
  }
519
519
 
520
+ // src/internal/beacon.ts
521
+ var firedTokens = /* @__PURE__ */ new Set();
522
+ function fireBeacon(token) {
523
+ if (typeof window === "undefined") return;
524
+ if (!token) return;
525
+ if (token.startsWith("sk_")) return;
526
+ if (firedTokens.has(token)) return;
527
+ firedTokens.add(token);
528
+ const img = new Image();
529
+ img.src = `${BASE_URL}/_beacon?token=${token}&page=${encodeURIComponent(location.href)}`;
530
+ }
531
+
520
532
  // src/client/index.ts
521
533
  var QuikturnLogos = class {
522
534
  constructor(options) {
@@ -592,6 +604,7 @@ var QuikturnLogos = class {
592
604
  const metadata = parseLogoHeaders(response.headers);
593
605
  const objectUrl = URL.createObjectURL(blob);
594
606
  this.objectUrls.add(objectUrl);
607
+ fireBeacon(this.token);
595
608
  return { url: objectUrl, blob, contentType, metadata };
596
609
  }
597
610
  /**
@@ -515,6 +515,18 @@ async function handleScrapeResponse(response, originalUrl, fetchFn, options) {
515
515
  }
516
516
  }
517
517
 
518
+ // src/internal/beacon.ts
519
+ var firedTokens = /* @__PURE__ */ new Set();
520
+ function fireBeacon(token) {
521
+ if (typeof window === "undefined") return;
522
+ if (!token) return;
523
+ if (token.startsWith("sk_")) return;
524
+ if (firedTokens.has(token)) return;
525
+ firedTokens.add(token);
526
+ const img = new Image();
527
+ img.src = `${BASE_URL}/_beacon?token=${token}&page=${encodeURIComponent(location.href)}`;
528
+ }
529
+
518
530
  // src/client/index.ts
519
531
  var QuikturnLogos = class {
520
532
  constructor(options) {
@@ -590,6 +602,7 @@ var QuikturnLogos = class {
590
602
  const metadata = parseLogoHeaders(response.headers);
591
603
  const objectUrl = URL.createObjectURL(blob);
592
604
  this.objectUrls.add(objectUrl);
605
+ fireBeacon(this.token);
593
606
  return { url: objectUrl, blob, contentType, metadata };
594
607
  }
595
608
  /**
@@ -0,0 +1,288 @@
1
+ 'use strict';
2
+
3
+ // src/constants.ts
4
+ var BASE_URL = "https://logos.getquikturn.io";
5
+ var DEFAULT_WIDTH = 128;
6
+ var MAX_WIDTH = 800;
7
+ var MAX_WIDTH_SERVER = 1200;
8
+ var SUPPORTED_FORMATS = /* @__PURE__ */ new Set([
9
+ "image/png",
10
+ "image/jpeg",
11
+ "image/webp",
12
+ "image/avif"
13
+ ]);
14
+ var FORMAT_ALIASES = {
15
+ png: "image/png",
16
+ jpeg: "image/jpeg",
17
+ webp: "image/webp",
18
+ avif: "image/avif"
19
+ };
20
+
21
+ // src/errors.ts
22
+ var LogoError = class extends Error {
23
+ constructor(message, code, status) {
24
+ super(message);
25
+ this.name = "LogoError";
26
+ this.code = code;
27
+ this.status = status;
28
+ Object.setPrototypeOf(this, new.target.prototype);
29
+ }
30
+ };
31
+ var DomainValidationError = class extends LogoError {
32
+ constructor(message, domain) {
33
+ super(message, "DOMAIN_VALIDATION_ERROR");
34
+ this.name = "DomainValidationError";
35
+ this.domain = domain;
36
+ }
37
+ };
38
+
39
+ // src/url-builder.ts
40
+ var IP_ADDRESS_RE = /^(\d{1,3}\.){3}\d{1,3}$/;
41
+ var LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
42
+ function validateDomain(domain) {
43
+ const normalized = domain.trim().toLowerCase();
44
+ const clean = normalized.endsWith(".") ? normalized.slice(0, -1) : normalized;
45
+ if (clean.length === 0) {
46
+ throw new DomainValidationError("Domain must not be empty", domain);
47
+ }
48
+ if (clean.includes("://")) {
49
+ throw new DomainValidationError(
50
+ 'Domain must not include a protocol scheme (e.g. remove "https://")',
51
+ domain
52
+ );
53
+ }
54
+ if (clean.includes("/")) {
55
+ throw new DomainValidationError(
56
+ "Domain must not include a path \u2014 provide only the hostname",
57
+ domain
58
+ );
59
+ }
60
+ if (IP_ADDRESS_RE.test(clean)) {
61
+ throw new DomainValidationError(
62
+ "IP addresses are not supported \u2014 provide a domain name",
63
+ domain
64
+ );
65
+ }
66
+ if (clean === "localhost") {
67
+ throw new DomainValidationError(
68
+ '"localhost" is not a valid domain',
69
+ domain
70
+ );
71
+ }
72
+ if (clean.length > 253) {
73
+ throw new DomainValidationError(
74
+ `Domain exceeds maximum length of 253 characters (got ${clean.length})`,
75
+ domain
76
+ );
77
+ }
78
+ const labels = clean.split(".");
79
+ if (labels.length < 2) {
80
+ throw new DomainValidationError(
81
+ 'Domain must contain at least two labels (e.g. "example.com")',
82
+ domain
83
+ );
84
+ }
85
+ for (const label of labels) {
86
+ if (label.length === 0) {
87
+ throw new DomainValidationError(
88
+ "Domain contains an empty label (consecutive dots)",
89
+ domain
90
+ );
91
+ }
92
+ if (label.length > 63) {
93
+ throw new DomainValidationError(
94
+ `Label "${label}" exceeds maximum length of 63 characters (got ${label.length})`,
95
+ domain
96
+ );
97
+ }
98
+ if (!LABEL_RE.test(label)) {
99
+ throw new DomainValidationError(
100
+ `Label "${label}" contains invalid characters \u2014 only letters, digits, and hyphens are allowed, and labels must not start or end with a hyphen`,
101
+ domain
102
+ );
103
+ }
104
+ }
105
+ return clean;
106
+ }
107
+ function resolveFormat(format) {
108
+ let candidate = format;
109
+ if (candidate.startsWith("image/")) {
110
+ candidate = candidate.slice(6);
111
+ }
112
+ if (candidate in FORMAT_ALIASES) {
113
+ return candidate;
114
+ }
115
+ if (SUPPORTED_FORMATS.has(format)) {
116
+ const shorthand = format.slice(6);
117
+ if (shorthand in FORMAT_ALIASES) {
118
+ return shorthand;
119
+ }
120
+ }
121
+ return void 0;
122
+ }
123
+ function logoUrl(domain, options) {
124
+ const validDomain = validateDomain(domain);
125
+ const {
126
+ token,
127
+ size,
128
+ width,
129
+ greyscale,
130
+ theme,
131
+ format,
132
+ baseUrl
133
+ } = options ?? {};
134
+ const maxWidth = token?.startsWith("sk_") ? MAX_WIDTH_SERVER : MAX_WIDTH;
135
+ let resolvedSize = size ?? width ?? DEFAULT_WIDTH;
136
+ if (resolvedSize <= 0) {
137
+ resolvedSize = DEFAULT_WIDTH;
138
+ }
139
+ resolvedSize = Math.max(1, Math.min(resolvedSize, maxWidth));
140
+ const resolvedFormat = format ? resolveFormat(format) : void 0;
141
+ const effectiveBaseUrl = baseUrl ?? BASE_URL;
142
+ const url = new URL(`${effectiveBaseUrl}/${validDomain}`);
143
+ if (token !== void 0) {
144
+ url.searchParams.set("token", token);
145
+ }
146
+ if (resolvedSize !== DEFAULT_WIDTH) {
147
+ url.searchParams.set("size", String(resolvedSize));
148
+ }
149
+ if (greyscale === true) {
150
+ url.searchParams.set("greyscale", "1");
151
+ }
152
+ if (theme === "light" || theme === "dark") {
153
+ url.searchParams.set("theme", theme);
154
+ }
155
+ if (resolvedFormat !== void 0) {
156
+ url.searchParams.set("format", resolvedFormat);
157
+ }
158
+ url.searchParams.set("autoScrape", "true");
159
+ return url.toString();
160
+ }
161
+
162
+ // src/internal/beacon.ts
163
+ var firedTokens = /* @__PURE__ */ new Set();
164
+ function fireBeacon(token) {
165
+ if (typeof window === "undefined") return;
166
+ if (!token) return;
167
+ if (token.startsWith("sk_")) return;
168
+ if (firedTokens.has(token)) return;
169
+ firedTokens.add(token);
170
+ const img = new Image();
171
+ img.src = `${BASE_URL}/_beacon?token=${token}&page=${encodeURIComponent(location.href)}`;
172
+ }
173
+
174
+ // src/element/styles.ts
175
+ var STYLES = (
176
+ /* css */
177
+ `
178
+ :host {
179
+ display: inline-block !important;
180
+ line-height: 0 !important;
181
+ }
182
+
183
+ .qt-logo-container {
184
+ display: inline-flex !important;
185
+ flex-direction: column !important;
186
+ align-items: center !important;
187
+ gap: 4px !important;
188
+ }
189
+
190
+ .qt-logo-img {
191
+ display: block !important;
192
+ max-width: 100% !important;
193
+ height: auto !important;
194
+ }
195
+
196
+ .qt-attribution {
197
+ display: block !important;
198
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
199
+ font-size: 10px !important;
200
+ color: #888 !important;
201
+ text-decoration: none !important;
202
+ white-space: nowrap !important;
203
+ }
204
+
205
+ .qt-attribution:hover {
206
+ color: #555 !important;
207
+ text-decoration: underline !important;
208
+ }
209
+ `
210
+ );
211
+
212
+ // src/element/index.ts
213
+ var QuikturnLogo = class extends HTMLElement {
214
+ constructor() {
215
+ super();
216
+ this._beaconFired = false;
217
+ this.attachShadow({ mode: "open" });
218
+ }
219
+ static get observedAttributes() {
220
+ return ["domain", "token", "size", "format", "greyscale", "theme"];
221
+ }
222
+ connectedCallback() {
223
+ this._fireBeaconOnce();
224
+ this._render();
225
+ }
226
+ attributeChangedCallback() {
227
+ if (this.shadowRoot) {
228
+ this._render();
229
+ }
230
+ }
231
+ _fireBeaconOnce() {
232
+ if (this._beaconFired) return;
233
+ const domain = this.getAttribute("domain");
234
+ const token = this.getAttribute("token") ?? "";
235
+ if (!domain) return;
236
+ this._beaconFired = true;
237
+ fireBeacon(token);
238
+ }
239
+ _clearShadow() {
240
+ const shadow = this.shadowRoot;
241
+ if (!shadow) return;
242
+ while (shadow.firstChild) {
243
+ shadow.removeChild(shadow.firstChild);
244
+ }
245
+ }
246
+ _render() {
247
+ const shadow = this.shadowRoot;
248
+ if (!shadow) return;
249
+ const domain = this.getAttribute("domain");
250
+ this._clearShadow();
251
+ const style = document.createElement("style");
252
+ style.textContent = STYLES;
253
+ shadow.appendChild(style);
254
+ if (!domain) return;
255
+ const token = this.getAttribute("token") ?? "";
256
+ const size = this.getAttribute("size");
257
+ const format = this.getAttribute("format");
258
+ const greyscale = this.hasAttribute("greyscale");
259
+ const theme = this.getAttribute("theme");
260
+ const container = document.createElement("div");
261
+ container.className = "qt-logo-container";
262
+ const img = document.createElement("img");
263
+ img.className = "qt-logo-img";
264
+ img.loading = "lazy";
265
+ img.alt = `${domain} logo`;
266
+ img.src = logoUrl(domain, {
267
+ token: token || void 0,
268
+ size: size ? parseInt(size, 10) : void 0,
269
+ format,
270
+ greyscale,
271
+ theme
272
+ });
273
+ container.appendChild(img);
274
+ const link = document.createElement("a");
275
+ link.className = "qt-attribution";
276
+ link.href = `https://getquikturn.io?ref=${domain}`;
277
+ link.target = "_blank";
278
+ link.rel = "noopener noreferrer";
279
+ link.textContent = "Powered by Quikturn";
280
+ container.appendChild(link);
281
+ shadow.appendChild(container);
282
+ }
283
+ };
284
+ if (typeof customElements !== "undefined" && !customElements.get("quikturn-logo")) {
285
+ customElements.define("quikturn-logo", QuikturnLogo);
286
+ }
287
+
288
+ exports.QuikturnLogo = QuikturnLogo;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @quikturn/logos SDK — `<quikturn-logo>` Web Component
3
+ *
4
+ * Zero-effort attribution element for free-tier users. Uses shadow DOM to
5
+ * protect the attribution badge from accidental removal.
6
+ *
7
+ * Usage:
8
+ * ```html
9
+ * <script type="module" src="@quikturn/logos/element"></script>
10
+ * <quikturn-logo domain="github.com" token="qt_abc123" size="64"></quikturn-logo>
11
+ * ```
12
+ */
13
+ /**
14
+ * Custom element that renders a Quikturn logo with attribution.
15
+ *
16
+ * Observed attributes: `domain`, `token`, `size`, `format`, `greyscale`, `theme`.
17
+ */
18
+ declare class QuikturnLogo extends HTMLElement {
19
+ static get observedAttributes(): string[];
20
+ private _beaconFired;
21
+ constructor();
22
+ connectedCallback(): void;
23
+ attributeChangedCallback(): void;
24
+ private _fireBeaconOnce;
25
+ private _clearShadow;
26
+ private _render;
27
+ }
28
+
29
+ export { QuikturnLogo };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @quikturn/logos SDK — `<quikturn-logo>` Web Component
3
+ *
4
+ * Zero-effort attribution element for free-tier users. Uses shadow DOM to
5
+ * protect the attribution badge from accidental removal.
6
+ *
7
+ * Usage:
8
+ * ```html
9
+ * <script type="module" src="@quikturn/logos/element"></script>
10
+ * <quikturn-logo domain="github.com" token="qt_abc123" size="64"></quikturn-logo>
11
+ * ```
12
+ */
13
+ /**
14
+ * Custom element that renders a Quikturn logo with attribution.
15
+ *
16
+ * Observed attributes: `domain`, `token`, `size`, `format`, `greyscale`, `theme`.
17
+ */
18
+ declare class QuikturnLogo extends HTMLElement {
19
+ static get observedAttributes(): string[];
20
+ private _beaconFired;
21
+ constructor();
22
+ connectedCallback(): void;
23
+ attributeChangedCallback(): void;
24
+ private _fireBeaconOnce;
25
+ private _clearShadow;
26
+ private _render;
27
+ }
28
+
29
+ export { QuikturnLogo };
@@ -0,0 +1,286 @@
1
+ // src/constants.ts
2
+ var BASE_URL = "https://logos.getquikturn.io";
3
+ var DEFAULT_WIDTH = 128;
4
+ var MAX_WIDTH = 800;
5
+ var MAX_WIDTH_SERVER = 1200;
6
+ var SUPPORTED_FORMATS = /* @__PURE__ */ new Set([
7
+ "image/png",
8
+ "image/jpeg",
9
+ "image/webp",
10
+ "image/avif"
11
+ ]);
12
+ var FORMAT_ALIASES = {
13
+ png: "image/png",
14
+ jpeg: "image/jpeg",
15
+ webp: "image/webp",
16
+ avif: "image/avif"
17
+ };
18
+
19
+ // src/errors.ts
20
+ var LogoError = class extends Error {
21
+ constructor(message, code, status) {
22
+ super(message);
23
+ this.name = "LogoError";
24
+ this.code = code;
25
+ this.status = status;
26
+ Object.setPrototypeOf(this, new.target.prototype);
27
+ }
28
+ };
29
+ var DomainValidationError = class extends LogoError {
30
+ constructor(message, domain) {
31
+ super(message, "DOMAIN_VALIDATION_ERROR");
32
+ this.name = "DomainValidationError";
33
+ this.domain = domain;
34
+ }
35
+ };
36
+
37
+ // src/url-builder.ts
38
+ var IP_ADDRESS_RE = /^(\d{1,3}\.){3}\d{1,3}$/;
39
+ var LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
40
+ function validateDomain(domain) {
41
+ const normalized = domain.trim().toLowerCase();
42
+ const clean = normalized.endsWith(".") ? normalized.slice(0, -1) : normalized;
43
+ if (clean.length === 0) {
44
+ throw new DomainValidationError("Domain must not be empty", domain);
45
+ }
46
+ if (clean.includes("://")) {
47
+ throw new DomainValidationError(
48
+ 'Domain must not include a protocol scheme (e.g. remove "https://")',
49
+ domain
50
+ );
51
+ }
52
+ if (clean.includes("/")) {
53
+ throw new DomainValidationError(
54
+ "Domain must not include a path \u2014 provide only the hostname",
55
+ domain
56
+ );
57
+ }
58
+ if (IP_ADDRESS_RE.test(clean)) {
59
+ throw new DomainValidationError(
60
+ "IP addresses are not supported \u2014 provide a domain name",
61
+ domain
62
+ );
63
+ }
64
+ if (clean === "localhost") {
65
+ throw new DomainValidationError(
66
+ '"localhost" is not a valid domain',
67
+ domain
68
+ );
69
+ }
70
+ if (clean.length > 253) {
71
+ throw new DomainValidationError(
72
+ `Domain exceeds maximum length of 253 characters (got ${clean.length})`,
73
+ domain
74
+ );
75
+ }
76
+ const labels = clean.split(".");
77
+ if (labels.length < 2) {
78
+ throw new DomainValidationError(
79
+ 'Domain must contain at least two labels (e.g. "example.com")',
80
+ domain
81
+ );
82
+ }
83
+ for (const label of labels) {
84
+ if (label.length === 0) {
85
+ throw new DomainValidationError(
86
+ "Domain contains an empty label (consecutive dots)",
87
+ domain
88
+ );
89
+ }
90
+ if (label.length > 63) {
91
+ throw new DomainValidationError(
92
+ `Label "${label}" exceeds maximum length of 63 characters (got ${label.length})`,
93
+ domain
94
+ );
95
+ }
96
+ if (!LABEL_RE.test(label)) {
97
+ throw new DomainValidationError(
98
+ `Label "${label}" contains invalid characters \u2014 only letters, digits, and hyphens are allowed, and labels must not start or end with a hyphen`,
99
+ domain
100
+ );
101
+ }
102
+ }
103
+ return clean;
104
+ }
105
+ function resolveFormat(format) {
106
+ let candidate = format;
107
+ if (candidate.startsWith("image/")) {
108
+ candidate = candidate.slice(6);
109
+ }
110
+ if (candidate in FORMAT_ALIASES) {
111
+ return candidate;
112
+ }
113
+ if (SUPPORTED_FORMATS.has(format)) {
114
+ const shorthand = format.slice(6);
115
+ if (shorthand in FORMAT_ALIASES) {
116
+ return shorthand;
117
+ }
118
+ }
119
+ return void 0;
120
+ }
121
+ function logoUrl(domain, options) {
122
+ const validDomain = validateDomain(domain);
123
+ const {
124
+ token,
125
+ size,
126
+ width,
127
+ greyscale,
128
+ theme,
129
+ format,
130
+ baseUrl
131
+ } = options ?? {};
132
+ const maxWidth = token?.startsWith("sk_") ? MAX_WIDTH_SERVER : MAX_WIDTH;
133
+ let resolvedSize = size ?? width ?? DEFAULT_WIDTH;
134
+ if (resolvedSize <= 0) {
135
+ resolvedSize = DEFAULT_WIDTH;
136
+ }
137
+ resolvedSize = Math.max(1, Math.min(resolvedSize, maxWidth));
138
+ const resolvedFormat = format ? resolveFormat(format) : void 0;
139
+ const effectiveBaseUrl = baseUrl ?? BASE_URL;
140
+ const url = new URL(`${effectiveBaseUrl}/${validDomain}`);
141
+ if (token !== void 0) {
142
+ url.searchParams.set("token", token);
143
+ }
144
+ if (resolvedSize !== DEFAULT_WIDTH) {
145
+ url.searchParams.set("size", String(resolvedSize));
146
+ }
147
+ if (greyscale === true) {
148
+ url.searchParams.set("greyscale", "1");
149
+ }
150
+ if (theme === "light" || theme === "dark") {
151
+ url.searchParams.set("theme", theme);
152
+ }
153
+ if (resolvedFormat !== void 0) {
154
+ url.searchParams.set("format", resolvedFormat);
155
+ }
156
+ url.searchParams.set("autoScrape", "true");
157
+ return url.toString();
158
+ }
159
+
160
+ // src/internal/beacon.ts
161
+ var firedTokens = /* @__PURE__ */ new Set();
162
+ function fireBeacon(token) {
163
+ if (typeof window === "undefined") return;
164
+ if (!token) return;
165
+ if (token.startsWith("sk_")) return;
166
+ if (firedTokens.has(token)) return;
167
+ firedTokens.add(token);
168
+ const img = new Image();
169
+ img.src = `${BASE_URL}/_beacon?token=${token}&page=${encodeURIComponent(location.href)}`;
170
+ }
171
+
172
+ // src/element/styles.ts
173
+ var STYLES = (
174
+ /* css */
175
+ `
176
+ :host {
177
+ display: inline-block !important;
178
+ line-height: 0 !important;
179
+ }
180
+
181
+ .qt-logo-container {
182
+ display: inline-flex !important;
183
+ flex-direction: column !important;
184
+ align-items: center !important;
185
+ gap: 4px !important;
186
+ }
187
+
188
+ .qt-logo-img {
189
+ display: block !important;
190
+ max-width: 100% !important;
191
+ height: auto !important;
192
+ }
193
+
194
+ .qt-attribution {
195
+ display: block !important;
196
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
197
+ font-size: 10px !important;
198
+ color: #888 !important;
199
+ text-decoration: none !important;
200
+ white-space: nowrap !important;
201
+ }
202
+
203
+ .qt-attribution:hover {
204
+ color: #555 !important;
205
+ text-decoration: underline !important;
206
+ }
207
+ `
208
+ );
209
+
210
+ // src/element/index.ts
211
+ var QuikturnLogo = class extends HTMLElement {
212
+ constructor() {
213
+ super();
214
+ this._beaconFired = false;
215
+ this.attachShadow({ mode: "open" });
216
+ }
217
+ static get observedAttributes() {
218
+ return ["domain", "token", "size", "format", "greyscale", "theme"];
219
+ }
220
+ connectedCallback() {
221
+ this._fireBeaconOnce();
222
+ this._render();
223
+ }
224
+ attributeChangedCallback() {
225
+ if (this.shadowRoot) {
226
+ this._render();
227
+ }
228
+ }
229
+ _fireBeaconOnce() {
230
+ if (this._beaconFired) return;
231
+ const domain = this.getAttribute("domain");
232
+ const token = this.getAttribute("token") ?? "";
233
+ if (!domain) return;
234
+ this._beaconFired = true;
235
+ fireBeacon(token);
236
+ }
237
+ _clearShadow() {
238
+ const shadow = this.shadowRoot;
239
+ if (!shadow) return;
240
+ while (shadow.firstChild) {
241
+ shadow.removeChild(shadow.firstChild);
242
+ }
243
+ }
244
+ _render() {
245
+ const shadow = this.shadowRoot;
246
+ if (!shadow) return;
247
+ const domain = this.getAttribute("domain");
248
+ this._clearShadow();
249
+ const style = document.createElement("style");
250
+ style.textContent = STYLES;
251
+ shadow.appendChild(style);
252
+ if (!domain) return;
253
+ const token = this.getAttribute("token") ?? "";
254
+ const size = this.getAttribute("size");
255
+ const format = this.getAttribute("format");
256
+ const greyscale = this.hasAttribute("greyscale");
257
+ const theme = this.getAttribute("theme");
258
+ const container = document.createElement("div");
259
+ container.className = "qt-logo-container";
260
+ const img = document.createElement("img");
261
+ img.className = "qt-logo-img";
262
+ img.loading = "lazy";
263
+ img.alt = `${domain} logo`;
264
+ img.src = logoUrl(domain, {
265
+ token: token || void 0,
266
+ size: size ? parseInt(size, 10) : void 0,
267
+ format,
268
+ greyscale,
269
+ theme
270
+ });
271
+ container.appendChild(img);
272
+ const link = document.createElement("a");
273
+ link.className = "qt-attribution";
274
+ link.href = `https://getquikturn.io?ref=${domain}`;
275
+ link.target = "_blank";
276
+ link.rel = "noopener noreferrer";
277
+ link.textContent = "Powered by Quikturn";
278
+ container.appendChild(link);
279
+ shadow.appendChild(container);
280
+ }
281
+ };
282
+ if (typeof customElements !== "undefined" && !customElements.get("quikturn-logo")) {
283
+ customElements.define("quikturn-logo", QuikturnLogo);
284
+ }
285
+
286
+ export { QuikturnLogo };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quikturn/logos",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Quikturn Logo SDK — URL builder, browser client, and server client for the Quikturn Logos API",
5
5
  "license": "MIT",
6
6
  "author": "Quikturn",
@@ -20,7 +20,10 @@
20
20
  "sdk"
21
21
  ],
22
22
  "type": "module",
23
- "sideEffects": false,
23
+ "sideEffects": [
24
+ "./dist/element/index.mjs",
25
+ "./dist/element/index.cjs"
26
+ ],
24
27
  "packageManager": "pnpm@10.28.1",
25
28
  "engines": {
26
29
  "node": ">=18"
@@ -55,6 +58,14 @@
55
58
  },
56
59
  "import": "./dist/server/index.mjs",
57
60
  "require": "./dist/server/index.cjs"
61
+ },
62
+ "./element": {
63
+ "types": {
64
+ "import": "./dist/element/index.d.ts",
65
+ "require": "./dist/element/index.d.cts"
66
+ },
67
+ "import": "./dist/element/index.mjs",
68
+ "require": "./dist/element/index.cjs"
58
69
  }
59
70
  },
60
71
  "publishConfig": {
@@ -65,7 +76,7 @@
65
76
  "types": "./dist/index.d.ts",
66
77
  "scripts": {
67
78
  "build": "tsup",
68
- "test": "vitest run --project unit --project client --project server",
79
+ "test": "vitest run --project unit --project client --project server --project element",
69
80
  "test:integration": "vitest run --project integration",
70
81
  "test:watch": "vitest",
71
82
  "test:coverage": "vitest run --coverage",