astro-better-declarative-screenshots 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/Screenshot.astro CHANGED
@@ -38,10 +38,8 @@ const {
38
38
  outputDir,
39
39
  } = Astro.props;
40
40
 
41
- // parse Highlight children from the rendered slot HTML
42
41
  function parseHighlights(html: string): HighlightData[] {
43
42
  const found: HighlightData[] = [];
44
- // match <screenshot-highlight data-selector="..." ...></screenshot-highlight>
45
43
  const tagRe = /<screenshot-highlight([^>]*)>/gi;
46
44
  const attrRe = /data-([\w-]+)="([^"]*)"/g;
47
45
  let tagMatch: RegExpExecArray | null;
@@ -66,29 +64,28 @@ function parseHighlights(html: string): HighlightData[] {
66
64
 
67
65
  const slotHtml = Astro.slots.has('default') ? await Astro.slots.render('default') : '';
68
66
  const highlights = parseHighlights(slotHtml);
69
-
70
67
  const name = id ?? deriveName(url, highlights);
71
68
 
72
- // resolve output directory: prop > env > screenshot.config.mjs > default
73
69
  let resolvedOutputDir = outputDir ?? process.env.SCREENSHOTS_DIR;
70
+ let chrome = { style: 'safari-macos', renderIn: 'png', showUrl: true, theme: 'light',
71
+ shadowBlur: 40, shadowPadding: 48, baseUrl: undefined as string | undefined };
72
+ let configBaseUrl = '';
74
73
  if (!resolvedOutputDir) {
75
74
  try {
76
75
  const cfg = await loadConfig(process.cwd());
77
76
  resolvedOutputDir = cfg.outputDir;
77
+ chrome = cfg.chrome as typeof chrome;
78
+ configBaseUrl = cfg.baseUrl ?? '';
78
79
  } catch {
79
80
  resolvedOutputDir = 'src/assets/screenshots';
80
81
  }
81
82
  }
82
83
 
83
- // resolve the public path the browser/img uses.
84
- // files in public/ are served from the root, so strip 'public/' prefix.
85
- // files in src/assets/ go through Astro's pipeline -- reference via root-relative path.
86
84
  const servePath = resolvedOutputDir
87
85
  .replace(/^\.\//, '')
88
86
  .replace(/^public\//, '');
89
87
  const imgPath = `/${servePath}/${name}.png`;
90
88
 
91
- // check whether the file exists on disk (uses full resolvedOutputDir, not stripped)
92
89
  const absolutePath = path.resolve(process.cwd(), resolvedOutputDir, `${name}.png`);
93
90
  const fileExists = existsSync(absolutePath);
94
91
 
@@ -100,32 +97,357 @@ if (!fileExists && strict) {
100
97
  );
101
98
  }
102
99
 
103
- // data attributes encode the screenshot spec for the CLI scanner
104
- // so `discover.mjs` can also pick these up from the built HTML if needed
105
100
  const specData = JSON.stringify({ url, highlights, width, height, fullPage });
101
+
102
+ const cssChrome = chrome.renderIn === 'css' && chrome.style !== 'none';
103
+ let displayUrl = url;
104
+ if (chrome.baseUrl && configBaseUrl && displayUrl.startsWith(configBaseUrl)) {
105
+ displayUrl = chrome.baseUrl.replace(/\/$/, '') + displayUrl.slice(configBaseUrl.length);
106
+ }
107
+ if (displayUrl.length > 72) displayUrl = displayUrl.slice(0, 69) + '...';
106
108
  ---
109
+
107
110
  <figure
108
111
  class="screenshot-figure"
109
112
  data-screenshot-name={name}
110
113
  data-screenshot-spec={specData}
111
114
  >
112
- {fileExists ? (
113
- <img
114
- src={imgPath}
115
- alt={alt ?? name.replace(/-/g, ' ')}
116
- loading="lazy"
117
- decoding="async"
118
- class="screenshot-img"
119
- />
120
- ) : (
121
- <div
122
- class="screenshot-placeholder"
123
- role="img"
124
- aria-label={alt ?? `Placeholder for ${name}`}
125
- style={`width:${width ?? 1280}px;max-width:100%;aspect-ratio:${(width ?? 1280) / (height ?? 800)};background:#f0f0f0;display:flex;align-items:center;justify-content:center;font-family:sans-serif;color:#999;font-size:14px;border:1px dashed #ccc;`}
126
- >
127
- Screenshot not yet generated: {name}.png<br />
128
- Run <code>take-screenshots</code> to generate it.
115
+ {cssChrome ? (
116
+ <div class="sc-window" data-chrome={chrome.style} data-theme={chrome.theme}>
117
+ <div class="sc-bar">
118
+ <div class="sc-lights">
119
+ <span class="sc-tl sc-close"></span>
120
+ <span class="sc-tl sc-min"></span>
121
+ <span class="sc-tl sc-zoom"></span>
122
+ </div>
123
+ {chrome.showUrl && (
124
+ <div class="sc-urlbar" aria-hidden="true">
125
+ <span class="sc-url">{displayUrl}</span>
126
+ </div>
127
+ )}
128
+ </div>
129
+ <div class="sc-content">
130
+ {fileExists ? (
131
+ <img
132
+ src={imgPath}
133
+ alt={alt ?? name.replace(/-/g, ' ')}
134
+ loading="lazy"
135
+ decoding="async"
136
+ />
137
+ ) : (
138
+ <div
139
+ class="sc-placeholder"
140
+ role="img"
141
+ aria-label={alt ?? `Placeholder for ${name}`}
142
+ >
143
+ Screenshot not yet generated: {name}.png &mdash;
144
+ run <code>take-screenshots</code> to generate it.
145
+ </div>
146
+ )}
147
+ </div>
129
148
  </div>
149
+ ) : (
150
+ fileExists ? (
151
+ <img
152
+ src={imgPath}
153
+ alt={alt ?? name.replace(/-/g, ' ')}
154
+ loading="lazy"
155
+ decoding="async"
156
+ class="screenshot-img"
157
+ />
158
+ ) : (
159
+ <div
160
+ class="screenshot-placeholder"
161
+ role="img"
162
+ aria-label={alt ?? `Placeholder for ${name}`}
163
+ style={`width:${width ?? 1280}px;max-width:100%;aspect-ratio:${(width ?? 1280) / (height ?? 800)};background:#f0f0f0;display:flex;align-items:center;justify-content:center;font-family:sans-serif;color:#999;font-size:14px;border:1px dashed #ccc;`}
164
+ >
165
+ Screenshot not yet generated: {name}.png<br />
166
+ Run <code>take-screenshots</code> to generate it.
167
+ </div>
168
+ )
130
169
  )}
131
170
  </figure>
171
+
172
+ <style>
173
+ /* ── layout ── */
174
+ .sc-window {
175
+ display: flex;
176
+ flex-direction: column;
177
+ width: 100%;
178
+ overflow: hidden;
179
+ border-radius: var(--sc-r);
180
+ box-shadow: var(--sc-shadow);
181
+ }
182
+
183
+ .sc-bar {
184
+ display: flex;
185
+ align-items: center;
186
+ height: var(--sc-bar-h, 40px);
187
+ padding: 0 14px;
188
+ flex-shrink: 0;
189
+ background: var(--sc-bar-bg);
190
+ border-bottom: 1px solid var(--sc-bar-sep);
191
+ }
192
+
193
+ .sc-lights {
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 8px;
197
+ flex-shrink: 0;
198
+ }
199
+
200
+ .sc-tl {
201
+ display: block;
202
+ width: 13px;
203
+ height: 13px;
204
+ border-radius: 50%;
205
+ flex-shrink: 0;
206
+ }
207
+
208
+ .sc-close { background: var(--sc-tl-close); }
209
+ .sc-min { background: var(--sc-tl-min); }
210
+ .sc-zoom { background: var(--sc-tl-zoom); }
211
+
212
+ .sc-urlbar {
213
+ flex: 1;
214
+ margin: 0 14px;
215
+ height: 26px;
216
+ border-radius: 5px;
217
+ display: flex;
218
+ align-items: center;
219
+ justify-content: center;
220
+ padding: 0 10px;
221
+ overflow: hidden;
222
+ background: var(--sc-url-bg);
223
+ border: 1px solid var(--sc-url-border);
224
+ }
225
+
226
+ .sc-url {
227
+ font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
228
+ font-size: 12px;
229
+ color: var(--sc-url-text);
230
+ white-space: nowrap;
231
+ overflow: hidden;
232
+ text-overflow: ellipsis;
233
+ max-width: 100%;
234
+ }
235
+
236
+ .sc-content {
237
+ line-height: 0;
238
+ overflow: hidden;
239
+ }
240
+
241
+ .sc-content img {
242
+ display: block;
243
+ width: 100%;
244
+ height: auto;
245
+ margin: 0;
246
+ }
247
+
248
+ .sc-placeholder {
249
+ background: #f0f0f0;
250
+ padding: 2rem;
251
+ font-family: sans-serif;
252
+ font-size: 14px;
253
+ color: #999;
254
+ text-align: center;
255
+ line-height: 1.5;
256
+ }
257
+
258
+ /* ── safari-macos light ── */
259
+ .sc-window[data-chrome="safari-macos"] {
260
+ --sc-r: 10px;
261
+ --sc-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.08),
262
+ 0 6px 8px rgba(0, 0, 0, 0.05),
263
+ 0 16px 16px rgba(0, 0, 0, 0.06),
264
+ 0 36px 28px rgba(0, 0, 0, 0.05);
265
+ --sc-bar-bg: linear-gradient(to bottom, #e8e8e8, #dcdcdc);
266
+ --sc-bar-sep: #b8b8b8;
267
+ --sc-url-bg: #ffffff;
268
+ --sc-url-border: #cacaca;
269
+ --sc-url-text: #666666;
270
+ --sc-tl-close: #ff5f57;
271
+ --sc-tl-min: #febc2e;
272
+ --sc-tl-zoom: #28c840;
273
+ }
274
+
275
+ /* ── safari-macos dark ── */
276
+ .sc-window[data-chrome="safari-macos"][data-theme="dark"],
277
+ :root[data-theme="dark"] .sc-window[data-chrome="safari-macos"],
278
+ :root.dark .sc-window[data-chrome="safari-macos"] {
279
+ --sc-bar-bg: linear-gradient(to bottom, #323232, #2c2c2c);
280
+ --sc-bar-sep: #1a1a1a;
281
+ --sc-url-bg: #454547;
282
+ --sc-url-border: #5a5a5c;
283
+ --sc-url-text: #f0f0f0;
284
+ }
285
+
286
+ @media (prefers-color-scheme: dark) {
287
+ .sc-window[data-chrome="safari-macos"]:not([data-theme="light"]) {
288
+ --sc-bar-bg: linear-gradient(to bottom, #323232, #2c2c2c);
289
+ --sc-bar-sep: #1a1a1a;
290
+ --sc-url-bg: #454547;
291
+ --sc-url-border: #5a5a5c;
292
+ --sc-url-text: #f0f0f0;
293
+ }
294
+ }
295
+
296
+ /* ── linux/gnome light ── */
297
+ .sc-window[data-chrome="linux"] {
298
+ --sc-r: 8px;
299
+ --sc-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.14),
300
+ 0 6px 8px rgba(0, 0, 0, 0.06),
301
+ 0 18px 24px rgba(0, 0, 0, 0.08);
302
+ --sc-bar-bg: #edeceb;
303
+ --sc-bar-sep: rgba(0, 0, 0, 0.10);
304
+ --sc-url-bg: #ffffff;
305
+ --sc-url-border: rgba(0, 0, 0, 0.22);
306
+ --sc-url-text: #555555;
307
+ --sc-tl-close: #e05555;
308
+ --sc-tl-min: #b8b8b8;
309
+ --sc-tl-zoom: #b8b8b8;
310
+ }
311
+
312
+ /* ── linux dark (solarized) ── */
313
+ .sc-window[data-chrome="linux"][data-theme="dark"],
314
+ :root[data-theme="dark"] .sc-window[data-chrome="linux"],
315
+ :root.dark .sc-window[data-chrome="linux"] {
316
+ --sc-bar-bg: #1a2d40;
317
+ --sc-bar-sep: #0f1c2c;
318
+ --sc-url-bg: #0f1c2c;
319
+ --sc-url-border: #2e4460;
320
+ --sc-url-text: #7a90a8;
321
+ --sc-tl-close: #dc322f;
322
+ --sc-tl-min: #2e4460;
323
+ --sc-tl-zoom: #2e4460;
324
+ }
325
+
326
+ @media (prefers-color-scheme: dark) {
327
+ .sc-window[data-chrome="linux"]:not([data-theme="light"]) {
328
+ --sc-bar-bg: #1a2d40;
329
+ --sc-bar-sep: #0f1c2c;
330
+ --sc-url-bg: #0f1c2c;
331
+ --sc-url-border: #2e4460;
332
+ --sc-url-text: #7a90a8;
333
+ --sc-tl-close: #dc322f;
334
+ --sc-tl-min: #2e4460;
335
+ --sc-tl-zoom: #2e4460;
336
+ }
337
+ }
338
+
339
+ /* ── windows light ── */
340
+ .sc-window[data-chrome="windows"] {
341
+ --sc-r: 8px;
342
+ --sc-bar-h: 32px;
343
+ --sc-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.08),
344
+ 0 6px 8px rgba(0, 0, 0, 0.05),
345
+ 0 18px 24px rgba(0, 0, 0, 0.07);
346
+ --sc-bar-bg: #f3f3f3;
347
+ --sc-bar-sep: rgba(0, 0, 0, 0.06);
348
+ --sc-url-bg: #ffffff;
349
+ --sc-url-border: rgba(0, 0, 0, 0.12);
350
+ --sc-url-text: #555555;
351
+ --sc-tl-close: transparent;
352
+ --sc-tl-min: transparent;
353
+ --sc-tl-zoom: transparent;
354
+ }
355
+
356
+ /* strip right padding so controls sit flush against the edge */
357
+ .sc-window[data-chrome="windows"] .sc-bar { padding-right: 0; }
358
+
359
+ /* move url bar left, controls right */
360
+ .sc-window[data-chrome="windows"] .sc-urlbar { order: 1; margin-left: 0; }
361
+ .sc-window[data-chrome="windows"] .sc-lights { order: 2; gap: 0; margin-left: 14px; align-self: stretch; align-items: stretch; }
362
+
363
+ /* windows order: min − max □ close ✕ */
364
+ .sc-window[data-chrome="windows"] .sc-min { order: 1; }
365
+ .sc-window[data-chrome="windows"] .sc-zoom { order: 2; }
366
+ .sc-window[data-chrome="windows"] .sc-close { order: 3; }
367
+
368
+ /* rectangular flat buttons with symbols */
369
+ .sc-window[data-chrome="windows"] .sc-tl {
370
+ width: 46px;
371
+ height: auto;
372
+ border-radius: 0;
373
+ display: flex;
374
+ align-items: center;
375
+ justify-content: center;
376
+ font: 13px/1 'Segoe UI', system-ui, sans-serif;
377
+ color: #333;
378
+ }
379
+
380
+ .sc-window[data-chrome="windows"] .sc-min::after { content: '\2212'; }
381
+ .sc-window[data-chrome="windows"] .sc-zoom::after { content: '\25A1'; }
382
+ .sc-window[data-chrome="windows"] .sc-close::after { content: '\00D7'; font-size: 16px; }
383
+
384
+ /* ── windows dark ── */
385
+ .sc-window[data-chrome="windows"][data-theme="dark"],
386
+ :root[data-theme="dark"] .sc-window[data-chrome="windows"],
387
+ :root.dark .sc-window[data-chrome="windows"] {
388
+ --sc-bar-bg: #202020;
389
+ --sc-bar-sep: rgba(255, 255, 255, 0.06);
390
+ --sc-url-bg: #2d2d2d;
391
+ --sc-url-border: rgba(255, 255, 255, 0.10);
392
+ --sc-url-text: #c0c0c0;
393
+ }
394
+
395
+ .sc-window[data-chrome="windows"][data-theme="dark"] .sc-tl,
396
+ :root[data-theme="dark"] .sc-window[data-chrome="windows"] .sc-tl,
397
+ :root.dark .sc-window[data-chrome="windows"] .sc-tl { color: #c0c0c0; }
398
+
399
+ @media (prefers-color-scheme: dark) {
400
+ .sc-window[data-chrome="windows"]:not([data-theme="light"]) {
401
+ --sc-bar-bg: #202020;
402
+ --sc-bar-sep: rgba(255, 255, 255, 0.06);
403
+ --sc-url-bg: #2d2d2d;
404
+ --sc-url-border: rgba(255, 255, 255, 0.10);
405
+ --sc-url-text: #c0c0c0;
406
+ }
407
+ .sc-window[data-chrome="windows"]:not([data-theme="light"]) .sc-tl { color: #c0c0c0; }
408
+ }
409
+
410
+ /* ── golden-gate light ── */
411
+ .sc-window[data-chrome="golden-gate"] {
412
+ --sc-r: 14px;
413
+ --sc-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.10),
414
+ 0 8px 10px rgba(0, 0, 0, 0.06),
415
+ 0 22px 22px rgba(0, 0, 0, 0.08),
416
+ 0 48px 36px rgba(0, 0, 0, 0.06);
417
+ --sc-bar-bg: linear-gradient(to bottom, #efefef, #e8e8e8);
418
+ --sc-bar-sep: rgba(0, 0, 0, 0.12);
419
+ --sc-url-bg: rgba(255, 255, 255, 0.88);
420
+ --sc-url-border: rgba(0, 0, 0, 0.14);
421
+ --sc-url-text: #666666;
422
+ --sc-tl-close: #e8524a;
423
+ --sc-tl-min: #e6be46;
424
+ --sc-tl-zoom: #5cc84e;
425
+ }
426
+
427
+ /* ── golden-gate dark ── */
428
+ .sc-window[data-chrome="golden-gate"][data-theme="dark"],
429
+ :root[data-theme="dark"] .sc-window[data-chrome="golden-gate"],
430
+ :root.dark .sc-window[data-chrome="golden-gate"] {
431
+ --sc-bar-bg: linear-gradient(to bottom, #2d2d2d, #272727);
432
+ --sc-bar-sep: rgba(255, 255, 255, 0.08);
433
+ --sc-url-bg: rgba(56, 56, 58, 0.9);
434
+ --sc-url-border: rgba(255, 255, 255, 0.1);
435
+ --sc-url-text: #e0e0e0;
436
+ --sc-tl-close: #c94847;
437
+ --sc-tl-min: #b89b38;
438
+ --sc-tl-zoom: #4ba043;
439
+ }
440
+
441
+ @media (prefers-color-scheme: dark) {
442
+ .sc-window[data-chrome="golden-gate"]:not([data-theme="light"]) {
443
+ --sc-bar-bg: linear-gradient(to bottom, #2d2d2d, #272727);
444
+ --sc-bar-sep: rgba(255, 255, 255, 0.08);
445
+ --sc-url-bg: rgba(56, 56, 58, 0.9);
446
+ --sc-url-border: rgba(255, 255, 255, 0.1);
447
+ --sc-url-text: #e0e0e0;
448
+ --sc-tl-close: #c94847;
449
+ --sc-tl-min: #b89b38;
450
+ --sc-tl-zoom: #4ba043;
451
+ }
452
+ }
453
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-better-declarative-screenshots",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.mjs",
package/src/config.mjs CHANGED
@@ -36,15 +36,21 @@ const DockerSchema = z.object({
36
36
  });
37
37
 
38
38
  const ChromeSchema = z.object({
39
- // 'safari-macos' | 'none'
40
- style: z.enum(['safari-macos', 'none']).default('safari-macos'),
39
+ // chrome style: 'safari-macos' (classic pre-2026), 'golden-gate' (macOS 26 Liquid Glass), 'none'
40
+ style: z.enum(['safari-macos', 'golden-gate', 'linux', 'windows', 'none']).default('safari-macos'),
41
+ // 'css' renders the window frame in HTML/CSS in the Screenshot component (recommended);
42
+ // 'png' composites the chrome into the image at capture time (legacy)
43
+ renderIn: z.enum(['css', 'png']).default('png'),
41
44
  // show the URL bar in the chrome
42
45
  showUrl: z.boolean().default(true),
43
- // dark chrome (title bar) regardless of page dark mode
44
- dark: z.boolean().default(false),
45
- // drop shadow radius in pixels; 0 to disable
46
+ // light or dark chrome (title bar)
47
+ theme: z.enum(['light', 'dark']).default('light'),
48
+ // override the URL shown in the address bar (e.g. a public hostname instead of localhost)
49
+ // replaces the config-level baseUrl prefix in the displayed URL
50
+ baseUrl: z.string().url().optional(),
51
+ // png-only: drop shadow radius in pixels; 0 to disable
46
52
  shadowBlur: z.number().int().min(0).default(40),
47
- // extra padding around the window to make room for the shadow
53
+ // png-only: extra padding around the window to make room for the shadow
48
54
  shadowPadding: z.number().int().min(0).default(48),
49
55
  });
50
56
 
package/src/highlight.mjs CHANGED
@@ -32,6 +32,13 @@ export async function injectHighlights(page, highlights) {
32
32
  }
33
33
 
34
34
  const rect = target.getBoundingClientRect();
35
+
36
+ // skip hidden/zero-size elements (e.g. hidden inputs inside a tokenizer widget)
37
+ if (rect.width === 0 && rect.height === 0) {
38
+ console.warn(`[screenshot] skipping zero-size element for selector: ${spec.selector}`);
39
+ continue;
40
+ }
41
+
35
42
  const scrollX = window.scrollX;
36
43
  const scrollY = window.scrollY;
37
44