@lexingtonthemes/astro-image-inspector 0.1.0 → 0.2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAC9C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAExD,YAAY,EAAE,qBAAqB,EAAE,CAAC;AAEtC,MAAM,CAAC,OAAO,UAAU,cAAc,CACpC,OAAO,GAAE,qBAA0B,GAClC,gBAAgB,CAmBlB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAC9C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAExD,YAAY,EAAE,qBAAqB,EAAE,CAAC;AAEtC,MAAM,CAAC,OAAO,UAAU,cAAc,CACpC,OAAO,GAAE,qBAA0B,GAClC,gBAAgB,CAyBlB"}
package/dist/index.js CHANGED
@@ -4,15 +4,20 @@ export default function imageInspector(options = {}) {
4
4
  return {
5
5
  name: "@lexingtonthemes/astro-image-inspector",
6
6
  hooks: {
7
- "astro:config:setup"({ addDevToolbarApp, command }) {
7
+ "astro:config:setup"({ addDevToolbarApp, command, logger }) {
8
8
  if (!enabled || command !== "dev")
9
9
  return;
10
- addDevToolbarApp({
11
- id: "astro-image-inspector",
12
- name: appName,
13
- icon: "image",
14
- entrypoint: new URL("./toolbar-app.js", import.meta.url),
15
- });
10
+ try {
11
+ addDevToolbarApp({
12
+ id: "astro-image-inspector",
13
+ name: appName,
14
+ icon: "image",
15
+ entrypoint: new URL("./toolbar-app.js", import.meta.url),
16
+ });
17
+ }
18
+ catch (err) {
19
+ logger.warn(`[astro-image-inspector] Could not add dev toolbar app: ${err instanceof Error ? err.message : String(err)}. Toolbar will show without Image Inspector.`);
20
+ }
16
21
  },
17
22
  },
18
23
  };
@@ -1 +1 @@
1
- {"version":3,"file":"toolbar-app.d.ts","sourceRoot":"","sources":["../src/toolbar-app.ts"],"names":[],"mappings":";AAiKA,wBAuBG"}
1
+ {"version":3,"file":"toolbar-app.d.ts","sourceRoot":"","sources":["../src/toolbar-app.ts"],"names":[],"mappings":";AAkSA,wBAA2B"}
@@ -1,12 +1,32 @@
1
1
  import { defineToolbarApp } from "astro/toolbar";
2
2
  const OVERLAY_ID = "astro-image-inspector-overlay";
3
3
  const PANEL_ID = "astro-image-inspector-panel";
4
+ function gcd(a, b) {
5
+ a = Math.round(Math.abs(a));
6
+ b = Math.round(Math.abs(b));
7
+ if (b === 0)
8
+ return a;
9
+ return gcd(b, a % b);
10
+ }
11
+ function aspectRatioLabel(w, h) {
12
+ if (h <= 0)
13
+ return "—";
14
+ const d = gcd(w, h);
15
+ if (d === 0)
16
+ return "—";
17
+ return `${w / d}:${h / d}`;
18
+ }
19
+ function formatFromSrc(src) {
20
+ const match = src.match(/\.(jpe?g|png|gif|webp|avif|svg)(\?|$)/i);
21
+ return match ? match[1].toLowerCase() : "—";
22
+ }
4
23
  function getImageInfo(img) {
5
24
  const rect = img.getBoundingClientRect();
6
25
  const nw = img.naturalWidth || 0;
7
26
  const nh = img.naturalHeight || 0;
8
27
  const rw = Math.round(rect.width);
9
28
  const rh = Math.round(rect.height);
29
+ const src = img.currentSrc || img.src || "";
10
30
  const warnings = [];
11
31
  if (nw > 0 && nh > 0) {
12
32
  if (rw > nw || rh > nh)
@@ -17,8 +37,10 @@ function getImageInfo(img) {
17
37
  if (!img.hasAttribute("width") && !img.hasAttribute("height")) {
18
38
  warnings.push("No width/height");
19
39
  }
40
+ const srcset = img.srcset || "";
41
+ const srcsetCount = srcset ? srcset.split(",").length : 0;
20
42
  return {
21
- src: img.currentSrc || img.src || "",
43
+ src,
22
44
  naturalWidth: nw,
23
45
  naturalHeight: nh,
24
46
  renderedWidth: rw,
@@ -26,6 +48,15 @@ function getImageInfo(img) {
26
48
  loading: img.loading || "auto",
27
49
  decoding: img.decoding || "auto",
28
50
  warnings,
51
+ aspectIntrinsic: aspectRatioLabel(nw, nh),
52
+ aspectRendered: aspectRatioLabel(rw, rh),
53
+ format: formatFromSrc(src),
54
+ alt: img.alt !== undefined && img.alt !== "" ? img.alt : "(missing)",
55
+ fetchPriority: img.fetchPriority || "auto",
56
+ sizes: img.sizes?.trim() || "(none)",
57
+ srcsetCount,
58
+ attrWidth: img.getAttribute("width") ?? "—",
59
+ attrHeight: img.getAttribute("height") ?? "—",
29
60
  };
30
61
  }
31
62
  function ensureOverlay() {
@@ -80,41 +111,87 @@ function hidePanel() {
80
111
  if (el)
81
112
  el.style.display = "none";
82
113
  }
83
- function showOverlay(rect) {
114
+ function showOverlay(rect, hasWarnings) {
84
115
  const el = ensureOverlay();
85
116
  el.style.display = "block";
86
117
  el.style.top = `${rect.top + window.scrollY}px`;
87
118
  el.style.left = `${rect.left + window.scrollX}px`;
88
119
  el.style.width = `${rect.width}px`;
89
120
  el.style.height = `${rect.height}px`;
90
- el.style.borderColor = "rgba(139, 92, 246, 0.8)";
121
+ el.style.borderColor = hasWarnings
122
+ ? "rgba(251, 191, 36, 0.9)"
123
+ : "rgba(34, 197, 94, 0.8)";
91
124
  }
92
125
  function showPanel(info) {
93
126
  const el = ensurePanel();
94
127
  el.style.display = "block";
95
- const srcShort = info.src.length > 50 ? info.src.slice(0, 47) + "…" : info.src;
128
+ const srcShort = info.src.length > 52 ? info.src.slice(0, 49) + "…" : info.src;
96
129
  const warnText = info.warnings.length > 0
97
130
  ? `<span style="color: #fbbf24;">${info.warnings.join(", ")}</span>`
98
131
  : "none";
132
+ const altShort = info.alt.length > 40 ? info.alt.slice(0, 37) + "…" : info.alt;
133
+ const sizesShort = info.sizes.length > 36 ? info.sizes.slice(0, 33) + "…" : info.sizes;
99
134
  el.innerHTML = `
100
- <div><strong>src</strong> ${srcShort}</div>
101
- <div><strong>natural</strong> ${info.naturalWidth}×${info.naturalHeight}</div>
102
- <div><strong>rendered</strong> ${info.renderedWidth}×${info.renderedHeight}</div>
103
- <div><strong>loading</strong> ${info.loading}</div>
104
- <div><strong>warnings</strong> ${warnText}</div>
135
+ <div style="display: grid; gap: 6px;">
136
+ <div><strong>src</strong><br/><span style="word-break: break-all;">${srcShort}</span></div>
137
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
138
+ <div><strong>natural</strong><br/>${info.naturalWidth}×${info.naturalHeight}</div>
139
+ <div><strong>rendered</strong><br/>${info.renderedWidth}×${info.renderedHeight}</div>
140
+ </div>
141
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
142
+ <div><strong>aspect (int)</strong><br/>${info.aspectIntrinsic}</div>
143
+ <div><strong>aspect (out)</strong><br/>${info.aspectRendered}</div>
144
+ </div>
145
+ <div><strong>format</strong> ${info.format} · <strong>loading</strong> ${info.loading} · <strong>decoding</strong> ${info.decoding}</div>
146
+ ${info.fetchPriority !== "auto" ? `<div><strong>fetchpriority</strong> ${info.fetchPriority}</div>` : ""}
147
+ <div><strong>alt</strong><br/><span style="color: ${info.alt === "(missing)" ? "#f87171" : "inherit"}">${altShort}</span></div>
148
+ ${info.srcsetCount > 0 ? `<div><strong>srcset</strong> ${info.srcsetCount} source(s)</div>` : ""}
149
+ ${info.sizes !== "(none)" ? `<div><strong>sizes</strong><br/>${sizesShort}</div>` : ""}
150
+ <div><strong>width/height attrs</strong> ${info.attrWidth} × ${info.attrHeight}</div>
151
+ <div><strong>warnings</strong> ${warnText}</div>
152
+ <div style="display: flex; gap: 8px; margin-top: 6px;">
153
+ <button type="button" data-action="copy" style="padding: 4px 8px; font-size: 11px; cursor: pointer; border-radius: 4px; border: 1px solid #64748b; background: #334155; color: #e2e8f0;">Copy src</button>
154
+ <button type="button" data-action="open" style="padding: 4px 8px; font-size: 11px; cursor: pointer; border-radius: 4px; border: 1px solid #64748b; background: #334155; color: #e2e8f0;">Open in tab</button>
155
+ </div>
156
+ </div>
105
157
  `;
158
+ el.querySelectorAll("[data-action]").forEach((btn) => {
159
+ btn.addEventListener("click", (e) => {
160
+ e.preventDefault();
161
+ if (btn.dataset.action === "copy") {
162
+ void navigator.clipboard.writeText(info.src);
163
+ btn.textContent = "Copied!";
164
+ setTimeout(() => (btn.textContent = "Copy src"), 1200);
165
+ }
166
+ else {
167
+ window.open(info.src, "_blank", "noopener");
168
+ }
169
+ });
170
+ });
106
171
  }
172
+ const INSTRUCTION_TEXT = "Hover an image to inspect it.";
107
173
  function enableInspector(windowContent) {
108
- windowContent.textContent = "Hover an image to inspect it.";
174
+ windowContent.textContent = INSTRUCTION_TEXT;
175
+ windowContent.style.display = "";
176
+ const showInstruction = () => {
177
+ windowContent.textContent = INSTRUCTION_TEXT;
178
+ windowContent.style.display = "";
179
+ };
180
+ const hideInstruction = () => {
181
+ windowContent.textContent = "";
182
+ windowContent.style.display = "none";
183
+ };
109
184
  const onMouseOver = (event) => {
110
185
  const target = event.target;
111
186
  if (!(target instanceof HTMLImageElement)) {
112
187
  hideOverlay();
113
188
  hidePanel();
189
+ showInstruction();
114
190
  return;
115
191
  }
192
+ hideInstruction();
116
193
  const info = getImageInfo(target);
117
- showOverlay(target.getBoundingClientRect());
194
+ showOverlay(target.getBoundingClientRect(), info.warnings.length > 0);
118
195
  showPanel(info);
119
196
  };
120
197
  const onMouseOut = (event) => {
@@ -123,6 +200,7 @@ function enableInspector(windowContent) {
123
200
  return;
124
201
  hideOverlay();
125
202
  hidePanel();
203
+ showInstruction();
126
204
  };
127
205
  document.addEventListener("mouseover", onMouseOver, true);
128
206
  document.addEventListener("mouseout", onMouseOut, true);
@@ -133,25 +211,37 @@ function enableInspector(windowContent) {
133
211
  hidePanel();
134
212
  };
135
213
  }
136
- export default defineToolbarApp({
137
- init(canvas, app) {
138
- const windowEl = document.createElement("astro-dev-toolbar-window");
139
- const content = document.createElement("div");
140
- content.style.cssText = "padding: 12px; font-size: 14px;";
141
- content.textContent = "Toggle on and hover an image to inspect it.";
142
- windowEl.appendChild(content);
143
- canvas.appendChild(windowEl);
144
- let cleanup = null;
145
- app.onToggled(({ state }) => {
146
- if (cleanup) {
147
- cleanup();
148
- cleanup = null;
149
- }
150
- if (!state) {
214
+ function createApp() {
215
+ return defineToolbarApp({
216
+ init(canvas, app) {
217
+ try {
218
+ const windowEl = document.createElement("astro-dev-toolbar-window");
219
+ const content = document.createElement("div");
220
+ content.style.cssText = "padding: 12px; font-size: 14px;";
151
221
  content.textContent = "Toggle on and hover an image to inspect it.";
152
- return;
222
+ windowEl.appendChild(content);
223
+ canvas.appendChild(windowEl);
224
+ let cleanup = null;
225
+ app.onToggled(({ state }) => {
226
+ if (cleanup) {
227
+ cleanup();
228
+ cleanup = null;
229
+ }
230
+ if (!state) {
231
+ content.textContent =
232
+ "Toggle on and hover an image to inspect it.";
233
+ return;
234
+ }
235
+ cleanup = enableInspector(content);
236
+ });
153
237
  }
154
- cleanup = enableInspector(content);
155
- });
156
- },
157
- });
238
+ catch (err) {
239
+ const msg = document.createElement("div");
240
+ msg.style.cssText = "padding: 12px; font-size: 12px; color: #f87171;";
241
+ msg.textContent = `Image Inspector failed to load: ${err instanceof Error ? err.message : String(err)}`;
242
+ canvas.appendChild(msg);
243
+ }
244
+ },
245
+ });
246
+ }
247
+ export default createApp();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexingtonthemes/astro-image-inspector",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Astro Dev Toolbar app to inspect images on hover: actual size, render size, and warnings",
5
5
  "type": "module",
6
6
  "exports": {