Diff -- /var/www/vhosts/3dshawn.com/repricer.3dshawn.com/MODS/RePricer/Layouts.pm
Diff

/var/www/vhosts/3dshawn.com/repricer.3dshawn.com/MODS/RePricer/Layouts.pm

added on local at 2026-07-01 21:47:11

Added
+1169
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to cfc66ccda0a8
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1package MODS::RePricer::Layouts;
2#======================================================================
3# RePricer — Storefront Layouts registry.
4#
5# A "layout" is the HTML structure of a storefront page (hero shape,
6# product grid style, where about/reviews sit). Layouts are paired
7# independently with color "themes" (in MODS::RePricer::Themes), so any
8# layout can wear any color palette.
9#
10# To add a layout:
11# 1. Drop a template file in TEMPLATES/store_layouts/<slug>.html
12# (it receives the same tvars as any other store template).
13# 2. Add a hash to @LAYOUTS below — copy any existing block.
14# 3. Done. Picker updates, no DB migration, no other code changes.
15#
16# WHAT EACH FIELD DOES:
17# id — number. MUST be unique. Used by storefronts.layout_id.
18# slug — short lowercase name. Doubles as the template filename
19# prefix (TEMPLATES/store_layouts/<slug>.html).
20# name — what creators see in the picker.
21# tagline — one-line description of the vibe / who it's for.
22# best_for — short hint for the creator ("for: visual sellers").
23# thumbnail — name of a static thumbnail SVG/preview helper.
24# Currently we render it inline via thumbnail_html().
25#======================================================================
26use strict;
27use warnings;
28
29#----------------------------------------------------------------------
30# Layouts — add new ones here.
31#----------------------------------------------------------------------
32our @LAYOUTS = (
33 {
34 id => 1,
35 slug => 'catalog',
36 name => 'Catalog Classic',
37 tagline => 'Hero with floating product cards, big grid, about strip, reviews.',
38 best_for => 'Most sellers. The safe, balanced default.',
39 },
40 {
41 id => 2,
42 slug => 'lookbook',
43 name => 'Lookbook',
44 tagline => 'Magazine-style. Large alternating images with caption text.',
45 best_for => 'Visually-driven sellers. Art prints, sculpture, terrain.',
46 },
47 {
48 id => 3,
49 slug => 'spotlight',
50 name => 'Single Spotlight',
51 tagline => 'One huge featured product up top, smaller catalog below.',
52 best_for => 'Sellers with a flagship, pre-orders, or weekly drops.',
53 },
54 {
55 id => 4,
56 slug => 'storyteller',
57 name => 'Storyteller',
58 tagline => 'About-the-maker section first. Studio photo + bio, then catalog.',
59 best_for => 'Premium artisans, eBay-style sellers, brand-driven shops.',
60 },
61 {
62 id => 5,
63 slug => 'workshop',
64 name => 'Dense Workshop',
65 tagline => 'Compact, technical, lots of products visible at once. Sparse decoration.',
66 best_for => 'Functional / utility prints. Prolific sellers with deep catalogs.',
67 },
68 {
69 id => 6,
70 slug => 'gallery',
71 name => 'Gallery Wall',
72 tagline => 'Masonry image grid. Minimal text per card, hover reveals details.',
73 best_for => 'Image-first sellers. Scenery, dioramas, big variety.',
74 },
75 {
76 id => 7,
77 slug => 'marketplace',
78 name => 'Marketplace',
79 tagline => 'Sidebar filters + dense product grid. Amazon / Etsy shop feel.',
80 best_for => 'Big catalogs with many categories. Buyers who want to filter.',
81 },
82 {
83 id => 8,
84 slug => 'boutique',
85 name => 'Boutique',
86 tagline => 'Premium minimalism. Wide whitespace, big imagery, refined typography.',
87 best_for => 'High-end art, sculpture, fine-craft makers. Limited drops.',
88 },
89 {
90 id => 9,
91 slug => 'bento',
92 name => 'Bento',
93 tagline => 'Mixed-size tiles like a bento box. Modern startup vibe.',
94 best_for => 'Variety-driven brands. Tech accessories, varied product lines.',
95 },
96 {
97 id => 10,
98 slug => 'megastore',
99 name => 'Megastore',
100 tagline => 'Horizontal scrolling rows by category. Netflix-style browsing.',
101 best_for => 'Huge catalogs, multiple lines, themed weekly drops.',
102 },
103 {
104 id => 11,
105 slug => 'index',
106 name => 'Index',
107 tagline => 'Detailed table list. Sortable columns: name, type, price, rating.',
108 best_for => 'Buyers who want scannable data. Technical / functional prints.',
109 },
110 {
111 id => 12,
112 slug => 'postcard',
113 name => 'Postcard',
114 tagline => 'Zine / handmade vibe. Decorative borders, scrappy energy.',
115 best_for => 'Indie creators, niche fandoms, kawaii / cute prints.',
116 },
117 {
118 id => 13,
119 slug => 'blueprint',
120 name => 'Blueprint',
121 tagline => 'Technical drawing aesthetic. Grid background, callout annotations.',
122 best_for => 'Engineering parts, mech replicas, technical miniatures.',
123 },
124 {
125 id => 14,
126 slug => 'dropcal',
127 name => 'Drop Calendar',
128 tagline => 'Timeline of releases. Weekly schedule, dated drops.',
129 best_for => 'Subscription / eBay-style drops, episodic releases.',
130 },
131 {
132 id => 15,
133 slug => 'brutalist',
134 name => 'Brutalist',
135 tagline => 'Raw web. System fonts, no decoration, monochrome, intentional.',
136 best_for => 'Retro / Y2K aesthetic, programmer-friendly, anti-design.',
137 },
138 {
139 id => 16,
140 slug => 'console',
141 name => 'Console',
142 tagline => 'Sci-fi HUD dashboard. Status bars, hex panels, gaming overlays.',
143 best_for => 'Cyberpunk / mecha / gaming creators. Tech-forward brands.',
144 },
145 {
146 id => 17,
147 slug => 'reel',
148 name => 'Reel',
149 tagline => 'Full-page vertical scroll. One product per snap, story-style.',
150 best_for => 'Limited drops, cinematic showcase, mobile-first audiences.',
151 },
152 {
153 id => 18,
154 slug => 'magazine',
155 name => 'Magazine',
156 tagline => 'Multi-column editorial grid. Mixed sizes, pull-quotes, awards-style.',
157 best_for => 'Creators with mixed content (designs + write-ups + features).',
158 },
159 {
160 id => 19,
161 slug => 'tabs',
162 name => 'Tabs',
163 tagline => 'Categorized tabbed sections. Click between collections.',
164 best_for => 'Sellers with distinct lines, campaigns, or seasonal collections.',
165 },
166 {
167 id => 20,
168 slug => 'cinema',
169 name => 'Cinema',
170 tagline => 'Movie poster cards. Featured products as theatrical releases.',
171 best_for => 'Dramatic / fantasy / character-driven sellers. Big personalities.',
172 },
173 {
174 id => 21,
175 slug => 'splash',
176 name => 'Splash',
177 tagline => 'Bold landing page. One huge CTA above the fold, products below.',
178 best_for => 'Launches, hype drops, single-purpose campaigns.',
179 },
180 {
181 id => 22,
182 slug => 'sidebar',
183 name => 'Sidebar',
184 tagline => 'Persistent left nav. Linear / Figma feel with content panels.',
185 best_for => 'Detailed catalogs, deep navigation, tech-savvy buyers.',
186 },
187 {
188 id => 23,
189 slug => 'forum',
190 name => 'Forum',
191 tagline => 'Discussion-style feed. Each design is a thread with votes and replies.',
192 best_for => 'Community-driven creators, eBay-style audiences.',
193 },
194 {
195 id => 24,
196 slug => 'holo',
197 name => 'Holo',
198 tagline => 'Glassmorphism panels. Frosted blur, neon edges, holographic accents.',
199 best_for => 'Futurist / sci-fi / sci-fantasy. Glow-heavy creators.',
200 },
201 {
202 id => 25,
203 slug => 'concept',
204 name => 'Concept',
205 tagline => 'Full-bleed concept posters. Dramatic image takeovers, minimal chrome.',
206 best_for => 'Cinematic showcase pieces. Big set-pieces, dioramas, dragons.',
207 },
208 {
209 id => 26,
210 slug => 'pinterest',
211 name => 'Pinterest',
212 tagline => 'Masonry grid with always-visible captions. Save-and-share feel.',
213 best_for => 'Browsable showcases, wishlist culture, idea-board sellers.',
214 },
215 {
216 id => 27,
217 slug => 'newsstand',
218 name => 'Newsstand',
219 tagline => 'Newspaper columns with headlines, bylines, and pull quotes.',
220 best_for => 'Long-form storytellers, behind-the-scenes write-ups, editorial.',
221 },
222 {
223 id => 28,
224 slug => 'manifesto',
225 name => 'Manifesto',
226 tagline => 'A bold statement fills the page. Products are footnotes to the message.',
227 best_for => 'Brand-led creators with strong points of view. Activist makers.',
228 },
229 {
230 id => 29,
231 slug => 'tcg',
232 name => 'Trading Cards',
233 tagline => 'TCG-style cards with stat blocks, rarity badges, mana costs.',
234 best_for => 'Game / character / monster designs. Card-game inspired sellers.',
235 },
236 {
237 id => 30,
238 slug => 'vinyl',
239 name => 'Vinyl',
240 tagline => 'Square cards like record sleeves. Album-art aesthetic, B-side details.',
241 best_for => 'Music-adjacent / sample-pack vibe. Curated drop sellers.',
242 },
243 {
244 id => 31,
245 slug => 'comic',
246 name => 'Comic',
247 tagline => 'Comic book panels with speech bubbles and action callouts. POW!',
248 best_for => 'Action figures, hero designs, kids/family-friendly catalogs.',
249 },
250 {
251 id => 32,
252 slug => 'festival',
253 name => 'Festival',
254 tagline => 'Music-festival poster aesthetic. Stacked huge type, lineup feel.',
255 best_for => 'Drops as events. Limited release campaigns, tour-style launches.',
256 },
257 {
258 id => 33,
259 slug => 'periodic',
260 name => 'Periodic',
261 tagline => 'Periodic-table grid. Each design is an element with symbol + number.',
262 best_for => 'Encyclopedic catalogs. Collectible-set sellers (every-figure-numbered).',
263 },
264 {
265 id => 34,
266 slug => 'diary',
267 name => 'Diary',
268 tagline => 'Dated journal entries. Each design is a date-stamped log post.',
269 best_for => 'Process-driven creators, work-in-progress streams, build journals.',
270 },
271 {
272 id => 35,
273 slug => 'stage',
274 name => 'Stage',
275 tagline => 'Theatre stage with curtain frames and a spotlight on the featured.',
276 best_for => 'Dramatic showcase, character makers, performance/cosplay creators.',
277 },
278 {
279 id => 36,
280 slug => 'receipt',
281 name => 'Receipt',
282 tagline => 'Long thermal receipt. Each design is a line item with price tag.',
283 best_for => 'Indie / handmade / quirky creators. Retro thermal aesthetic.',
284 },
285);
286
287#----------------------------------------------------------------------
288# OO interface
289#----------------------------------------------------------------------
290sub new { return bless({}, shift) }
291
292sub all {
293 return [ map { { %$_ } } @LAYOUTS ];
294}
295
296# Look up a layout by id. Falls back to layout #1 (Catalog Classic) so
297# the storefront never renders without a known template.
298sub by_id {
299 my ($self, $id) = @_;
300 $id ||= 0;
301 $id =~ s/[^0-9]//g;
302 foreach my $L (@LAYOUTS) {
303 return { %$L } if $L->{id} == $id;
304 }
305 return { %{ $LAYOUTS[0] } };
306}
307
308# Resolve a layout id → template filename for store.cgi.
309sub template_for {
310 my ($self, $id) = @_;
311 my $L = $self->by_id($id);
312 return "store_layouts/$L->{slug}.html";
313}
314
315# Inline mock-up thumbnail for the layout picker. Renders the layout's
316# shape using the supplied theme's actual colors + sample images so the
317# creator can see roughly what their storefront will look like with that
318# layout/theme combo before clicking Preview.
319#
320# Pass the creator's currently-selected theme (a hash from
321# MODS::RePricer::Themes->by_id) so the picker shows themed previews.
322sub thumbnail_html {
323 my ($self, $layout, $theme) = @_;
324 $layout ||= $LAYOUTS[0];
325 my $slug = $layout->{slug};
326
327 # Build a palette hash once and pass to every sketch. Falls back to a
328 # neutral dark palette if no theme was passed (older callers).
329 my $p = _theme_palette($theme);
330
331 my $sketches = {
332 catalog => _sketch_catalog($p),
333 lookbook => _sketch_lookbook($p),
334 spotlight => _sketch_spotlight($p),
335 storyteller => _sketch_storyteller($p),
336 workshop => _sketch_workshop($p),
337 gallery => _sketch_gallery($p),
338 marketplace => _sketch_marketplace($p),
339 boutique => _sketch_boutique($p),
340 bento => _sketch_bento($p),
341 megastore => _sketch_megastore($p),
342 index => _sketch_index($p),
343 postcard => _sketch_postcard($p),
344 blueprint => _sketch_blueprint($p),
345 dropcal => _sketch_dropcal($p),
346 brutalist => _sketch_brutalist($p),
347 console => _sketch_console($p),
348 reel => _sketch_reel($p),
349 magazine => _sketch_magazine($p),
350 tabs => _sketch_tabs($p),
351 cinema => _sketch_cinema($p),
352 splash => _sketch_splash($p),
353 sidebar => _sketch_sidebar($p),
354 forum => _sketch_forum($p),
355 holo => _sketch_holo($p),
356 concept => _sketch_concept($p),
357 pinterest => _sketch_pinterest($p),
358 newsstand => _sketch_newsstand($p),
359 manifesto => _sketch_manifesto($p),
360 tcg => _sketch_tcg($p),
361 vinyl => _sketch_vinyl($p),
362 comic => _sketch_comic($p),
363 festival => _sketch_festival($p),
364 periodic => _sketch_periodic($p),
365 diary => _sketch_diary($p),
366 stage => _sketch_stage($p),
367 receipt => _sketch_receipt($p),
368 };
369 return $sketches->{$slug} || _sketch_catalog($p);
370}
371
372# Convert a theme hash into a palette ready for the sketch helpers.
373# Adds derived values (accent gradient, sample image URLs) so each
374# sketch can stay declarative.
375sub _theme_palette {
376 my ($theme) = @_;
377 $theme ||= {};
378 my $bg = $theme->{bg} || '#0b1020';
379 my $bg_2 = $theme->{bg_2} || '#141a2e';
380 my $surface = $theme->{surface} || '#1e293b';
381 my $border = $theme->{border} || '#334155';
382 my $text = $theme->{text} || '#cbd5e1';
383 my $text_2 = $theme->{text_2} || '#64748b';
384 my $accent = $theme->{accent} || '#0f766e';
385 my $accent_2 = $theme->{accent_2} || '#059669';
386 return {
387 bg => $bg,
388 bg_2 => $bg_2,
389 surface => $surface,
390 border => $border,
391 text => $text,
392 text_2 => $text_2,
393 accent => $accent,
394 accent_2 => $accent_2,
395 gradient => "linear-gradient(135deg, $accent 0%, $accent_2 100%)",
396 # Three reference images cycled through for visual variety. Files
397 # ship in /assets/images/ — these particular ones are the strong
398 # tabletop minis renders that read well at thumbnail size.
399 img_a => "/assets/images/4.webp",
400 img_b => "/assets/images/9.webp",
401 img_c => "/assets/images/14.webp",
402 img_d => "/assets/images/22.webp",
403 };
404}
405
406#----------------------------------------------------------------------
407# Sketch helpers -- each returns a tiny HTML thumbnail showing the
408# shape of the layout, rendered in the supplied theme's actual colors
409# and including a few real reference images so the picker functions as
410# a real preview.
411#----------------------------------------------------------------------
412
413sub _bar {
414 my ($w, $color, $extra) = @_;
415 $extra ||= '';
416 return qq~<span style="display:inline-block;height:5px;width:$w;background:$color;border-radius:2px;$extra"></span>~;
417}
418
419sub _wrap {
420 my ($p, $inner) = @_;
421 return qq~<div style="background:$p->{bg};height:100%;width:100%;padding:8px;display:flex;flex-direction:column;gap:6px;font-family:system-ui,sans-serif">$inner</div>~;
422}
423
424sub _header_strip {
425 my ($p) = @_;
426 return qq~
427 <div style="display:flex;align-items:center;gap:6px;padding:5px 6px;background:$p->{bg_2};border-radius:3px;border-bottom:1px solid $p->{border}">
428 <span style="width:12px;height:12px;background:$p->{gradient};border-radius:3px"></span>
429 @{[ _bar('30px', $p->{text}) ]}
430 <span style="margin-left:auto;display:flex;gap:4px">
431 @{[ _bar('12px', $p->{text_2}) ]}
432 @{[ _bar('12px', $p->{text_2}) ]}
433 @{[ _bar('12px', $p->{text_2}) ]}
434 </span>
435 </div>~;
436}
437
438# Helper: a tiny image tile. Defaults to a square (aspect-ratio:1) so the
439# images don't get crushed into thin strips. Pass a height explicitly only
440# when the layout shape genuinely calls for it (hero strips, list rows).
441sub _tile {
442 my ($p, $img, $h) = @_;
443 my $sizing = $h ? "height:$h" : "aspect-ratio:1";
444 return qq~<div style="background:url('$img') center/cover;border-radius:3px;border:1px solid $p->{border};$sizing"></div>~;
445}
446
447# 1) Catalog Classic: hero text + 3-col image grid
448sub _sketch_catalog {
449 my ($p) = @_;
450 my $h = _header_strip($p);
451 my $hero = qq~
452 <div style="display:grid;grid-template-columns:1.2fr 1fr;gap:6px;padding:4px 2px">
453 <div style="display:flex;flex-direction:column;gap:4px;justify-content:center">
454 @{[ _bar('80%', $p->{text}) ]}
455 @{[ _bar('60%', $p->{accent}) ]}
456 @{[ _bar('30px', $p->{gradient}, 'margin-top:6px;height:10px') ]}
457 </div>
458 <div style="display:flex;gap:3px">
459 @{[ _tile($p, $p->{img_a}) ]}
460 @{[ _tile($p, $p->{img_b}) ]}
461 </div>
462 </div>~;
463 my $grid = qq~
464 <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;margin-top:auto">
465 @{[ _tile($p, $p->{img_a}) ]}
466 @{[ _tile($p, $p->{img_b}) ]}
467 @{[ _tile($p, $p->{img_c}) ]}
468 </div>~;
469 return _wrap($p, "$h$hero$grid");
470}
471
472# 2) Lookbook: editorial cover (text + big image) then alternating rows
473sub _sketch_lookbook {
474 my ($p) = @_;
475 my $h = _header_strip($p);
476 # Editorial cover: little eyebrow + big title bars on left, large image right
477 my $cover = qq~
478 <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;padding:2px 0">
479 <div style="display:flex;flex-direction:column;gap:4px;justify-content:center">
480 @{[ _bar('40%', $p->{accent}, 'height:3px') ]}
481 @{[ _bar('95%', $p->{text}, 'height:9px') ]}
482 @{[ _bar('70%', $p->{text}, 'height:9px') ]}
483 @{[ _bar('30px', $p->{gradient}, 'margin-top:5px;height:9px') ]}
484 </div>
485 @{[ _tile($p, $p->{img_a}, '78px') ]}
486 </div>~;
487 # One alternating-row teaser to indicate the magazine flow continues
488 my $row = qq~
489 <div style="display:grid;grid-template-columns:1fr 1.4fr;gap:5px;margin-top:auto">
490 <div style="display:flex;flex-direction:column;gap:4px;justify-content:center">
491 @{[ _bar('90%', $p->{text}) ]}
492 @{[ _bar('60%', $p->{accent}) ]}
493 </div>
494 @{[ _tile($p, $p->{img_b}, '40px') ]}
495 </div>~;
496 return _wrap($p, "$h$cover$row");
497}
498
499# 3) Spotlight: one huge product, then a strip of small ones
500sub _sketch_spotlight {
501 my ($p) = @_;
502 my $h = _header_strip($p);
503 my $hero = qq~<div style="background:url('$p->{img_a}') center/cover;border-radius:3px;height:80px;display:flex;align-items:flex-end;padding:6px;border:1px solid $p->{border}"><span style="height:8px;width:60%;background:$p->{gradient};border-radius:2px"></span></div>~;
504 my $strip = qq~
505 <div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:3px;margin-top:auto">
506 @{[ _tile($p, $p->{img_b}) ]}
507 @{[ _tile($p, $p->{img_c}) ]}
508 @{[ _tile($p, $p->{img_d}) ]}
509 @{[ _tile($p, $p->{img_a}) ]}
510 </div>~;
511 return _wrap($p, "$h$hero$strip");
512}
513
514# 4) Storyteller: portrait + bio, then small grid
515sub _sketch_storyteller {
516 my ($p) = @_;
517 my $h = _header_strip($p);
518 my $about = qq~
519 <div style="display:grid;grid-template-columns:1fr 1.4fr;gap:6px;padding:2px 0">
520 <div style="background:url('$p->{img_d}') center/cover;border-radius:50%;height:46px;width:46px;align-self:center;justify-self:center;border:2px solid $p->{accent}"></div>
521 <div style="display:flex;flex-direction:column;gap:3px;justify-content:center">
522 @{[ _bar('90%', $p->{text}) ]}
523 @{[ _bar('70%', $p->{accent}) ]}
524 @{[ _bar('60%', $p->{text_2}) ]}
525 </div>
526 </div>~;
527 my $grid = qq~
528 <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;margin-top:auto">
529 @{[ _tile($p, $p->{img_a}) ]}
530 @{[ _tile($p, $p->{img_b}) ]}
531 @{[ _tile($p, $p->{img_c}) ]}
532 </div>~;
533 return _wrap($p, "$h$about$grid");
534}
535
536# 5) Dense Workshop: dense 5-col compact grid of small images
537sub _sketch_workshop {
538 my ($p) = @_;
539 my $h = _header_strip($p);
540 my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}, $p->{img_d}];
541 my $row = sub {
542 join('', map { _tile($p, $imgs->[$_ % 4]) } (0..4));
543 };
544 my $rows = '';
545 for (1..2) {
546 $rows .= qq~<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:2px">@{[ $row->() ]}</div>~;
547 }
548 return _wrap($p, "$h<div style=\"display:flex;flex-direction:column;gap:2px;margin-top:2px\">$rows</div>");
549}
550
551# 6) Gallery Wall: masonry varied heights
552sub _sketch_gallery {
553 my ($p) = @_;
554 my $h = _header_strip($p);
555 my $grid = qq~
556 <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;margin-top:auto">
557 <div style="display:flex;flex-direction:column;gap:3px">
558 @{[ _tile($p, $p->{img_a}, '30px') ]}
559 @{[ _tile($p, $p->{img_b}, '18px') ]}
560 </div>
561 <div style="display:flex;flex-direction:column;gap:3px">
562 @{[ _tile($p, $p->{img_c}, '18px') ]}
563 @{[ _tile($p, $p->{img_d}, '30px') ]}
564 </div>
565 <div style="display:flex;flex-direction:column;gap:3px">
566 @{[ _tile($p, $p->{img_b}, '24px') ]}
567 @{[ _tile($p, $p->{img_a}, '24px') ]}
568 </div>
569 </div>~;
570 return _wrap($p, "$h$grid");
571}
572
573# 7) Marketplace: sidebar filters + grid of images
574sub _sketch_marketplace {
575 my ($p) = @_;
576 my $h = _header_strip($p);
577 my $body = qq~
578 <div style="display:grid;grid-template-columns:34px 1fr;gap:4px;margin-top:auto;flex:1">
579 <div style="background:$p->{bg_2};border:1px solid $p->{border};border-radius:3px;display:flex;flex-direction:column;gap:4px;padding:4px">
580 @{[ _bar('100%', $p->{accent}) ]}
581 @{[ _bar('80%', $p->{text_2}) ]}
582 @{[ _bar('70%', $p->{text_2}) ]}
583 @{[ _bar('90%', $p->{text_2}) ]}
584 </div>
585 <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px">
586 @{[ _tile($p, $p->{img_a}) ]}
587 @{[ _tile($p, $p->{img_b}) ]}
588 @{[ _tile($p, $p->{img_c}) ]}
589 @{[ _tile($p, $p->{img_d}) ]}
590 @{[ _tile($p, $p->{img_a}) ]}
591 @{[ _tile($p, $p->{img_b}) ]}
592 </div>
593 </div>~;
594 return _wrap($p, "$h$body");
595}
596
597# 8) Boutique: centered editorial title, then airy 2-col catalog grid
598sub _sketch_boutique {
599 my ($p) = @_;
600 my $h = _header_strip($p);
601 # Centered editorial title with eyebrow
602 my $cover = qq~
603 <div style="display:flex;flex-direction:column;align-items:center;gap:4px;padding:6px 0">
604 @{[ _bar('22%', $p->{accent}, 'height:3px') ]}
605 @{[ _bar('60%', $p->{text}, 'height:11px') ]}
606 @{[ _bar('40%', $p->{text}, 'height:11px') ]}
607 </div>~;
608 # 2-col airy grid below (real layout's defining trait is the wide gutter)
609 my $grid = qq~
610 <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:auto;padding:0 14px">
611 @{[ _tile($p, $p->{img_a}) ]}
612 @{[ _tile($p, $p->{img_b}) ]}
613 </div>~;
614 return _wrap($p, "$h$cover$grid");
615}
616
617# 9) Bento: mixed-size image tiles
618sub _sketch_bento {
619 my ($p) = @_;
620 my $h = _header_strip($p);
621 my $body = qq~
622 <div style="display:grid;grid-template-columns:1.4fr 1fr 1fr;grid-template-rows:1fr 1fr;gap:3px;flex:1;margin-top:4px">
623 <div style="background:url('$p->{img_a}') center/cover;border-radius:3px;grid-row:1 / span 2;border:1px solid $p->{border}"></div>
624 <div style="background:url('$p->{img_b}') center/cover;border-radius:3px;border:1px solid $p->{border}"></div>
625 <div style="background:url('$p->{img_c}') center/cover;border-radius:3px;border:1px solid $p->{border}"></div>
626 <div style="background:url('$p->{img_d}') center/cover;border-radius:3px;border:1px solid $p->{border}"></div>
627 <div style="background:$p->{gradient};border-radius:3px"></div>
628 </div>~;
629 return _wrap($p, "$h$body");
630}
631
632# 10) Megastore: big billboard hero + horizontal rows of small tiles
633sub _sketch_megastore {
634 my ($p) = @_;
635 my $h = _header_strip($p);
636 my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}, $p->{img_d}];
637 my $row = sub {
638 join('', map { _tile($p, $imgs->[$_ % 4]) } (0..4));
639 };
640 # Big billboard featuring the first product (matches real layout's hero)
641 my $billboard = qq~
642 <div style="background:url('$p->{img_a}') center/cover;height:60px;border-radius:3px;border:1px solid $p->{border};display:flex;align-items:flex-end;padding:5px;margin-bottom:4px">
643 <div style="display:flex;flex-direction:column;gap:3px">
644 @{[ _bar('60px', $p->{text}, 'height:7px') ]}
645 @{[ _bar('40px', $p->{gradient}, 'height:7px;border-radius:2px') ]}
646 </div>
647 </div>~;
648 my $body = qq~
649 <div style="display:flex;flex-direction:column;gap:4px;flex:1">
650 @{[ _bar('30%', $p->{text}, 'margin-bottom:1px') ]}
651 <div style="display:grid;grid-template-columns:repeat(5,1fr);gap:2px">@{[ $row->() ]}</div>
652 </div>~;
653 return _wrap($p, "$h$billboard$body");
654}
655
656# 11) Index: rows of detail with thumbnails
657sub _sketch_index {
658 my ($p) = @_;
659 my $h = _header_strip($p);
660 my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}, $p->{img_d}, $p->{img_a}];
661 my $rows = '';
662 for my $i (0..4) {
663 $rows .= qq~<div style="display:flex;gap:4px;padding:2px 4px;border-bottom:1px solid $p->{border};align-items:center">
664 <div style="width:8px;height:8px;background:url('$imgs->[$i]') center/cover;border-radius:1px;flex-shrink:0"></div>
665 <span style="height:3px;width:30%;background:$p->{text};border-radius:1px"></span>
666 <span style="height:3px;width:15%;background:$p->{text_2};border-radius:1px"></span>
667 <span style="height:3px;width:18%;background:$p->{accent};border-radius:1px;margin-left:auto"></span>
668 </div>~;
669 }
670 return _wrap($p, "$h<div style=\"display:flex;flex-direction:column;flex:1\">$rows</div>");
671}
672
673# 12) Postcard: dashed border + image grid
674sub _sketch_postcard {
675 my ($p) = @_;
676 my $h = _header_strip($p);
677 my $body = qq~
678 <div style="border:2px dashed $p->{accent};border-radius:4px;padding:5px;flex:1;display:flex;flex-direction:column;gap:4px">
679 <div style="display:flex;justify-content:center;gap:3px;align-items:center">
680 @{[ _bar('25%', $p->{accent}) ]}
681 </div>
682 <div style="display:grid;grid-template-columns:1fr 1fr;gap:3px;flex:1">
683 @{[ _tile($p, $p->{img_a}) ]}
684 @{[ _tile($p, $p->{img_b}) ]}
685 @{[ _tile($p, $p->{img_c}) ]}
686 @{[ _tile($p, $p->{img_d}) ]}
687 </div>
688 </div>~;
689 return _wrap($p, "$h$body");
690}
691
692# 13) Blueprint: grid background with image cards
693sub _sketch_blueprint {
694 my ($p) = @_;
695 my $h = _header_strip($p);
696 my $body = qq~
697 <div style="background-image:linear-gradient($p->{border} 1px, transparent 1px), linear-gradient(90deg, $p->{border} 1px, transparent 1px);background-size:8px 8px;background-color:$p->{bg};flex:1;padding:6px;display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-top:2px;border:1px solid $p->{accent}">
698 <div style="background:url('$p->{img_a}') center/cover;border:1px solid $p->{accent};border-radius:0"></div>
699 <div style="background:url('$p->{img_b}') center/cover;border:1px solid $p->{accent};border-radius:0"></div>
700 <div style="background:url('$p->{img_c}') center/cover;border:1px solid $p->{accent};border-radius:0"></div>
701 <div style="background:url('$p->{img_d}') center/cover;border:1px solid $p->{accent};border-radius:0"></div>
702 </div>~;
703 return _wrap($p, "$h$body");
704}
705
706# 14) Drop calendar: timeline rows with thumbnails
707sub _sketch_dropcal {
708 my ($p) = @_;
709 my $h = _header_strip($p);
710 my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}, $p->{img_d}, $p->{img_a}];
711 my $rows = '';
712 for my $i (0..4) {
713 $rows .= qq~<div style="display:flex;gap:6px;align-items:center;padding:2px 4px">
714 <span style="width:6px;height:6px;border-radius:50%;background:$p->{accent};flex-shrink:0;box-shadow:0 0 0 2px $p->{bg}"></span>
715 <div style="width:10px;height:10px;background:url('$imgs->[$i]') center/cover;border-radius:1px;flex-shrink:0"></div>
716 <span style="height:3px;width:30%;background:$p->{text};border-radius:1px"></span>
717 <span style="height:3px;width:35%;background:$p->{text_2};border-radius:1px"></span>
718 </div>~;
719 }
720 return _wrap($p, "$h<div style=\"display:flex;flex-direction:column;flex:1;border-left:1px solid $p->{border};margin-left:4px;padding-left:2px\">$rows</div>");
721}
722
723# 16) Console: HUD dashboard with status bars + image cells
724sub _sketch_console {
725 my ($p) = @_;
726 my $h = _header_strip($p);
727 my $hud = qq~
728 <div style="display:flex;gap:3px;margin-top:2px">
729 <div style="flex:1;background:$p->{bg_2};border:1px solid $p->{accent};padding:3px;display:flex;flex-direction:column;gap:2px">
730 @{[ _bar('80%', $p->{accent}, 'height:3px') ]}
731 @{[ _bar('60%', $p->{accent_2}, 'height:3px') ]}
732 </div>
733 <div style="flex:1;background:$p->{bg_2};border:1px solid $p->{accent};padding:3px;display:flex;flex-direction:column;gap:2px">
734 @{[ _bar('50%', $p->{accent_2}, 'height:3px') ]}
735 @{[ _bar('70%', $p->{accent}, 'height:3px') ]}
736 </div>
737 <div style="flex:1;background:$p->{bg_2};border:1px solid $p->{accent};padding:3px;display:flex;flex-direction:column;gap:2px">
738 @{[ _bar('90%', $p->{accent}, 'height:3px') ]}
739 @{[ _bar('40%', $p->{accent_2}, 'height:3px') ]}
740 </div>
741 </div>~;
742 my $grid = qq~
743 <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;flex:1;margin-top:auto">
744 <div style="background:url('$p->{img_a}') center/cover;border:1px solid $p->{accent};box-shadow:0 0 0 1px $p->{bg}, 0 0 8px $p->{accent_glow}"></div>
745 <div style="background:url('$p->{img_b}') center/cover;border:1px solid $p->{accent};box-shadow:0 0 0 1px $p->{bg}, 0 0 8px $p->{accent_glow}"></div>
746 <div style="background:url('$p->{img_c}') center/cover;border:1px solid $p->{accent};box-shadow:0 0 0 1px $p->{bg}, 0 0 8px $p->{accent_glow}"></div>
747 </div>~;
748 return _wrap($p, "$h$hud$grid");
749}
750
751# 17) Reel: full-bleed sections, scroll-snap feel
752sub _sketch_reel {
753 my ($p) = @_;
754 my $h = _header_strip($p);
755 my $section = sub {
756 my ($img) = @_;
757 return qq~<div style="background:url('$img') center/cover;flex:1;border-radius:3px;border:1px solid $p->{border};display:flex;align-items:flex-end;padding:5px">
758 <div style="display:flex;flex-direction:column;gap:2px">
759 @{[ _bar('40px', $p->{text}, 'height:5px') ]}
760 @{[ _bar('25px', $p->{accent}, 'height:5px') ]}
761 </div>
762 </div>~;
763 };
764 return _wrap($p, "$h<div style=\"display:flex;flex-direction:column;gap:3px;flex:1\">@{[ $section->($p->{img_a}) ]}@{[ $section->($p->{img_b}) ]}</div>");
765}
766
767# 18) Magazine: mixed-size editorial grid (2x3 with one tall)
768sub _sketch_magazine {
769 my ($p) = @_;
770 my $h = _header_strip($p);
771 my $body = qq~
772 <div style="display:grid;grid-template-columns:1fr 1fr 1fr;grid-template-rows:auto auto;gap:3px;flex:1;margin-top:2px">
773 <div style="grid-row:1 / span 2;background:url('$p->{img_a}') center/cover;border-radius:3px;border:1px solid $p->{border}"></div>
774 <div style="background:url('$p->{img_b}') center/cover;border-radius:3px;border:1px solid $p->{border}"></div>
775 <div style="background:url('$p->{img_c}') center/cover;border-radius:3px;border:1px solid $p->{border}"></div>
776 <div style="grid-column:2 / span 2;background:$p->{surface};border:1px solid $p->{border};border-radius:3px;padding:4px;display:flex;flex-direction:column;gap:3px;justify-content:center">
777 @{[ _bar('80%', $p->{text}) ]}
778 @{[ _bar('60%', $p->{accent}) ]}
779 </div>
780 </div>~;
781 return _wrap($p, "$h$body");
782}
783
784# 19) Tabs: tab strip then grid
785sub _sketch_tabs {
786 my ($p) = @_;
787 my $h = _header_strip($p);
788 my $tabs = qq~
789 <div style="display:flex;gap:0;border-bottom:2px solid $p->{border};margin:2px 0">
790 <span style="padding:3px 8px;background:$p->{accent};color:#fff;font-size:8px;font-weight:700;border-radius:3px 3px 0 0">All</span>
791 @{[ _bar('30px', $p->{text_2}, 'margin:6px 0 0 8px') ]}
792 @{[ _bar('30px', $p->{text_2}, 'margin:6px 0 0 8px') ]}
793 @{[ _bar('30px', $p->{text_2}, 'margin:6px 0 0 8px') ]}
794 </div>~;
795 my $grid = qq~
796 <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;flex:1">
797 @{[ _tile($p, $p->{img_a}) ]}
798 @{[ _tile($p, $p->{img_b}) ]}
799 @{[ _tile($p, $p->{img_c}) ]}
800 </div>~;
801 return _wrap($p, "$h$tabs$grid");
802}
803
804# 20) Cinema: vertical poster cards with letterbox
805sub _sketch_cinema {
806 my ($p) = @_;
807 my $h = _header_strip($p);
808 my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}, $p->{img_d}];
809 my $row = '';
810 for my $i (0..3) {
811 $row .= qq~<div style="aspect-ratio:2/3;background:url('$imgs->[$i]') center/cover;border:2px solid $p->{text};border-radius:0;position:relative">
812 <div style="position:absolute;bottom:0;left:0;right:0;height:30%;background:linear-gradient(0deg,$p->{bg},transparent)"></div>
813 </div>~;
814 }
815 return _wrap($p, "$h<div style=\"display:grid;grid-template-columns:repeat(4,1fr);gap:3px;margin-top:2px;flex:1\">$row</div>");
816}
817
818# 21) Splash: huge CTA at top, tiny product list below
819sub _sketch_splash {
820 my ($p) = @_;
821 my $h = _header_strip($p);
822 my $hero = qq~
823 <div style="background:$p->{gradient};border-radius:3px;padding:8px;flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:4px;min-height:60px">
824 @{[ _bar('70%', '#fff', 'height:8px;opacity:0.9') ]}
825 @{[ _bar('40%', '#fff', 'height:8px;opacity:0.9') ]}
826 <span style="background:$p->{bg};padding:3px 10px;font-size:8px;color:$p->{text};margin-top:4px;border-radius:2px">CTA</span>
827 </div>~;
828 my $strip = qq~
829 <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:2px">
830 @{[ _tile($p, $p->{img_a}) ]}
831 @{[ _tile($p, $p->{img_b}) ]}
832 @{[ _tile($p, $p->{img_c}) ]}
833 @{[ _tile($p, $p->{img_d}) ]}
834 </div>~;
835 return _wrap($p, "$h$hero$strip");
836}
837
838# 22) Sidebar: left nav rail + content panels
839sub _sketch_sidebar {
840 my ($p) = @_;
841 my $body = qq~
842 <div style="display:grid;grid-template-columns:50px 1fr;gap:0;height:100%;font-family:system-ui,sans-serif">
843 <div style="background:$p->{bg_2};border-right:1px solid $p->{border};padding:8px 4px;display:flex;flex-direction:column;gap:5px">
844 <span style="width:14px;height:14px;background:$p->{gradient};border-radius:3px;margin:0 auto"></span>
845 <span style="height:4px;width:80%;background:$p->{accent};border-radius:1px;margin:6px auto 0"></span>
846 <span style="height:4px;width:70%;background:$p->{text_2};border-radius:1px;margin:0 auto"></span>
847 <span style="height:4px;width:60%;background:$p->{text_2};border-radius:1px;margin:0 auto"></span>
848 <span style="height:4px;width:75%;background:$p->{text_2};border-radius:1px;margin:0 auto"></span>
849 <span style="height:4px;width:65%;background:$p->{text_2};border-radius:1px;margin:0 auto"></span>
850 </div>
851 <div style="padding:8px;display:flex;flex-direction:column;gap:4px;background:$p->{bg}">
852 @{[ _bar('40%', $p->{text}, 'height:7px') ]}
853 <div style="display:grid;grid-template-columns:1fr 1fr;gap:3px;flex:1;margin-top:2px">
854 @{[ _tile($p, $p->{img_a}) ]}
855 @{[ _tile($p, $p->{img_b}) ]}
856 @{[ _tile($p, $p->{img_c}) ]}
857 @{[ _tile($p, $p->{img_d}) ]}
858 </div>
859 </div>
860 </div>~;
861 return $body;
862}
863
864# 23) Forum: list of "post" rows with vote bars
865sub _sketch_forum {
866 my ($p) = @_;
867 my $h = _header_strip($p);
868 my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}, $p->{img_d}];
869 my $rows = '';
870 for my $i (0..3) {
871 $rows .= qq~<div style="display:flex;gap:5px;align-items:center;padding:3px 4px;border-bottom:1px solid $p->{border}">
872 <div style="display:flex;flex-direction:column;align-items:center;gap:1px;width:14px">
873 <span style="font-size:6px;color:$p->{accent}">&#9650;</span>
874 <span style="font-size:6px;color:$p->{text};font-weight:700">$i$i</span>
875 <span style="font-size:6px;color:$p->{text_3}">&#9660;</span>
876 </div>
877 <div style="width:18px;height:18px;background:url('$imgs->[$i]') center/cover;border-radius:2px;flex-shrink:0;border:1px solid $p->{border}"></div>
878 <div style="flex:1;display:flex;flex-direction:column;gap:2px">
879 @{[ _bar('70%', $p->{text}, 'height:3px') ]}
880 @{[ _bar('40%', $p->{text_2}, 'height:3px') ]}
881 </div>
882 </div>~;
883 }
884 return _wrap($p, "$h<div style=\"display:flex;flex-direction:column;flex:1\">$rows</div>");
885}
886
887# 24) Holo: glassmorphism translucent panels with glow
888sub _sketch_holo {
889 my ($p) = @_;
890 # Background uses an image to suggest the frosted-blur look
891 my $body = qq~
892 <div style="background:url('$p->{img_a}') center/cover;height:100%;width:100%;padding:8px;display:flex;flex-direction:column;gap:5px;position:relative">
893 <div style="position:absolute;inset:0;background:rgba(0,0,0,0.4);backdrop-filter:blur(2px)"></div>
894 <div style="position:relative;display:flex;align-items:center;gap:6px;padding:5px 6px;background:rgba(255,255,255,0.10);border:1px solid rgba(255,255,255,0.25);border-radius:6px;box-shadow:0 0 10px $p->{accent_glow}">
895 <span style="width:10px;height:10px;background:$p->{gradient};border-radius:50%"></span>
896 @{[ _bar('30px', '#fff', 'opacity:0.9') ]}
897 </div>
898 <div style="position:relative;background:rgba(255,255,255,0.10);border:1px solid $p->{accent};border-radius:6px;padding:6px;flex:1;display:flex;flex-direction:column;gap:3px;box-shadow:0 0 12px $p->{accent_glow}">
899 @{[ _bar('80%', '#fff', 'opacity:0.85') ]}
900 @{[ _bar('55%', $p->{accent}) ]}
901 <div style="display:grid;grid-template-columns:1fr 1fr;gap:3px;margin-top:auto;flex:1">
902 <div style="background:rgba(255,255,255,0.15);border:1px solid rgba(255,255,255,0.3);border-radius:3px"></div>
903 <div style="background:rgba(255,255,255,0.15);border:1px solid rgba(255,255,255,0.3);border-radius:3px"></div>
904 </div>
905 </div>
906 </div>~;
907 return $body;
908}
909
910# 26) Pinterest: masonry with caption tiles always visible
911sub _sketch_pinterest {
912 my ($p) = @_;
913 my $h = _header_strip($p);
914 my $col = sub {
915 my ($img1, $h1, $img2, $h2) = @_;
916 return qq~<div style="display:flex;flex-direction:column;gap:3px">
917 <div style="background:url('$img1') center/cover;border-radius:6px;height:$h1;border:1px solid $p->{border}"></div>
918 <div style="background:$p->{surface};border-radius:4px;padding:3px 4px;height:9px"></div>
919 <div style="background:url('$img2') center/cover;border-radius:6px;height:$h2;border:1px solid $p->{border}"></div>
920 <div style="background:$p->{surface};border-radius:4px;padding:3px 4px;height:9px"></div>
921 </div>~;
922 };
923 my $body = qq~<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;flex:1;margin-top:auto">
924 @{[ $col->($p->{img_a}, '36px', $p->{img_b}, '24px') ]}
925 @{[ $col->($p->{img_c}, '24px', $p->{img_d}, '36px') ]}
926 @{[ $col->($p->{img_b}, '30px', $p->{img_a}, '30px') ]}
927 </div>~;
928 return _wrap($p, "$h$body");
929}
930
931# 27) Newsstand: newspaper columns
932sub _sketch_newsstand {
933 my ($p) = @_;
934 my $h = _header_strip($p);
935 my $col = qq~<div style="display:flex;flex-direction:column;gap:2px">
936 @{[ _bar('100%', $p->{text}, 'height:4px') ]}
937 @{[ _bar('80%', $p->{text_2}, 'height:2px') ]}
938 @{[ _bar('100%', $p->{text_2}, 'height:2px') ]}
939 @{[ _bar('70%', $p->{text_2}, 'height:2px') ]}
940 @{[ _bar('100%', $p->{text_2}, 'height:2px') ]}
941 @{[ _bar('60%', $p->{text_2}, 'height:2px') ]}
942 </div>~;
943 my $body = qq~
944 <div style="text-align:center;padding-bottom:4px;border-bottom:2px solid $p->{text}">
945 @{[ _bar('60%', $p->{text}, 'height:8px;margin:0 auto') ]}
946 </div>
947 <div style="display:grid;grid-template-columns:1fr 1px 1fr 1px 1fr;gap:5px;flex:1;margin-top:4px">
948 $col<div style="background:$p->{border}"></div>$col<div style="background:$p->{border}"></div>$col
949 </div>~;
950 return _wrap($p, "$h$body");
951}
952
953# 28) Manifesto: huge text takes the page, products are tiny tags
954sub _sketch_manifesto {
955 my ($p) = @_;
956 my $h = _header_strip($p);
957 my $body = qq~
958 <div style="flex:1;display:flex;flex-direction:column;justify-content:center;gap:4px;padding:6px 4px">
959 @{[ _bar('90%', $p->{text}, 'height:12px') ]}
960 @{[ _bar('70%', $p->{accent}, 'height:12px') ]}
961 @{[ _bar('80%', $p->{text}, 'height:12px') ]}
962 </div>
963 <div style="display:flex;gap:3px;margin-top:auto">
964 @{[ _tile($p, $p->{img_a}, '14px') ]}
965 @{[ _tile($p, $p->{img_b}, '14px') ]}
966 @{[ _tile($p, $p->{img_c}, '14px') ]}
967 @{[ _tile($p, $p->{img_d}, '14px') ]}
968 </div>~;
969 return _wrap($p, "$h$body");
970}
971
972# 29) Trading cards: portrait cards with rarity strip on top
973sub _sketch_tcg {
974 my ($p) = @_;
975 my $h = _header_strip($p);
976 my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}, $p->{img_d}];
977 my $card = sub {
978 my ($img) = @_;
979 return qq~<div style="display:flex;flex-direction:column;background:$p->{surface};border:1px solid $p->{accent};border-radius:4px;overflow:hidden">
980 <div style="background:$p->{gradient};height:4px"></div>
981 <div style="background:url('$img') center/cover;flex:1;min-height:30px"></div>
982 <div style="padding:2px 3px;display:flex;justify-content:space-between;align-items:center">
983 <span style="height:2px;width:60%;background:$p->{text};border-radius:1px"></span>
984 <span style="font-size:6px;color:$p->{accent};font-weight:700">**</span>
985 </div>
986 </div>~;
987 };
988 my $row = qq~<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:3px;flex:1;margin-top:auto">
989 @{[ $card->($imgs->[0]) ]}@{[ $card->($imgs->[1]) ]}@{[ $card->($imgs->[2]) ]}@{[ $card->($imgs->[3]) ]}
990 </div>~;
991 return _wrap($p, "$h$row");
992}
993
994# 30) Vinyl: square album-cover cards
995sub _sketch_vinyl {
996 my ($p) = @_;
997 my $h = _header_strip($p);
998 my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}, $p->{img_d}];
999 my $sleeve = sub {
1000 my ($img) = @_;
1001 return qq~<div style="position:relative;background:url('$img') center/cover;aspect-ratio:1;border:1px solid $p->{border};border-radius:1px">
1002 <div style="position:absolute;top:50%;left:50%;width:30%;height:30%;border-radius:50%;background:#000;border:2px solid $p->{accent};transform:translate(-50%,-50%);box-shadow:0 0 0 1px #333 inset"></div>
1003 </div>~;
1004 };
1005 my $body = qq~<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;flex:1;margin-top:auto">
1006 @{[ $sleeve->($imgs->[0]) ]}@{[ $sleeve->($imgs->[1]) ]}
1007 </div>~;
1008 return _wrap($p, "$h$body");
1009}
1010
1011# 31) Comic: panels with thick borders + accent flash
1012sub _sketch_comic {
1013 my ($p) = @_;
1014 my $h = _header_strip($p);
1015 my $body = qq~
1016 <div style="display:grid;grid-template-columns:1.4fr 1fr;grid-template-rows:1fr 1fr;gap:3px;flex:1;margin-top:2px;border:2px solid $p->{text}">
1017 <div style="background:url('$p->{img_a}') center/cover;border-right:2px solid $p->{text};border-bottom:2px solid $p->{text};position:relative">
1018 <span style="position:absolute;top:3px;left:3px;background:$p->{accent};color:#fff;padding:1px 4px;font-size:6px;font-weight:900;border:1px solid $p->{text};transform:rotate(-3deg)">POW!</span>
1019 </div>
1020 <div style="background:url('$p->{img_b}') center/cover;border-bottom:2px solid $p->{text}"></div>
1021 <div style="background:url('$p->{img_c}') center/cover;border-right:2px solid $p->{text}"></div>
1022 <div style="background:url('$p->{img_d}') center/cover"></div>
1023 </div>~;
1024 return _wrap($p, "$h$body");
1025}
1026
1027# 32) Festival: stacked huge type with date strip
1028sub _sketch_festival {
1029 my ($p) = @_;
1030 my $h = _header_strip($p);
1031 my $body = qq~
1032 <div style="flex:1;display:flex;flex-direction:column;justify-content:center;gap:3px;padding:4px 0;text-align:center">
1033 @{[ _bar('60%', $p->{text}, 'height:10px;margin:0 auto') ]}
1034 @{[ _bar('80%', $p->{accent}, 'height:14px;margin:0 auto') ]}
1035 @{[ _bar('50%', $p->{text}, 'height:10px;margin:0 auto') ]}
1036 </div>
1037 <div style="display:flex;gap:2px;justify-content:center;font-family:ui-monospace,monospace">
1038 @{[ _bar('15%', $p->{text_2}, 'height:4px') ]}
1039 @{[ _bar('15%', $p->{text_2}, 'height:4px') ]}
1040 @{[ _bar('15%', $p->{text_2}, 'height:4px') ]}
1041 </div>~;
1042 return _wrap($p, "$h$body");
1043}
1044
1045# 33) Periodic: element grid
1046sub _sketch_periodic {
1047 my ($p) = @_;
1048 my $h = _header_strip($p);
1049 my $cell = qq~<div style="background:$p->{surface};border:1px solid $p->{border};border-radius:2px;padding:2px;display:flex;flex-direction:column;justify-content:space-between">
1050 <span style="font-size:5px;color:$p->{text_3};line-height:1">01</span>
1051 <span style="font-size:9px;color:$p->{accent};font-weight:900;line-height:1;text-align:center;font-family:ui-monospace,monospace">Aa</span>
1052 </div>~;
1053 my $row = $cell x 5;
1054 my $rows = '';
1055 for (1..3) {
1056 $rows .= qq~<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:2px">$row</div>~;
1057 }
1058 return _wrap($p, "$h<div style=\"display:flex;flex-direction:column;gap:2px;flex:1;margin-top:2px\">$rows</div>");
1059}
1060
1061# 34) Diary: dated entries with vertical rule
1062sub _sketch_diary {
1063 my ($p) = @_;
1064 my $h = _header_strip($p);
1065 my $imgs = [$p->{img_a}, $p->{img_b}, $p->{img_c}];
1066 my $rows = '';
1067 for my $i (0..2) {
1068 $rows .= qq~<div style="display:grid;grid-template-columns:24px 1fr 18px;gap:5px;align-items:center;padding:3px 0;border-bottom:1px dashed $p->{border}">
1069 <div style="font-family:ui-monospace,monospace;text-align:right">
1070 <div style="font-size:8px;color:$p->{accent};font-weight:700;line-height:1">$i$i</div>
1071 <div style="font-size:5px;color:$p->{text_3};line-height:1">JAN</div>
1072 </div>
1073 <div style="display:flex;flex-direction:column;gap:2px">
1074 @{[ _bar('80%', $p->{text}, 'height:3px') ]}
1075 @{[ _bar('60%', $p->{text_2}, 'height:2px') ]}
1076 </div>
1077 <div style="width:18px;height:18px;background:url('$imgs->[$i]') center/cover;border-radius:2px"></div>
1078 </div>~;
1079 }
1080 return _wrap($p, "$h<div style=\"display:flex;flex-direction:column;flex:1;margin-top:2px\">$rows</div>");
1081}
1082
1083# 35) Stage: spotlight on featured image, side curtains
1084sub _sketch_stage {
1085 my ($p) = @_;
1086 my $h = _header_strip($p);
1087 my $body = qq~
1088 <div style="display:grid;grid-template-columns:8px 1fr 8px;gap:3px;flex:1;margin-top:2px">
1089 <div style="background:linear-gradient(180deg, $p->{accent_deep} 0%, $p->{accent} 100%);border-radius:2px"></div>
1090 <div style="background:url('$p->{img_a}') center/cover;border-radius:3px;border:1px solid $p->{accent};box-shadow:0 0 12px $p->{accent_glow};position:relative;display:flex;align-items:flex-end;padding:5px">
1091 <div style="position:absolute;top:-2px;left:50%;transform:translateX(-50%);width:14px;height:6px;background:radial-gradient(ellipse at top, $p->{accent}, transparent);opacity:0.7"></div>
1092 @{[ _bar('40px', '#fff', 'height:5px') ]}
1093 </div>
1094 <div style="background:linear-gradient(180deg, $p->{accent_deep} 0%, $p->{accent} 100%);border-radius:2px"></div>
1095 </div>~;
1096 return _wrap($p, "$h$body");
1097}
1098
1099# 36) Receipt: long narrow center column with line items
1100sub _sketch_receipt {
1101 my ($p) = @_;
1102 my $h = _header_strip($p);
1103 my $body = qq~
1104 <div style="background:#f8f8f4;color:#1a1a1a;flex:1;margin:2px auto 0;width:65%;padding:6px;font-family:ui-monospace,monospace;display:flex;flex-direction:column;gap:3px;box-shadow:0 4px 8px rgba(0,0,0,0.3);border-radius:1px">
1105 <div style="text-align:center;border-bottom:1px dashed #1a1a1a;padding-bottom:3px">
1106 <span style="height:5px;width:40%;background:#1a1a1a;display:inline-block;border-radius:1px"></span>
1107 </div>
1108 @{[ _bar('100%', '#5a5a5a', 'height:2px') ]}
1109 @{[ _bar('100%', '#5a5a5a', 'height:2px') ]}
1110 @{[ _bar('100%', '#5a5a5a', 'height:2px') ]}
1111 @{[ _bar('100%', '#5a5a5a', 'height:2px') ]}
1112 @{[ _bar('100%', '#5a5a5a', 'height:2px') ]}
1113 <div style="border-top:1px dashed #1a1a1a;padding-top:3px;text-align:right">
1114 <span style="height:4px;width:40%;background:#1a1a1a;display:inline-block;border-radius:1px"></span>
1115 </div>
1116 </div>~;
1117 return _wrap($p, "$h$body");
1118}
1119
1120# 25) Concept: full-bleed poster rows with overlay
1121sub _sketch_concept {
1122 my ($p) = @_;
1123 my $h = _header_strip($p);
1124 my $row = sub {
1125 my ($img) = @_;
1126 return qq~<div style="background:url('$img') center/cover;flex:1;border-radius:3px;display:flex;align-items:center;padding:0 6px;position:relative">
1127 <div style="position:absolute;inset:0;background:linear-gradient(90deg,$p->{bg},transparent 60%);border-radius:3px"></div>
1128 <div style="position:relative;display:flex;flex-direction:column;gap:2px">
1129 @{[ _bar('40px', $p->{text}, 'height:6px') ]}
1130 @{[ _bar('25px', $p->{accent}, 'height:5px') ]}
1131 </div>
1132 </div>~;
1133 };
1134 return _wrap($p, "$h<div style=\"display:flex;flex-direction:column;gap:3px;flex:1\">@{[ $row->($p->{img_a}) ]}@{[ $row->($p->{img_b}) ]}@{[ $row->($p->{img_c}) ]}</div>");
1135}
1136
1137# 15) Brutalist: massive headline + asymmetric grid + accent blocks
1138sub _sketch_brutalist {
1139 my ($p) = @_;
1140 return qq~
1141 <div style="background:$p->{bg};height:100%;width:100%;padding:8px;display:flex;flex-direction:column;gap:5px;border-bottom:5px solid $p->{text}">
1142 <!-- Header strip with massive name + dateline -->
1143 <div style="display:flex;justify-content:space-between;align-items:baseline;padding-bottom:5px;border-bottom:5px solid $p->{text}">
1144 <span style="font-family:ui-sans-serif,system-ui;font-weight:900;font-size:18px;line-height:1;color:$p->{text};letter-spacing:-0.04em">Aa.</span>
1145 <span style="background:$p->{text};color:$p->{bg};padding:2px 6px;font-family:ui-monospace,monospace;font-size:7px;font-weight:700;letter-spacing:1px">CART</span>
1146 </div>
1147 <!-- Section label like a newspaper dateline -->
1148 <div style="font-family:ui-monospace,monospace;font-size:7px;letter-spacing:2px;text-transform:uppercase;color:$p->{accent};font-weight:700">&sect; 02 &mdash; CATALOG</div>
1149 <!-- Asymmetric image grid: one tall + 3 small -->
1150 <div style="display:grid;grid-template-columns:1.2fr 1fr;gap:3px;flex:1">
1151 <div style="background:url('$p->{img_a}') center/cover;position:relative">
1152 <span style="position:absolute;top:3px;left:3px;background:$p->{accent};color:#fff;padding:1px 4px;font-family:ui-monospace,monospace;font-size:6px;font-weight:700">01</span>
1153 </div>
1154 <div style="display:grid;grid-template-rows:1fr 1fr 1fr;gap:3px">
1155 <div style="background:url('$p->{img_b}') center/cover"></div>
1156 <div style="background:url('$p->{img_c}') center/cover"></div>
1157 <div style="background:url('$p->{img_d}') center/cover"></div>
1158 </div>
1159 </div>
1160 <!-- Footer rule + colophon -->
1161 <div style="height:3px;background:$p->{text};margin-top:2px"></div>
1162 <div style="display:flex;justify-content:space-between;font-family:ui-monospace,monospace;font-size:7px;letter-spacing:1px;text-transform:uppercase;color:$p->{text_2}">
1163 <span>VOL. I</span>
1164 <span>WEBSTLS</span>
1165 </div>
1166 </div>~;
1167}
1168
11691;
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help