feat: implement Tabletop QR Stand Generator with high-res PNG and print-ready A5 PDF export

This commit is contained in:
AyrisAI
2026-08-20 18:16:49 +03:00
parent 54a27903c8
commit d396cbb221
12 changed files with 1039 additions and 5 deletions
+3 -2
View File
@@ -3,8 +3,9 @@
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"module": "NodeNext",
"moduleResolution": "NodeNext"
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true
},
"include": ["src"]
}
+35
View File
@@ -2,7 +2,9 @@ import { useEffect, useState } from "react";
import { router } from "expo-router";
import {
ActivityIndicator,
Alert,
Image,
Linking,
Pressable,
ScrollView,
Share,
@@ -87,6 +89,14 @@ export default function QrScreen() {
);
}
function handleOpenStandGenerator() {
if (!active?.slug) return;
const standUrl = `https://menul.io/qr/${active.slug}`;
Linking.openURL(standUrl).catch(() => {
Alert.alert("Bilgi", `Stant Jeneratörü adresi: ${standUrl}`);
});
}
return (
<ScrollView contentContainerStyle={{ flexGrow: 1, backgroundColor: "#FAF8F5", padding: 24, justifyContent: "center" }}>
<View style={{ maxWidth: 440, width: "100%", alignSelf: "center", alignItems: "center" }}>
@@ -158,6 +168,31 @@ export default function QrScreen() {
{/* Action Buttons */}
<View style={{ width: "100%", gap: 12 }}>
{/* Stand Generator PDF / PNG Button */}
<Pressable
onPress={handleOpenStandGenerator}
style={({ pressed }) => ({
backgroundColor: "#C8A96B",
borderRadius: 14,
padding: 16,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 10,
opacity: pressed ? 0.9 : 1,
shadowColor: "#C8A96B",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 10,
elevation: 4,
})}
>
<Ionicons name="color-palette-outline" size={18} color="#FFFFFF" />
<Text style={{ color: "#FFFFFF", fontSize: 15, fontWeight: "800" }}>
Masa Stant Tasarımları & İndir (PDF / PNG)
</Text>
</Pressable>
{/* Share Button */}
<Pressable
onPress={handleShare}
+3
View File
@@ -13,7 +13,10 @@
"@menulio/shared": "workspace:*",
"@supabase/supabase-js": "^2.45.4",
"@swc/helpers": "^0.5.15",
"html-to-image": "^1.11.13",
"jspdf": "^4.2.1",
"next": "^15.2.1",
"qrcode.react": "^4.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
+71
View File
@@ -0,0 +1,71 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { supabase } from "@/lib/supabase";
import { QrStandGeneratorClient } from "@/components/QrStandGeneratorClient";
import { DEMO_RESTAURANT, DEMO_CATEGORIES } from "@/lib/demo-data";
type PageProps = {
params: Promise<{ slug: string }>;
};
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
return {
title: `${slug} | Masa Stant QR Kod Jeneratörü | Menulio`,
description: "Restoranınız için yüksek kalitede baskıya hazır A5 masa kartı ve QR stant tasarımları oluşturun.",
};
}
export default async function RestaurantQrPage({ params }: PageProps) {
const { slug } = await params;
if (slug === "demo" || slug === "gusto-brasserie") {
return (
<QrStandGeneratorClient
restaurantName={DEMO_RESTAURANT.name}
logoUrl={DEMO_RESTAURANT.logo_url}
slug={slug}
categoriesList={DEMO_CATEGORIES.map((c) => c.name)}
/>
);
}
const { data: restaurant } = await supabase
.from("restaurants")
.select("id, name, slug, logo_url")
.eq("slug", slug)
.maybeSingle();
if (!restaurant) {
notFound();
}
const { data: menu } = await supabase
.from("menus")
.select("id, locations!inner(restaurant_id)")
.eq("locations.restaurant_id", restaurant.id)
.maybeSingle();
let categoryNames: string[] = [];
if (menu) {
const { data: categories } = await supabase
.from("menu_categories")
.select("name")
.eq("menu_id", menu.id)
.eq("is_active", true)
.order("sort_order", { ascending: true });
if (categories) {
categoryNames = categories.map((c) => c.name);
}
}
return (
<QrStandGeneratorClient
restaurantName={restaurant.name}
logoUrl={restaurant.logo_url}
slug={restaurant.slug}
categoriesList={categoryNames}
/>
);
}
+19
View File
@@ -0,0 +1,19 @@
import type { Metadata } from "next";
import { QrStandGeneratorClient } from "@/components/QrStandGeneratorClient";
import { DEMO_RESTAURANT, DEMO_CATEGORIES } from "@/lib/demo-data";
export const metadata: Metadata = {
title: "Masa Stant QR Kod Tasarım Jeneratörü | Menulio",
description: "Restoranınız için 5 farklı lüks tema seçeneğiyle yüksek çözünürlüklü PNG ve A5 PDF masa kartı stant çıktıları alın.",
};
export default function QrStandPage() {
return (
<QrStandGeneratorClient
restaurantName={DEMO_RESTAURANT.name}
logoUrl={DEMO_RESTAURANT.logo_url}
slug="gusto-brasserie"
categoriesList={DEMO_CATEGORIES.map((c) => c.name)}
/>
);
}
@@ -0,0 +1,302 @@
"use client";
import { useRef, useState } from "react";
import Link from "next/link";
import { QRCodeCanvas } from "qrcode.react";
import { toPng } from "html-to-image";
import { jsPDF } from "jspdf";
import { QR_STAND_PRESETS, type QrStandPreset } from "@menulio/shared";
interface QrStandGeneratorClientProps {
restaurantName?: string;
logoUrl?: string | null;
slug?: string;
categoriesList?: string[];
}
export function QrStandGeneratorClient({
restaurantName = "THE URBAN BURGER",
logoUrl = null,
slug = "kebapci-sinan-3",
categoriesList = [],
}: QrStandGeneratorClientProps) {
const [selectedKey, setSelectedKey] = useState<string>("urban");
const [downloadingPng, setDownloadingPng] = useState(false);
const [downloadingPdf, setDownloadingPdf] = useState(false);
const standRef = useRef<HTMLDivElement>(null);
const activePreset: QrStandPreset = (QR_STAND_PRESETS[selectedKey] || QR_STAND_PRESETS.urban)!;
const targetUrl = `https://menul.io/${slug}`;
const displayCategories =
categoriesList.length > 0 ? categoriesList.slice(0, 4) : activePreset.categories;
async function handleDownloadPng() {
if (!standRef.current) return;
setDownloadingPng(true);
try {
const dataUrl = await toPng(standRef.current, {
cacheBust: true,
pixelRatio: 3, // 300 DPI high resolution
});
const link = document.createElement("a");
link.download = `${slug}-masa-stant-qr.png`;
link.href = dataUrl;
link.click();
} catch (err) {
console.error("PNG indirme hatası:", err);
alert("PNG indirilemedi. Lütfen tekrar deneyin.");
} finally {
setDownloadingPng(false);
}
}
async function handleDownloadPdf() {
if (!standRef.current) return;
setDownloadingPdf(true);
try {
const dataUrl = await toPng(standRef.current, {
cacheBust: true,
pixelRatio: 3,
});
// A5 dimensions in mm: 148 x 210
const pdf = new jsPDF({
orientation: "portrait",
unit: "mm",
format: "a5",
});
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
pdf.addImage(dataUrl, "PNG", 0, 0, pdfWidth, pdfHeight);
pdf.save(`${slug}-masa-stant-a5.pdf`);
} catch (err) {
console.error("PDF indirme hatası:", err);
alert("PDF indirilemedi. Lütfen tekrar deneyin.");
} finally {
setDownloadingPdf(false);
}
}
return (
<div className="min-h-screen bg-stone-950 text-stone-100 flex flex-col font-sans select-none">
{/* Top Navbar */}
<header className="border-b border-stone-800/80 bg-stone-900/90 backdrop-blur-xl sticky top-0 z-50 px-4 sm:px-6 py-3.5">
<div className="max-w-7xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/" className="font-black text-xl tracking-tight text-amber-400">
MENULIO
</Link>
<span className="text-stone-600 hidden sm:inline">/</span>
<span className="text-xs font-semibold text-stone-400 hidden sm:inline">
Masa Stant QR Jeneratörü
</span>
</div>
<Link
href="/"
className="text-xs font-bold px-4 py-2 rounded-xl bg-amber-500 hover:bg-amber-400 text-stone-950 transition-all shadow-md shadow-amber-500/20"
>
Ana Sayfa
</Link>
</div>
</header>
{/* Main Content Area */}
<main className="flex-1 max-w-6xl w-full mx-auto p-4 sm:p-8 flex flex-col lg:flex-row gap-8 items-center lg:items-start justify-center">
{/* Left Panel: Preset Selectors & Download Controls */}
<div className="w-full lg:w-96 flex flex-col gap-6">
<div className="bg-stone-900/80 border border-stone-800 p-6 rounded-3xl space-y-4 shadow-xl">
<div>
<span className="text-xs font-bold uppercase tracking-wider text-amber-400">
1. Stant Teması Seçin
</span>
<h1 className="text-xl sm:text-2xl font-extrabold text-white mt-1">
Masa Kartı Şablonları
</h1>
<p className="text-xs text-stone-400 mt-1 leading-relaxed">
Restoranınızın konseptine uygun masa stant tasarımını seçin.
</p>
</div>
{/* Presets List */}
<div className="space-y-2.5 pt-2">
{Object.values(QR_STAND_PRESETS).map((preset) => {
const isSelected = preset.key === selectedKey;
return (
<button
key={preset.key}
onClick={() => setSelectedKey(preset.key)}
className={`w-full p-3.5 rounded-2xl border text-left transition-all flex items-center justify-between ${
isSelected
? "bg-amber-500/10 border-amber-500 text-amber-400 shadow-md shadow-amber-500/5"
: "bg-stone-950/60 border-stone-800 text-stone-300 hover:border-stone-700"
}`}
>
<div className="flex items-center gap-3">
<div
className="w-4 h-4 rounded-full border border-white/20 shadow-sm"
style={{ background: preset.plaqueBg }}
/>
<div>
<div className="text-xs font-bold">{preset.name}</div>
<div className="text-[10px] text-stone-400">{preset.categoryName}</div>
</div>
</div>
{isSelected && <span className="text-xs font-bold"></span>}
</button>
);
})}
</div>
</div>
{/* Action Download Buttons */}
<div className="bg-stone-900/80 border border-stone-800 p-6 rounded-3xl space-y-3.5 shadow-xl">
<span className="text-xs font-bold uppercase tracking-wider text-stone-400">
2. Baskı Çıktısı Alın
</span>
{/* PNG Download Button */}
<button
onClick={handleDownloadPng}
disabled={downloadingPng}
className="w-full py-3.5 px-4 rounded-2xl bg-amber-500 hover:bg-amber-400 text-stone-950 font-extrabold text-sm transition-all shadow-lg shadow-amber-500/20 flex items-center justify-center gap-2"
>
{downloadingPng ? (
<span>Hazırlanıyor...</span>
) : (
<>
<svg className="w-5 h-5 fill-current" viewBox="0 0 24 24">
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" />
</svg>
<span>PNG Olarak İndir (300 DPI Ultra HD)</span>
</>
)}
</button>
{/* PDF Download Button */}
<button
onClick={handleDownloadPdf}
disabled={downloadingPdf}
className="w-full py-3.5 px-4 rounded-2xl bg-stone-800 hover:bg-stone-700 text-white font-extrabold text-sm border border-stone-700 transition-all flex items-center justify-center gap-2"
>
{downloadingPdf ? (
<span>Hazırlanıyor...</span>
) : (
<>
<svg className="w-5 h-5 fill-current" viewBox="0 0 24 24">
<path d="M20 2H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H8V4h12v12zM4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6z" />
</svg>
<span>PDF Olarak İndir (A5 Matbaa Baskı)</span>
</>
)}
</button>
</div>
</div>
{/* Right Panel: Stand Live Visual Canvas Preview */}
<div className="flex-1 flex flex-col items-center justify-center w-full">
<div
className="p-6 sm:p-10 rounded-[3rem] shadow-2xl flex items-center justify-center border border-white/5"
style={{ background: activePreset.bgGradient }}
>
{/* Tabletop Plaque Container */}
<div
ref={standRef}
className="w-[360px] sm:w-[380px] min-h-[660px] rounded-[2.5rem] p-7 flex flex-col items-center text-center relative shadow-[0_25px_60px_rgba(0,0,0,0.6)] border-2"
style={{
background: activePreset.plaqueBg,
borderColor: activePreset.borderAccent,
}}
>
{/* Inner Line Accent */}
<div
className="absolute inset-2.5 rounded-[2rem] border pointer-events-none"
style={{ borderColor: activePreset.borderAccent }}
/>
{/* 01. Brand Badge */}
<header className="flex flex-col items-center text-center mt-2 z-10">
<div
className="border-2 rounded-xl px-4 py-2 flex flex-col items-center mb-4 shadow-md"
style={{
borderColor: activePreset.logoBorder,
backgroundColor: activePreset.logoBg,
}}
>
{logoUrl ? (
<img
src={logoUrl}
alt={restaurantName}
className="h-10 w-auto max-w-[140px] object-contain mb-1"
/>
) : null}
<span
className="font-black text-xl tracking-wider uppercase font-['Montserrat'] leading-tight"
style={{ color: activePreset.accentText }}
>
{restaurantName}
</span>
</div>
{/* Scan Action Text */}
<h2
className="font-['Montserrat'] font-black text-2xl sm:text-3xl tracking-tight uppercase leading-[0.95] drop-shadow-sm"
style={{ color: activePreset.headerText }}
>
DİJİTAL MENÜ<br />
<span className="tracking-wide">KAMERANIZLA TARATIN</span>
</h2>
</header>
{/* 02. Centered Custom QR Code Box */}
<section
className="w-60 h-60 sm:w-64 sm:h-64 rounded-3xl p-5 shadow-2xl flex items-center justify-center my-6 relative border border-black/15 z-10"
style={{ backgroundColor: activePreset.qrBg }}
>
<QRCodeCanvas
value={targetUrl}
size={200}
bgColor={activePreset.qrBg}
fgColor={activePreset.qrColor}
level="H"
includeMargin={false}
/>
</section>
{/* 03. Category Highlights Footer */}
<footer className="w-full relative px-4 text-center z-10 pb-2 flex-1 flex flex-col justify-end">
<p
className="text-xs tracking-wide mb-3 font-semibold"
style={{ color: activePreset.subText }}
>
Telefonunuzun kamerasını doğrultun
</p>
{/* Category Text Stack */}
<div
className="flex flex-col gap-1.5 max-w-[210px] mx-auto font-['Montserrat'] font-extrabold uppercase tracking-widest text-sm sm:text-base"
style={{ color: activePreset.headerText }}
>
{displayCategories.map((cat, idx) => (
<div key={idx} className="w-full">
<div className="py-0.5">{cat}</div>
{idx < displayCategories.length - 1 && (
<div
className="w-full h-[1px] my-0.5"
style={{ backgroundColor: activePreset.dividerColor }}
/>
)}
</div>
))}
</div>
</footer>
</div>
</div>
</div>
</main>
</div>
);
}
+308
View File
@@ -0,0 +1,308 @@
# The Urban Burger - Tabletop QR Menu Stand Design System
## Color Palette
- **Forest Green Background**: `#1c3e2b` / `#163323` (ambient tabletop / environment backdrop)
- **Rust / Terracotta Leather Plaque**: `#a64d2a` / `#b35431` (primary card & stand body)
- **Cream / Warm Alabaster QR Inset**: `#f8f3eb` (QR code container background)
- **Rust QR Pattern**: `#a64d2a` / `#944222` (custom branded QR pixel matrix)
- **Warm Golden Wheat**: `#f5be72` / `#e9b366` (logo outline, flame accents, hand-drawn icons)
- **Pure White / Cream UI Text**: `#ffffff` / `#fef8f0` (display headers, scan instructions, category list)
- **Subtle Divider / Border Lines**: `#c96b46` / `rgba(255, 255, 255, 0.2)` (category separation rules)
## Typography
- **Display / Headers**: `Montserrat`, `Plus Jakarta Sans`, or `Oswald` (weights 800-900, uppercase, clean geometric sans-serif with tight line-height)
- **Logo Typography**: Stylized bold outline font with flame icon ligature (or stylized custom geometric sans)
- **Instructional & Body**: `Inter`, `Plus Jakarta Sans`, or `DM Sans` (weights 500-700, generous tracking `0.1em` to `0.2em`)
Font imports:
```css
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@700;800;900&family=Plus+Jakarta+Sans:wght@500;600;700;800&display=swap');
```
## Spacing & Layout
- **Stand Plaque Dimensions**: `360px` `400px` width, `~700px` height (aspect ratio ~1:1.8 portrait tabletop stand)
- **Border Radius**: `28px` (`rounded-[2rem]`) on outer card with physical drop shadow
- **QR Plate Corner Radius**: `24px` (`rounded-3xl`) with inset shadow
- **Hierarchy Order**:
1. Brand Logo (Top)
2. Action Callout (`DIGITAL MENU / SCAN TO ORDER`)
3. Centered Large QR Code Box with custom rounded finder patterns
4. Camera Prompt (`Use your phone's camera`)
5. Category Quick-View List (Burgers, Fries, Shakes, Vegan Options) flanked by illustrative line icons
---
## Reusable Components
### 1. Urban Burger Brand Badge & Header
```html
<div class="flex flex-col items-center text-center">
<!-- Stylized Brand Logo -->
<div class="border-2 border-[#f5be72] rounded-xl px-4 py-2 flex flex-col items-center mb-5 bg-[#a64d2a]/60 shadow-sm">
<div class="flex items-center gap-1.5 text-[#f5be72] font-black text-xl tracking-wider uppercase font-['Montserrat']">
<span>THE URBAN</span>
<!-- Flame Icon -->
<svg class="w-5 h-5 fill-[#f5be72]" viewBox="0 0 24 24">
<path d="M12 2c1.5 3.5 4 6.5 4 10 0 4.42-3.58 8-8 8s-8-3.58-8-8c0-3.5 2.5-6.5 4-10 .5 2 1.5 3.5 3 4.5.5-2.5 2-5 5-6.5z"/>
</svg>
</div>
<span class="text-[#f5be72] font-black text-2xl tracking-[0.25em] uppercase font-['Montserrat'] -mt-1">
BURGER
</span>
</div>
<!-- Scan Call to Action -->
<h2 class="text-white font-['Montserrat'] font-black text-2xl sm:text-3xl tracking-tight uppercase leading-[0.95] drop-shadow-sm">
DIGITAL MENU<br>
<span class="tracking-wide">SCAN TO ORDER</span>
</h2>
</div>
```
### 2. Custom Branded QR Code Box
```html
<div class="w-56 h-56 sm:w-64 sm:h-64 bg-[#f8f3eb] rounded-[2rem] p-5 shadow-inner flex items-center justify-center relative border border-black/10 my-6">
<!-- QR Code SVG with Custom Rust Pixels -->
<svg class="w-full h-full text-[#a64d2a]" viewBox="0 0 100 100" fill="currentColor">
<!-- Top-Left Finder -->
<rect x="5" y="5" width="28" height="28" rx="8" fill="currentColor"/>
<rect x="10" y="10" width="18" height="18" rx="5" fill="#f8f3eb"/>
<rect x="14" y="14" width="10" height="10" rx="3" fill="currentColor"/>
<!-- Top-Right Finder -->
<rect x="67" y="5" width="28" height="28" rx="8" fill="currentColor"/>
<rect x="72" y="10" width="18" height="18" rx="5" fill="#f8f3eb"/>
<rect x="76" y="14" width="10" height="10" rx="3" fill="currentColor"/>
<!-- Bottom-Left Finder -->
<rect x="5" y="67" width="28" height="28" rx="8" fill="currentColor"/>
<rect x="10" y="72" width="18" height="18" rx="5" fill="#f8f3eb"/>
<rect x="14" y="76" width="10" height="10" rx="3" fill="currentColor"/>
<!-- Data Blocks -->
<rect x="38" y="8" width="8" height="8" rx="2"/>
<rect x="50" y="8" width="8" height="14" rx="2"/>
<rect x="38" y="22" width="14" height="8" rx="2"/>
<rect x="8" y="38" width="8" height="12" rx="2"/>
<rect x="20" y="42" width="12" height="8" rx="2"/>
<rect x="38" y="38" width="24" height="24" rx="4"/>
<rect x="42" y="42" width="16" height="16" rx="2" fill="#f8f3eb"/>
<rect x="46" y="46" width="8" height="8" rx="2" fill="currentColor"/>
<rect x="68" y="38" width="10" height="14" rx="2"/>
<rect x="82" y="42" width="10" height="10" rx="2"/>
<rect x="38" y="68" width="12" height="10" rx="2"/>
<rect x="54" y="72" width="8" height="18" rx="2"/>
<rect x="68" y="68" width="12" height="12" rx="2"/>
<rect x="84" y="72" width="8" height="8" rx="2"/>
<rect x="68" y="84" width="24" height="8" rx="2"/>
</svg>
</div>
```
### 3. Menu Quick-Categories Section with Floating Icons
```html
<div class="w-full relative px-6 mt-2 text-center">
<p class="text-white/90 text-sm font-['Plus_Jakarta_Sans'] tracking-wide mb-4 font-medium">
Use your phone's camera
</p>
<!-- Left Burger Line Icon -->
<div class="absolute left-2 bottom-3 text-[#f5be72] opacity-80">
<svg class="w-10 h-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
<path d="M4 11a8 8 0 0 1 16 0H4z"/>
<rect x="3" y="14" width="18" height="3" rx="1.5"/>
<path d="M5 20a3 3 0 0 0 14 0H5z"/>
</svg>
</div>
<!-- Right Drink / Shake Line Icon -->
<div class="absolute right-2 bottom-3 text-[#f5be72] opacity-80">
<svg class="w-10 h-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
<path d="M7 8h10l-1.5 13h-7L7 8z"/>
<path d="M5 8h14"/>
<path d="M12 2v6"/>
<path d="M9 2l3 6"/>
</svg>
</div>
<!-- Category List -->
<div class="flex flex-col gap-2 max-w-[200px] mx-auto text-white font-['Montserrat'] font-extrabold uppercase tracking-widest text-base sm:text-lg">
<div class="py-1">BURGERS</div>
<div class="w-full h-[1px] bg-white/20"></div>
<div class="py-1">FRIES</div>
<div class="w-full h-[1px] bg-white/20"></div>
<div class="py-1">SHAKES</div>
<div class="w-full h-[1px] bg-white/20"></div>
<div class="py-1 text-sm sm:text-base">VEGAN OPTIONS</div>
</div>
</div>
```
---
## Full Responsive Standalone HTML / CSS Implementation
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>The Urban Burger - Tabletop QR Menu</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
tableGreen: '#1a3a29',
tableGreenDark: '#12281c',
rustStand: '#a64d2a',
rustStandDark: '#8e3f20',
qrCream: '#f8f3eb',
goldWheat: '#f5be72',
},
fontFamily: {
display: ['Montserrat', 'sans-serif'],
sans: ['Plus Jakarta Sans', 'sans-serif']
}
}
}
}
</script>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@700;800;900&family=Plus+Jakarta+Sans:wght@500;600;700;800&display=swap" rel="stylesheet">
<style>
body {
background-color: #1a3a29;
background-image:
radial-gradient(circle at 50% 50%, rgba(26, 58, 41, 0.9) 0%, rgba(14, 32, 22, 1) 100%),
repeating-linear-gradient(45deg, rgba(0,0,0,0.03) 0px, rgba(0,0,0,0.03) 2px, transparent 2px, transparent 8px);
font-family: 'Plus Jakarta Sans', sans-serif;
}
.leather-texture {
background-color: #a64d2a;
background-image:
radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.08) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(0, 0, 0, 0.18) 0%, transparent 60%);
}
</style>
</head>
<body class="min-h-screen flex items-center justify-center p-4 sm:p-8 select-none">
<!-- Outer Tabletop QR Stand Plaque -->
<main class="w-full max-w-[380px] leather-texture rounded-[2.5rem] p-6 sm:p-7 flex flex-col items-center text-white relative shadow-[0_25px_60px_rgba(0,0,0,0.6),0_4px_10px_rgba(0,0,0,0.3)] border-2 border-black/30">
<!-- Subtle Inner Border Accent -->
<div class="absolute inset-2.5 rounded-[2rem] border border-white/10 pointer-events-none"></div>
<!-- 01. Top Brand Badge -->
<header class="flex flex-col items-center text-center mt-2 z-10">
<div class="border-2 border-goldWheat rounded-xl px-4 py-1.5 flex flex-col items-center mb-5 bg-[#a64d2a]/80 shadow-md">
<div class="flex items-center gap-1 text-goldWheat font-black text-lg sm:text-xl tracking-wider uppercase font-display">
<span>THE URBAN</span>
<!-- Flame Icon -->
<svg class="w-4 h-4 sm:w-5 sm:h-5 fill-goldWheat" viewBox="0 0 24 24">
<path d="M12 2c1.5 3.5 4 6.5 4 10 0 4.42-3.58 8-8 8s-8-3.58-8-8c0-3.5 2.5-6.5 4-10 .5 2 1.5 3.5 3 4.5.5-2.5 2-5 5-6.5z"/>
</svg>
</div>
<span class="text-goldWheat font-black text-2xl tracking-[0.22em] uppercase font-display -mt-1">
BURGER
</span>
</div>
<!-- Main Action Callout -->
<h1 class="font-display font-black text-2xl sm:text-3xl tracking-tight uppercase leading-[0.92] text-white drop-shadow">
DIGITAL MENU<br>
<span class="tracking-wider">SCAN TO ORDER</span>
</h1>
</header>
<!-- 02. Centered Branded QR Code Box -->
<section class="w-60 h-60 sm:w-64 sm:h-64 bg-qrCream rounded-3xl p-5 shadow-2xl flex items-center justify-center my-6 relative border-2 border-black/15 z-10">
<svg class="w-full h-full text-rustStand" viewBox="0 0 100 100" fill="currentColor">
<!-- Top-Left Target -->
<rect x="6" y="6" width="28" height="28" rx="8" fill="currentColor"/>
<rect x="11" y="11" width="18" height="18" rx="5" fill="#f8f3eb"/>
<rect x="15" y="15" width="10" height="10" rx="3" fill="currentColor"/>
<!-- Top-Right Target -->
<rect x="66" y="6" width="28" height="28" rx="8" fill="currentColor"/>
<rect x="71" y="11" width="18" height="18" rx="5" fill="#f8f3eb"/>
<rect x="75" y="15" width="10" height="10" rx="3" fill="currentColor"/>
<!-- Bottom-Left Target -->
<rect x="6" y="66" width="28" height="28" rx="8" fill="currentColor"/>
<rect x="11" y="71" width="18" height="18" rx="5" fill="#f8f3eb"/>
<rect x="15" y="75" width="10" height="10" rx="3" fill="currentColor"/>
<!-- Data Matrix Pattern -->
<rect x="38" y="8" width="8" height="8" rx="2"/>
<rect x="50" y="8" width="8" height="14" rx="2"/>
<rect x="38" y="22" width="14" height="8" rx="2"/>
<rect x="8" y="38" width="8" height="12" rx="2"/>
<rect x="20" y="42" width="12" height="8" rx="2"/>
<rect x="38" y="38" width="24" height="24" rx="5"/>
<rect x="42" y="42" width="16" height="16" rx="3" fill="#f8f3eb"/>
<rect x="46" y="46" width="8" height="8" rx="2" fill="currentColor"/>
<rect x="68" y="38" width="10" height="14" rx="2"/>
<rect x="82" y="42" width="10" height="10" rx="2"/>
<rect x="38" y="68" width="12" height="10" rx="2"/>
<rect x="54" y="72" width="8" height="18" rx="2"/>
<rect x="68" y="68" width="12" height="12" rx="2"/>
<rect x="84" y="72" width="8" height="8" rx="2"/>
<rect x="68" y="84" width="24" height="8" rx="2"/>
</svg>
</section>
<!-- 03. Prompt & Category Highlights -->
<footer class="w-full relative px-4 text-center z-10 pb-2">
<p class="text-white/90 text-sm font-sans tracking-wide mb-3 font-semibold">
Use your phone's camera
</p>
<!-- Left Decorative Burger Icon -->
<div class="absolute left-1 bottom-4 text-goldWheat/90">
<svg class="w-10 h-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
<path d="M4 10a8 8 0 0 1 16 0H4z"/>
<rect x="3" y="13" width="18" height="3" rx="1.5"/>
<path d="M5 19a3 3 0 0 0 14 0H5z"/>
</svg>
</div>
<!-- Right Decorative Shake Icon -->
<div class="absolute right-1 bottom-4 text-goldWheat/90">
<svg class="w-10 h-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
<path d="M7 8h10l-1.5 13h-7L7 8z"/>
<path d="M5 8h14"/>
<path d="M12 2v6"/>
<path d="M9 2l3 6"/>
</svg>
</div>
<!-- Category Text Stack -->
<div class="flex flex-col gap-1.5 max-w-[190px] mx-auto text-white font-display font-extrabold uppercase tracking-widest text-base">
<div class="py-0.5">BURGERS</div>
<div class="w-full h-[1px] bg-white/25"></div>
<div class="py-0.5">FRIES</div>
<div class="w-full h-[1px] bg-white/25"></div>
<div class="py-0.5">SHAKES</div>
<div class="w-full h-[1px] bg-white/25"></div>
<div class="py-0.5 text-sm tracking-wider">VEGAN OPTIONS</div>
</div>
</footer>
</main>
</body>
</html>
```
## Design Philosophy
- **Tactile Leather Plaque Aesthetic**: Earthy rust-terracotta background paired with an ambient forest green setting to replicate physical wooden/leather restaurant table stands.
- **High-Scanability Contrast**: Clean high-contrast cream QR card embedded with rust custom rounded pixels for instant mobile scanning.
- **Bold Urban Typography**: Punchy condensed uppercase headings paired with clean line-art food icons for quick menu category orientation.
+4
View File
@@ -4,7 +4,11 @@
"version": "0.0.1",
"type": "module",
"main": "./src/index.ts",
"module": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit"
},
+4 -3
View File
@@ -1,3 +1,4 @@
export * from "./slug.js";
export * from "./theme.js";
export * from "./types.js";
export * from "./slug";
export * from "./theme";
export * from "./types";
export * from "./qr-stands";
+105
View File
@@ -0,0 +1,105 @@
export interface QrStandPreset {
key: string;
name: string;
categoryName: string;
bgGradient: string;
plaqueBg: string;
borderAccent: string;
qrBg: string;
qrColor: string;
logoBorder: string;
logoBg: string;
accentText: string;
headerText: string;
subText: string;
dividerColor: string;
categories: string[];
}
export const QR_STAND_PRESETS: Record<string, QrStandPreset> = {
urban: {
key: "urban",
name: "Urban Terracotta",
categoryName: "Burger & Fast Casual",
bgGradient: "radial-gradient(circle at 50% 50%, #1a3a29 0%, #0e2016 100%)",
plaqueBg: "linear-gradient(135deg, #a64d2a 0%, #8e3f20 100%)",
borderAccent: "rgba(255, 255, 255, 0.15)",
qrBg: "#f8f3eb",
qrColor: "#a64d2a",
logoBorder: "#f5be72",
logoBg: "rgba(166, 77, 42, 0.8)",
accentText: "#f5be72",
headerText: "#ffffff",
subText: "rgba(255, 255, 255, 0.9)",
dividerColor: "rgba(255, 255, 255, 0.25)",
categories: ["BURGERS", "FRIES", "SHAKES", "VEGAN OPTIONS"],
},
"la-maison": {
key: "la-maison",
name: "La Maison Gold",
categoryName: "Fine Dining & Steakhouse",
bgGradient: "radial-gradient(circle at 50% 50%, #18181b 0%, #09090b 100%)",
plaqueBg: "linear-gradient(135deg, #1c1917 0%, #0c0a09 100%)",
borderAccent: "rgba(200, 169, 107, 0.4)",
qrBg: "#faf8f5",
qrColor: "#1c1917",
logoBorder: "#c8a96b",
logoBg: "rgba(28, 25, 23, 0.9)",
accentText: "#c8a96b",
headerText: "#ffffff",
subText: "#d6d3d1",
dividerColor: "rgba(200, 169, 107, 0.3)",
categories: ["BAŞLANGIÇLAR", "ANA YEMEKLER", "ŞARAPLAR", "TATLILAR"],
},
nordic: {
key: "nordic",
name: "Nordic Minimal",
categoryName: "Kafe, Fırın & Kahve",
bgGradient: "radial-gradient(circle at 50% 50%, #e7e5e4 0%, #d6d3d1 100%)",
plaqueBg: "linear-gradient(135deg, #ffffff 0%, #f5f5f4 100%)",
borderAccent: "rgba(0, 0, 0, 0.08)",
qrBg: "#18181b",
qrColor: "#ffffff",
logoBorder: "#18181b",
logoBg: "#ffffff",
accentText: "#18181b",
headerText: "#18181b",
subText: "#78716c",
dividerColor: "rgba(0, 0, 0, 0.12)",
categories: ["ESPRESSO BARI", "FROZEN & İÇECEK", "KRUVASAN", "TATLILAR"],
},
bistro: {
key: "bistro",
name: "Classic Bistro",
categoryName: "Pizzeria & İtalyan",
bgGradient: "radial-gradient(circle at 50% 50%, #3f1212 0%, #240a0a 100%)",
plaqueBg: "linear-gradient(135deg, #8b1e1e 0%, #6e1616 100%)",
borderAccent: "rgba(255, 235, 200, 0.2)",
qrBg: "#fffbeb",
qrColor: "#8b1e1e",
logoBorder: "#f59e0b",
logoBg: "rgba(139, 30, 30, 0.85)",
accentText: "#fbbf24",
headerText: "#fffbeb",
subText: "#fef3c7",
dividerColor: "rgba(255, 235, 200, 0.3)",
categories: ["ODUN ATEŞİ PİZZA", "MAKARNALAR", "SALATALAR", "TİRAMİSU"],
},
neon: {
key: "neon",
name: "Neon Sapphire",
categoryName: "Bar, Pub & Nightlife",
bgGradient: "radial-gradient(circle at 50% 50%, #0f172a 0%, #020617 100%)",
plaqueBg: "linear-gradient(135deg, #1e1b4b 0%, #0f172a 100%)",
borderAccent: "rgba(99, 102, 241, 0.4)",
qrBg: "#e0e7ff",
qrColor: "#312e81",
logoBorder: "#818cf8",
logoBg: "rgba(30, 27, 75, 0.9)",
accentText: "#818cf8",
headerText: "#ffffff",
subText: "#c7d2fe",
dividerColor: "rgba(99, 102, 241, 0.3)",
categories: ["İMKAN KOKTEYLLER", "BİRALAR", "ATIŞTIRMALIKLAR", "MÜZİK & ETKİNLİK"],
},
};
+184
View File
@@ -144,9 +144,18 @@ importers:
'@swc/helpers':
specifier: ^0.5.15
version: 0.5.23
html-to-image:
specifier: ^1.11.13
version: 1.11.13
jspdf:
specifier: ^4.2.1
version: 4.2.1
next:
specifier: ^15.2.1
version: 15.5.23(@babel/core@7.29.7(supports-color@8.1.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
qrcode.react:
specifier: ^4.2.0
version: 4.2.0(react@18.3.1)
react:
specifier: ^18.3.1
version: 18.3.1
@@ -2001,12 +2010,18 @@ packages:
'@types/node@22.20.1':
resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==}
'@types/pako@2.0.4':
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
'@types/prop-types@15.7.15':
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
'@types/qrcode@1.5.6':
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
'@types/raf@3.4.3':
resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
'@types/react-dom@18.3.7':
resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
peerDependencies:
@@ -2018,6 +2033,9 @@ packages:
'@types/stack-utils@2.0.3':
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@types/yargs-parser@21.0.3':
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
@@ -2247,6 +2265,10 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@@ -2363,6 +2385,10 @@ packages:
caniuse-lite@1.0.30001809:
resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==}
canvg@3.0.11:
resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==}
engines: {node: '>=10.0.0'}
chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
@@ -2509,6 +2535,9 @@ packages:
resolution: {integrity: sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==}
engines: {node: '>=6.4.0'}
core-js@3.50.0:
resolution: {integrity: sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==}
cosmiconfig@5.2.1:
resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==}
engines: {node: '>=4'}
@@ -2531,6 +2560,9 @@ packages:
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
engines: {node: '>=8'}
css-line-break@2.1.0:
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
@@ -2633,6 +2665,9 @@ packages:
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
dompurify@3.4.14:
resolution: {integrity: sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==}
dotenv-expand@11.0.7:
resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==}
engines: {node: '>=12'}
@@ -2887,6 +2922,9 @@ packages:
fast-json-stringify@7.0.1:
resolution: {integrity: sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==}
fast-png@6.4.0:
resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==}
fast-querystring@1.1.2:
resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==}
@@ -2929,6 +2967,9 @@ packages:
fetch-retry@4.1.1:
resolution: {integrity: sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA==}
fflate@0.8.3:
resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@@ -3124,6 +3165,13 @@ packages:
resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==}
engines: {node: ^16.14.0 || >=18.0.0}
html-to-image@1.11.13:
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
html2canvas@1.4.1:
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
engines: {node: '>=8.0.0'}
http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
@@ -3177,6 +3225,9 @@ packages:
invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
iobuffer@5.4.0:
resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==}
ip-regex@2.1.0:
resolution: {integrity: sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==}
engines: {node: '>=4'}
@@ -3375,6 +3426,9 @@ packages:
jsonfile@6.2.1:
resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==}
jspdf@4.2.1:
resolution: {integrity: sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==}
kind-of@6.0.3:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
@@ -3867,6 +3921,9 @@ packages:
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
pako@2.2.0:
resolution: {integrity: sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==}
parse-json@4.0.0:
resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}
engines: {node: '>=4'}
@@ -3913,6 +3970,9 @@ packages:
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -4066,6 +4126,11 @@ packages:
resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==}
hasBin: true
qrcode.react@4.2.0:
resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
@@ -4084,6 +4149,9 @@ packages:
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
raf@3.4.1:
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
@@ -4279,6 +4347,10 @@ packages:
rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
rgbcolor@1.0.1:
resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
engines: {node: '>= 0.8.15'}
rimraf@2.6.3:
resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==}
deprecated: Rimraf versions prior to v4 are no longer supported
@@ -4474,6 +4546,10 @@ packages:
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
engines: {node: '>=10'}
stackblur-canvas@2.7.0:
resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==}
engines: {node: '>=0.1.14'}
stackframe@1.3.4:
resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==}
@@ -4575,6 +4651,10 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
svg-pathdata@6.0.3:
resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==}
engines: {node: '>=12.0.0'}
tailwindcss@3.4.19:
resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==}
engines: {node: '>=14.0.0'}
@@ -4610,6 +4690,9 @@ packages:
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
engines: {node: '>=8'}
text-segmentation@1.0.3:
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
@@ -4785,6 +4868,9 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
utrie@1.0.2:
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
uuid@7.0.3:
resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==}
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
@@ -7014,12 +7100,17 @@ snapshots:
dependencies:
undici-types: 6.21.0
'@types/pako@2.0.4': {}
'@types/prop-types@15.7.15': {}
'@types/qrcode@1.5.6':
dependencies:
'@types/node': 22.20.1
'@types/raf@3.4.3':
optional: true
'@types/react-dom@18.3.7(@types/react@18.3.31)':
dependencies:
'@types/react': 18.3.31
@@ -7031,6 +7122,9 @@ snapshots:
'@types/stack-utils@2.0.3': {}
'@types/trusted-types@2.0.7':
optional: true
'@types/yargs-parser@21.0.3': {}
'@types/yargs@17.0.35':
@@ -7285,6 +7379,9 @@ snapshots:
balanced-match@1.0.2: {}
base64-arraybuffer@1.0.2:
optional: true
base64-js@1.5.1: {}
baseline-browser-mapping@2.11.15: {}
@@ -7401,6 +7498,18 @@ snapshots:
caniuse-lite@1.0.30001809: {}
canvg@3.0.11:
dependencies:
'@babel/runtime': 7.29.7
'@types/raf': 3.4.3
core-js: 3.50.0
raf: 3.4.1
regenerator-runtime: 0.13.11
rgbcolor: 1.0.1
stackblur-canvas: 2.7.0
svg-pathdata: 6.0.3
optional: true
chalk@2.4.2:
dependencies:
ansi-styles: 3.2.1
@@ -7563,6 +7672,9 @@ snapshots:
dependencies:
browserslist: 4.28.8
core-js@3.50.0:
optional: true
cosmiconfig@5.2.1:
dependencies:
import-fresh: 2.0.0
@@ -7594,6 +7706,11 @@ snapshots:
crypto-random-string@2.0.0: {}
css-line-break@2.1.0:
dependencies:
utrie: 1.0.2
optional: true
cssesc@3.0.0: {}
csstype@3.2.3: {}
@@ -7669,6 +7786,11 @@ snapshots:
dlv@1.1.3: {}
dompurify@3.4.14:
optionalDependencies:
'@types/trusted-types': 2.0.7
optional: true
dotenv-expand@11.0.7:
dependencies:
dotenv: 16.4.7
@@ -8005,6 +8127,12 @@ snapshots:
json-schema-ref-resolver: 3.0.0
rfdc: 1.4.1
fast-png@6.4.0:
dependencies:
'@types/pako': 2.0.4
iobuffer: 5.4.0
pako: 2.2.0
fast-querystring@1.1.2:
dependencies:
fast-decode-uri-component: 1.0.1
@@ -8067,6 +8195,8 @@ snapshots:
fetch-retry@4.1.1: {}
fflate@0.8.3: {}
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
@@ -8282,6 +8412,14 @@ snapshots:
dependencies:
lru-cache: 10.4.3
html-to-image@1.11.13: {}
html2canvas@1.4.1:
dependencies:
css-line-break: 2.1.0
text-segmentation: 1.0.3
optional: true
http-errors@2.0.1:
dependencies:
depd: 2.0.0
@@ -8329,6 +8467,8 @@ snapshots:
dependencies:
loose-envify: 1.4.0
iobuffer@5.4.0: {}
ip-regex@2.1.0: {}
ipaddr.js@1.9.1: {}
@@ -8545,6 +8685,17 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.11
jspdf@4.2.1:
dependencies:
'@babel/runtime': 7.29.7
fast-png: 6.4.0
fflate: 0.8.3
optionalDependencies:
canvg: 3.0.11
core-js: 3.50.0
dompurify: 3.4.14
html2canvas: 1.4.1
kind-of@6.0.3: {}
kleur@3.0.3: {}
@@ -9088,6 +9239,8 @@ snapshots:
package-json-from-dist@1.0.1: {}
pako@2.2.0: {}
parse-json@4.0.0:
dependencies:
error-ex: 1.3.4
@@ -9120,6 +9273,9 @@ snapshots:
pathe@2.0.3: {}
performance-now@2.1.0:
optional: true
picocolors@1.1.1: {}
picomatch@2.3.2: {}
@@ -9262,6 +9418,10 @@ snapshots:
qrcode-terminal@0.11.0: {}
qrcode.react@4.2.0(react@18.3.1):
dependencies:
react: 18.3.1
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
@@ -9283,6 +9443,11 @@ snapshots:
quick-format-unescaped@4.0.4: {}
raf@3.4.1:
dependencies:
performance-now: 2.1.0
optional: true
range-parser@1.2.1: {}
rc@1.2.8:
@@ -9511,6 +9676,9 @@ snapshots:
rfdc@1.4.1: {}
rgbcolor@1.0.1:
optional: true
rimraf@2.6.3:
dependencies:
glob: 7.2.3
@@ -9739,6 +9907,9 @@ snapshots:
dependencies:
escape-string-regexp: 2.0.0
stackblur-canvas@2.7.0:
optional: true
stackframe@1.3.4: {}
stacktrace-parser@0.1.11:
@@ -9825,6 +9996,9 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
svg-pathdata@6.0.3:
optional: true
tailwindcss@3.4.19(tsx@4.23.12):
dependencies:
'@alloc/quick-lru': 5.2.0
@@ -9894,6 +10068,11 @@ snapshots:
glob: 7.2.3
minimatch: 3.1.5
text-segmentation@1.0.3:
dependencies:
utrie: 1.0.2
optional: true
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
@@ -10034,6 +10213,11 @@ snapshots:
utils-merge@1.0.1: {}
utrie@1.0.2:
dependencies:
base64-arraybuffer: 1.0.2
optional: true
uuid@7.0.3: {}
uuid@8.3.2: {}
+1
View File
@@ -2,5 +2,6 @@ packages:
- "apps/*"
- "packages/*"
allowBuilds:
core-js: set this to true or false
esbuild: true
sharp: true