eienmojiki commited on
Commit
fbcf5d4
1 Parent(s): eaac59e

Upload 21 files

Browse files
fonts/Oswald-Bold.ttf ADDED
Binary file (87.6 kB). View file
 
fonts/Oswald-ExtraLight.ttf ADDED
Binary file (87.2 kB). View file
 
fonts/Oswald-Light.ttf ADDED
Binary file (87.5 kB). View file
 
fonts/Oswald-Medium.ttf ADDED
Binary file (87.6 kB). View file
 
fonts/Oswald-Regular.ttf ADDED
Binary file (87.3 kB). View file
 
fonts/Oswald-SemiBold.ttf ADDED
Binary file (87.6 kB). View file
 
fonts/PlayfairDisplay-Black.ttf ADDED
Binary file (194 kB). View file
 
fonts/PlayfairDisplay-BlackItalic.ttf ADDED
Binary file (178 kB). View file
 
fonts/PlayfairDisplay-Bold.ttf ADDED
Binary file (194 kB). View file
 
fonts/PlayfairDisplay-BoldItalic.ttf ADDED
Binary file (178 kB). View file
 
fonts/PlayfairDisplay-ExtraBold.ttf ADDED
Binary file (194 kB). View file
 
fonts/PlayfairDisplay-ExtraBoldItalic.ttf ADDED
Binary file (178 kB). View file
 
fonts/PlayfairDisplay-Italic.ttf ADDED
Binary file (178 kB). View file
 
fonts/PlayfairDisplay-Medium.ttf ADDED
Binary file (194 kB). View file
 
fonts/PlayfairDisplay-MediumItalic.ttf ADDED
Binary file (178 kB). View file
 
fonts/PlayfairDisplay-Regular.ttf ADDED
Binary file (193 kB). View file
 
fonts/PlayfairDisplay-SemiBold.ttf ADDED
Binary file (194 kB). View file
 
fonts/PlayfairDisplay-SemiBoldItalic.ttf ADDED
Binary file (179 kB). View file
 
public/index.html ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Quote Image Generator</title>
7
+ <link rel="stylesheet" href="style.css" />
8
+ </head>
9
+ <body>
10
+ <button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme">
11
+ 🌙
12
+ </button>
13
+
14
+ <div class="container">
15
+ <div class="panel">
16
+ <h2>Quote Generator</h2>
17
+ <div class="form-group">
18
+ <label>Quote Text</label>
19
+ <textarea id="quoteText">
20
+ The only way to do great work is to love what you do.</textarea
21
+ >
22
+ </div>
23
+
24
+ <div class="form-group">
25
+ <label>Author</label>
26
+ <input type="text" id="author" value="Steve Jobs" />
27
+ </div>
28
+
29
+ <div class="color-grid">
30
+ <div class="form-group color-picker">
31
+ <label>Background Color</label>
32
+ <div class="color-input-wrapper">
33
+ <input type="color" id="bgColor" value="#ffffff" />
34
+ <input
35
+ type="text"
36
+ id="bgColorText"
37
+ value="#ffffff"
38
+ class="color-text"
39
+ />
40
+ </div>
41
+ </div>
42
+
43
+ <div class="form-group color-picker">
44
+ <label>Bar & Quote Mark Color</label>
45
+ <div class="color-input-wrapper">
46
+ <input type="color" id="barColor" value="#4f46e5" />
47
+ <input
48
+ type="text"
49
+ id="barColorText"
50
+ value="#4f46e5"
51
+ class="color-text"
52
+ />
53
+ </div>
54
+ </div>
55
+
56
+ <div class="form-group color-picker">
57
+ <label>Quote Text Color</label>
58
+ <div class="color-input-wrapper">
59
+ <input type="color" id="textColor" value="#1e293b" />
60
+ <input
61
+ type="text"
62
+ id="textColorText"
63
+ value="#1e293b"
64
+ class="color-text"
65
+ />
66
+ </div>
67
+ </div>
68
+
69
+ <div class="form-group color-picker">
70
+ <label>Author Color</label>
71
+ <div class="color-input-wrapper">
72
+ <input type="color" id="authorColor" value="#64748b" />
73
+ <input
74
+ type="text"
75
+ id="authorColorText"
76
+ value="#64748b"
77
+ class="color-text"
78
+ />
79
+ </div>
80
+ </div>
81
+ </div>
82
+
83
+ <div class="font-section">
84
+ <div class="form-group font-controls">
85
+ <label>Quote Font Style</label>
86
+ <div class="font-selectors">
87
+ <select id="quoteFontFamily" class="font-select">
88
+ <option value="">Loading fonts...</option>
89
+ </select>
90
+ <select id="quoteFontStyle" class="font-select">
91
+ <option value="normal">Normal</option>
92
+ </select>
93
+ </div>
94
+ </div>
95
+
96
+ <div class="form-group font-controls">
97
+ <label>Author Font Style</label>
98
+ <div class="font-selectors">
99
+ <select id="authorFontFamily" class="font-select">
100
+ <option value="">Loading fonts...</option>
101
+ </select>
102
+ <select id="authorFontStyle" class="font-select">
103
+ <option value="normal">Normal</option>
104
+ </select>
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ <div class="form-group">
110
+ <label>Bar Width <span id="barWidthValue">4px</span></label>
111
+ <input
112
+ type="range"
113
+ id="barWidth"
114
+ min="2"
115
+ max="10"
116
+ value="4"
117
+ class="slider"
118
+ oninput="document.getElementById('barWidthValue').textContent = this.value + 'px'"
119
+ />
120
+ </div>
121
+
122
+ <div class="btn-group">
123
+ <button class="btn btn-primary" onclick="generateQuote()">
124
+ <span class="btn-text">Generate</span>
125
+ <div class="btn-loader"></div>
126
+ </button>
127
+ <button class="btn btn-success" onclick="downloadQuote()">
128
+ Download
129
+ </button>
130
+ <button class="btn btn-secondary" onclick="viewRequests()">
131
+ View Requests
132
+ </button>
133
+ </div>
134
+ </div>
135
+
136
+ <div class="panel">
137
+ <h2>Preview</h2>
138
+ <div id="preview-container">
139
+ <img id="quoteImage" alt="Generated Quote" />
140
+ <div id="loading" class="loading">
141
+ <div class="loading-content">
142
+ <div class="loading-spinner"></div>
143
+ <span>Generating quote...</span>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <div id="requestsModal" class="modal">
151
+ <div class="modal-content">
152
+ <span class="close-btn" onclick="closeModal()">&times;</span>
153
+ <h2>Request History</h2>
154
+ <div id="requestsList" class="requests-list"></div>
155
+ </div>
156
+ </div>
157
+
158
+ <div id="status"></div>
159
+
160
+ <script src="script.js"></script>
161
+ </body>
162
+ </html>
public/script.js ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let isDarkMode = false;
2
+ let fontData = [];
3
+
4
+ // Load available fonts when page loads
5
+ async function loadAvailableFonts() {
6
+ try {
7
+ const response = await fetch("/api/fonts");
8
+ fontData = await response.json();
9
+
10
+ // Populate both quote and author font family selects
11
+ const quoteFontFamily = document.getElementById("quoteFontFamily");
12
+ const authorFontFamily = document.getElementById("authorFontFamily");
13
+
14
+ const fontOptions = fontData
15
+ .map((font) => `<option value="${font.family}">${font.family}</option>`)
16
+ .join("");
17
+
18
+ quoteFontFamily.innerHTML = fontOptions;
19
+ authorFontFamily.innerHTML = fontOptions;
20
+
21
+ // Initialize variations for both
22
+ if (fontData.length > 0) {
23
+ updateFontVariations("quote", fontData[0].family);
24
+ updateFontVariations("author", fontData[0].family);
25
+ generateQuote();
26
+ }
27
+ } catch (error) {
28
+ console.error("Error loading fonts:", error);
29
+ showStatus("Error loading fonts", "error");
30
+ }
31
+ }
32
+
33
+ // Update font variations based on selected font family
34
+ function updateFontVariations(type, family) {
35
+ const font = fontData.find((f) => f.family === family);
36
+ if (!font) return;
37
+
38
+ const styleSelect = document.getElementById(`${type}FontStyle`);
39
+
40
+ // Get unique styles
41
+ const styles = new Set(font.variations.map((v) => v.style));
42
+
43
+ // Update style options
44
+ styleSelect.innerHTML = Array.from(styles)
45
+ .map(
46
+ (style) => `<option value="${style}">${formatFontStyle(style)}</option>`
47
+ )
48
+ .join("");
49
+ }
50
+
51
+ // Format font weight for display
52
+ function formatFontWeight(weight) {
53
+ switch (weight) {
54
+ case "normal":
55
+ return "Regular";
56
+ case "bold":
57
+ return "Bold";
58
+ case "light":
59
+ return "Light";
60
+ case "medium":
61
+ return "Medium";
62
+ default:
63
+ return weight.charAt(0).toUpperCase() + weight.slice(1);
64
+ }
65
+ }
66
+
67
+ // Format font style for display
68
+ function formatFontStyle(style) {
69
+ switch (style) {
70
+ case "normal":
71
+ return "Normal";
72
+ case "italic":
73
+ return "Italic";
74
+ default:
75
+ return style.charAt(0).toUpperCase() + style.slice(1);
76
+ }
77
+ }
78
+
79
+ // Setup color pickers with hex input sync
80
+ function setupColorPickers() {
81
+ const colorInputs = document.querySelectorAll('input[type="color"]');
82
+ colorInputs.forEach((input) => {
83
+ const textInput = document.getElementById(input.id + "Text");
84
+
85
+ // Update text input when color changes
86
+ input.addEventListener("input", () => {
87
+ textInput.value = input.value.toUpperCase();
88
+ });
89
+
90
+ // Update color input when valid hex is entered
91
+ textInput.addEventListener("input", () => {
92
+ let hex = textInput.value.trim();
93
+ if (!hex.startsWith("#")) {
94
+ hex = "#" + hex;
95
+ }
96
+ if (/^#[0-9A-F]{6}$/i.test(hex)) {
97
+ input.value = hex;
98
+ }
99
+ });
100
+
101
+ // Format hex on blur
102
+ textInput.addEventListener("blur", () => {
103
+ let hex = textInput.value.trim();
104
+ if (!hex.startsWith("#")) {
105
+ hex = "#" + hex;
106
+ }
107
+ if (/^#[0-9A-F]{6}$/i.test(hex)) {
108
+ textInput.value = hex.toUpperCase();
109
+ input.value = hex;
110
+ } else {
111
+ textInput.value = input.value.toUpperCase();
112
+ }
113
+ });
114
+ });
115
+ }
116
+
117
+ async function generateQuote() {
118
+ const generateBtn = document.querySelector(".btn-primary");
119
+ const btnText = generateBtn.querySelector(".btn-text");
120
+ const btnLoader = generateBtn.querySelector(".btn-loader");
121
+
122
+ try {
123
+ // Show loading state
124
+ generateBtn.disabled = true;
125
+ btnText.style.opacity = "0";
126
+ btnLoader.style.display = "block";
127
+ document.getElementById("loading").style.display = "flex";
128
+ document.getElementById("quoteImage").style.opacity = "0.5";
129
+
130
+ const data = {
131
+ text: document.getElementById("quoteText").value,
132
+ author: document.getElementById("author").value,
133
+ bgColor: document.getElementById("bgColor").value,
134
+ barColor: document.getElementById("barColor").value,
135
+ textColor: document.getElementById("textColor").value,
136
+ authorColor: document.getElementById("authorColor").value,
137
+ quoteFontFamily: document.getElementById("quoteFontFamily").value,
138
+ quoteFontWeight: "bold", // Force bold for quote
139
+ quoteFontStyle: document.getElementById("quoteFontStyle").value,
140
+ authorFontFamily: document.getElementById("authorFontFamily").value,
141
+ authorFontWeight: "normal", // Force normal for author
142
+ authorFontStyle: document.getElementById("authorFontStyle").value,
143
+ barWidth: parseInt(document.getElementById("barWidth").value),
144
+ };
145
+
146
+ const response = await fetch("/api/generate-quote", {
147
+ method: "POST",
148
+ headers: {
149
+ "Content-Type": "application/json",
150
+ },
151
+ body: JSON.stringify(data),
152
+ });
153
+
154
+ const result = await response.json();
155
+
156
+ if (result.success) {
157
+ const img = document.getElementById("quoteImage");
158
+ img.src = result.imageUrl;
159
+ img.style.opacity = "1";
160
+ showStatus("Quote generated successfully!", "success");
161
+ } else {
162
+ throw new Error(result.error);
163
+ }
164
+ } catch (error) {
165
+ showStatus(error.message || "Error generating quote", "error");
166
+ console.error(error);
167
+ } finally {
168
+ // Reset loading state
169
+ generateBtn.disabled = false;
170
+ btnText.style.opacity = "1";
171
+ btnLoader.style.display = "none";
172
+ document.getElementById("loading").style.display = "none";
173
+ }
174
+ }
175
+
176
+ function downloadQuote() {
177
+ const img = document.getElementById("quoteImage");
178
+ if (!img.src || img.src === window.location.href) {
179
+ showStatus("Generate a quote first!", "error");
180
+ return;
181
+ }
182
+
183
+ const link = document.createElement("a");
184
+ const timestamp = new Date().toISOString().split("T")[0];
185
+ link.download = `quote-${timestamp}.png`;
186
+ link.href = img.src;
187
+ link.click();
188
+ }
189
+
190
+ async function viewRequests() {
191
+ try {
192
+ const response = await fetch("/api/requests-history");
193
+ const requests = await response.json();
194
+
195
+ const requestsList = document.getElementById("requestsList");
196
+ requestsList.innerHTML = requests
197
+ .map(
198
+ (req) => `
199
+ <div class="request-item">
200
+ <div class="request-time">${new Date(
201
+ req.timestamp
202
+ ).toLocaleString()}</div>
203
+ <pre class="request-data">${JSON.stringify(
204
+ req.request,
205
+ null,
206
+ 2
207
+ )}</pre>
208
+ </div>
209
+ `
210
+ )
211
+ .join("");
212
+
213
+ document.getElementById("requestsModal").style.display = "block";
214
+ } catch (error) {
215
+ showStatus("Error fetching requests", "error");
216
+ console.error(error);
217
+ }
218
+ }
219
+
220
+ function closeModal() {
221
+ document.getElementById("requestsModal").style.display = "none";
222
+ }
223
+
224
+ function toggleTheme() {
225
+ isDarkMode = !isDarkMode;
226
+ document.documentElement.setAttribute(
227
+ "data-theme",
228
+ isDarkMode ? "dark" : "light"
229
+ );
230
+ document.querySelector(".theme-toggle").textContent = isDarkMode
231
+ ? "☀️"
232
+ : "🌙";
233
+ }
234
+
235
+ function showStatus(message, type) {
236
+ const status = document.getElementById("status");
237
+ status.textContent = message;
238
+ status.style.color = type === "success" ? "var(--success)" : "#ef4444";
239
+ status.style.display = "block";
240
+
241
+ setTimeout(() => {
242
+ status.style.display = "none";
243
+ }, 3000);
244
+ }
245
+
246
+ // Event Listeners
247
+ document.addEventListener("DOMContentLoaded", () => {
248
+ loadAvailableFonts();
249
+ setupColorPickers();
250
+
251
+ // Font family change listeners
252
+ document.getElementById("quoteFontFamily").addEventListener("change", (e) => {
253
+ updateFontVariations("quote", e.target.value);
254
+ });
255
+
256
+ document
257
+ .getElementById("authorFontFamily")
258
+ .addEventListener("change", (e) => {
259
+ updateFontVariations("author", e.target.value);
260
+ });
261
+
262
+ // Check system theme preference
263
+ if (
264
+ window.matchMedia &&
265
+ window.matchMedia("(prefers-color-scheme: dark)").matches
266
+ ) {
267
+ toggleTheme();
268
+ }
269
+
270
+ // Setup bar width value display
271
+ const barWidthSlider = document.getElementById("barWidth");
272
+ const barWidthValue = document.getElementById("barWidthValue");
273
+ barWidthSlider.addEventListener("input", () => {
274
+ barWidthValue.textContent = `${barWidthSlider.value}px`;
275
+ });
276
+ });
277
+
278
+ // Close modal when clicking outside
279
+ window.onclick = function (event) {
280
+ const modal = document.getElementById("requestsModal");
281
+ if (event.target === modal) {
282
+ closeModal();
283
+ }
284
+ };
public/style.css ADDED
@@ -0,0 +1,489 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary: #4f46e5;
3
+ --success: #10b981;
4
+ --secondary: #64748b;
5
+ --bg: #f8fafc;
6
+ --text: #1e293b;
7
+ --panel-bg: #ffffff;
8
+ --border: #e2e8f0;
9
+ }
10
+
11
+ [data-theme="dark"] {
12
+ --primary: #818cf8;
13
+ --success: #34d399;
14
+ --secondary: #94a3b8;
15
+ --bg: #0f172a;
16
+ --text: #e2e8f0;
17
+ --panel-bg: #1e293b;
18
+ --border: #334155;
19
+ }
20
+
21
+ * {
22
+ margin: 0;
23
+ padding: 0;
24
+ box-sizing: border-box;
25
+ font-family: system-ui, -apple-system, sans-serif;
26
+ }
27
+
28
+ body {
29
+ background: var(--bg);
30
+ min-height: 100vh;
31
+ padding: 2rem;
32
+ color: var(--text);
33
+ transition: background-color 0.3s, color 0.3s;
34
+ }
35
+
36
+ .theme-toggle {
37
+ position: fixed;
38
+ top: 2rem;
39
+ right: 2rem;
40
+ background: var(--panel-bg);
41
+ border: 1px solid var(--border);
42
+ width: 3rem;
43
+ height: 3rem;
44
+ border-radius: 50%;
45
+ cursor: pointer;
46
+ display: flex;
47
+ align-items: center;
48
+ justify-content: center;
49
+ font-size: 1.5rem;
50
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
51
+ transition: transform 0.3s ease;
52
+ z-index: 100;
53
+ }
54
+
55
+ .theme-toggle:hover {
56
+ transform: scale(1.1);
57
+ }
58
+
59
+ .container {
60
+ max-width: 1400px;
61
+ margin: 0 auto;
62
+ display: grid;
63
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
64
+ gap: 2rem;
65
+ }
66
+
67
+ .panel {
68
+ background: var(--panel-bg);
69
+ padding: 2rem;
70
+ border-radius: 1rem;
71
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
72
+ transition: background-color 0.3s;
73
+ }
74
+
75
+ h2 {
76
+ color: var(--text);
77
+ margin-bottom: 1.5rem;
78
+ font-size: 1.5rem;
79
+ }
80
+
81
+ .form-group {
82
+ margin-bottom: 1.25rem;
83
+ }
84
+
85
+ label {
86
+ display: block;
87
+ margin-bottom: 0.5rem;
88
+ font-weight: 500;
89
+ color: var(--text);
90
+ }
91
+
92
+ input,
93
+ textarea,
94
+ select {
95
+ width: 100%;
96
+ padding: 0.75rem;
97
+ border: 1px solid var(--border);
98
+ border-radius: 0.5rem;
99
+ font-size: 0.95rem;
100
+ background: var(--panel-bg);
101
+ color: var(--text);
102
+ transition: all 0.15s;
103
+ }
104
+
105
+ input:focus,
106
+ textarea:focus,
107
+ select:focus {
108
+ outline: none;
109
+ border-color: var(--primary);
110
+ box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
111
+ }
112
+
113
+ textarea {
114
+ height: 120px;
115
+ resize: vertical;
116
+ line-height: 1.5;
117
+ }
118
+
119
+ /* Font section styling */
120
+ .font-section {
121
+ border: 1px solid var(--border);
122
+ border-radius: 0.75rem;
123
+ padding: 1.5rem;
124
+ margin-bottom: 1.5rem;
125
+ background: var(--bg);
126
+ }
127
+
128
+ .font-controls {
129
+ margin-bottom: 1.25rem;
130
+ }
131
+
132
+ .font-controls:last-child {
133
+ margin-bottom: 0;
134
+ }
135
+
136
+ .font-selectors {
137
+ display: grid;
138
+ grid-template-columns: 2fr 1fr;
139
+ gap: 0.75rem;
140
+ }
141
+
142
+ .font-select {
143
+ -webkit-appearance: none;
144
+ -moz-appearance: none;
145
+ appearance: none;
146
+ background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
147
+ background-repeat: no-repeat;
148
+ background-position: right 0.75rem center;
149
+ background-size: 1em;
150
+ padding-right: 2.5rem;
151
+ }
152
+
153
+ .font-select::-ms-expand {
154
+ display: none;
155
+ }
156
+
157
+ /* Color picker styling */
158
+ .color-grid {
159
+ display: grid;
160
+ grid-template-columns: repeat(2, 1fr);
161
+ gap: 1rem;
162
+ margin-bottom: 1.5rem;
163
+ }
164
+
165
+ .color-picker {
166
+ position: relative;
167
+ }
168
+
169
+ .color-input-wrapper {
170
+ display: flex;
171
+ align-items: center;
172
+ gap: 0.75rem;
173
+ }
174
+
175
+ input[type="color"] {
176
+ -webkit-appearance: none;
177
+ -moz-appearance: none;
178
+ appearance: none;
179
+ width: 48px;
180
+ height: 48px;
181
+ padding: 0;
182
+ border: none;
183
+ border-radius: 0.5rem;
184
+ cursor: pointer;
185
+ }
186
+
187
+ input[type="color"]::-webkit-color-swatch-wrapper {
188
+ padding: 0;
189
+ }
190
+
191
+ input[type="color"]::-webkit-color-swatch {
192
+ border: none;
193
+ border-radius: 0.5rem;
194
+ box-shadow: inset 0 0 0 1px var(--border);
195
+ }
196
+
197
+ input[type="color"]::-moz-color-swatch {
198
+ border: none;
199
+ border-radius: 0.5rem;
200
+ box-shadow: inset 0 0 0 1px var(--border);
201
+ }
202
+
203
+ .color-text {
204
+ flex: 1;
205
+ padding: 0.75rem;
206
+ border: 1px solid var(--border);
207
+ border-radius: 0.5rem;
208
+ font-family: monospace;
209
+ font-size: 0.95rem;
210
+ text-transform: uppercase;
211
+ }
212
+
213
+ /* Slider styling */
214
+ .slider {
215
+ -webkit-appearance: none;
216
+ -moz-appearance: none;
217
+ appearance: none;
218
+ width: 100%;
219
+ height: 6px;
220
+ border-radius: 3px;
221
+ background: var(--border);
222
+ outline: none;
223
+ margin-top: 0.5rem;
224
+ }
225
+
226
+ .slider::-webkit-slider-thumb {
227
+ -webkit-appearance: none;
228
+ -moz-appearance: none;
229
+ appearance: none;
230
+ width: 18px;
231
+ height: 18px;
232
+ border-radius: 50%;
233
+ background: var(--primary);
234
+ cursor: pointer;
235
+ transition: transform 0.2s;
236
+ }
237
+
238
+ .slider::-webkit-slider-thumb:hover {
239
+ transform: scale(1.1);
240
+ }
241
+
242
+ .slider::-moz-range-thumb {
243
+ width: 18px;
244
+ height: 18px;
245
+ border-radius: 50%;
246
+ background: var(--primary);
247
+ cursor: pointer;
248
+ border: none;
249
+ transition: transform 0.2s;
250
+ }
251
+
252
+ .slider::-moz-range-thumb:hover {
253
+ transform: scale(1.1);
254
+ }
255
+
256
+ /* Button styling */
257
+ .btn-group {
258
+ display: flex;
259
+ gap: 0.75rem;
260
+ flex-wrap: wrap;
261
+ }
262
+
263
+ .btn {
264
+ padding: 0.75rem 1.5rem;
265
+ border: none;
266
+ border-radius: 0.5rem;
267
+ font-weight: 500;
268
+ cursor: pointer;
269
+ transition: all 0.2s;
270
+ display: inline-flex;
271
+ align-items: center;
272
+ gap: 0.5rem;
273
+ min-width: 120px;
274
+ justify-content: center;
275
+ }
276
+
277
+ .btn:hover {
278
+ transform: translateY(-2px);
279
+ }
280
+
281
+ .btn:disabled {
282
+ opacity: 0.7;
283
+ cursor: not-allowed;
284
+ transform: none;
285
+ }
286
+
287
+ .btn-primary {
288
+ background: var(--primary);
289
+ color: white;
290
+ }
291
+ .btn-success {
292
+ background: var(--success);
293
+ color: white;
294
+ }
295
+ .btn-secondary {
296
+ background: var(--secondary);
297
+ color: white;
298
+ }
299
+
300
+ /* Preview container */
301
+ #preview-container {
302
+ width: 100%;
303
+ aspect-ratio: 16/9;
304
+ border-radius: 0.75rem;
305
+ overflow: hidden;
306
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
307
+ background: var(--panel-bg);
308
+ position: relative;
309
+ }
310
+
311
+ #quoteImage {
312
+ width: 100%;
313
+ height: 100%;
314
+ object-fit: contain;
315
+ transition: opacity 0.3s ease;
316
+ }
317
+
318
+ /* Loading states */
319
+ .loading {
320
+ position: absolute;
321
+ inset: 0;
322
+ display: none;
323
+ align-items: center;
324
+ justify-content: center;
325
+ background: rgba(0, 0, 0, 0.5);
326
+ backdrop-filter: blur(4px);
327
+ border-radius: 0.75rem;
328
+ }
329
+
330
+ .loading-content {
331
+ display: flex;
332
+ flex-direction: column;
333
+ align-items: center;
334
+ gap: 1rem;
335
+ color: white;
336
+ }
337
+
338
+ .loading-spinner {
339
+ width: 40px;
340
+ height: 40px;
341
+ border: 3px solid rgba(255, 255, 255, 0.3);
342
+ border-radius: 50%;
343
+ border-top-color: white;
344
+ animation: spin 1s linear infinite;
345
+ }
346
+
347
+ .btn-loader {
348
+ display: none;
349
+ width: 20px;
350
+ height: 20px;
351
+ border: 2px solid #ffffff;
352
+ border-radius: 50%;
353
+ border-top-color: transparent;
354
+ animation: spin 1s linear infinite;
355
+ }
356
+
357
+ @keyframes spin {
358
+ to {
359
+ transform: rotate(360deg);
360
+ }
361
+ }
362
+
363
+ /* Modal styling */
364
+ .modal {
365
+ display: none;
366
+ position: fixed;
367
+ inset: 0;
368
+ background: rgba(0, 0, 0, 0.5);
369
+ backdrop-filter: blur(4px);
370
+ z-index: 1000;
371
+ }
372
+
373
+ .modal-content {
374
+ position: absolute;
375
+ top: 50%;
376
+ left: 50%;
377
+ transform: translate(-50%, -50%);
378
+ background: var(--panel-bg);
379
+ padding: 2rem;
380
+ border-radius: 1rem;
381
+ max-width: 600px;
382
+ width: 90%;
383
+ max-height: 80vh;
384
+ overflow-y: auto;
385
+ }
386
+
387
+ .close-btn {
388
+ position: absolute;
389
+ right: 1.5rem;
390
+ top: 1.5rem;
391
+ font-size: 1.5rem;
392
+ cursor: pointer;
393
+ opacity: 0.7;
394
+ transition: opacity 0.2s;
395
+ color: var(--text);
396
+ }
397
+
398
+ .close-btn:hover {
399
+ opacity: 1;
400
+ }
401
+
402
+ /* Request history styling */
403
+ .requests-list {
404
+ margin-top: 1rem;
405
+ }
406
+
407
+ .request-item {
408
+ background: var(--bg);
409
+ border: 1px solid var(--border);
410
+ border-radius: 0.5rem;
411
+ padding: 1rem;
412
+ margin-bottom: 1rem;
413
+ }
414
+
415
+ .request-time {
416
+ font-size: 0.875rem;
417
+ color: var(--secondary);
418
+ margin-bottom: 0.5rem;
419
+ }
420
+
421
+ .request-data {
422
+ font-family: monospace;
423
+ font-size: 0.875rem;
424
+ white-space: pre-wrap;
425
+ overflow-x: auto;
426
+ color: var(--text);
427
+ }
428
+
429
+ /* Status message */
430
+ #status {
431
+ position: fixed;
432
+ bottom: 2rem;
433
+ right: 2rem;
434
+ padding: 1rem 1.5rem;
435
+ border-radius: 0.5rem;
436
+ background: var(--panel-bg);
437
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
438
+ display: none;
439
+ animation: slideIn 0.3s ease-out;
440
+ z-index: 100;
441
+ }
442
+
443
+ @keyframes slideIn {
444
+ from {
445
+ transform: translateY(100%);
446
+ opacity: 0;
447
+ }
448
+ to {
449
+ transform: translateY(0);
450
+ opacity: 1;
451
+ }
452
+ }
453
+
454
+ /* Responsive design */
455
+ @media (max-width: 768px) {
456
+ body {
457
+ padding: 1rem;
458
+ }
459
+
460
+ .container {
461
+ grid-template-columns: 1fr;
462
+ }
463
+
464
+ .color-grid {
465
+ grid-template-columns: 1fr;
466
+ }
467
+
468
+ .font-selectors {
469
+ grid-template-columns: 1fr;
470
+ }
471
+
472
+ .btn-group {
473
+ flex-direction: column;
474
+ }
475
+
476
+ .btn {
477
+ width: 100%;
478
+ }
479
+
480
+ .theme-toggle {
481
+ top: 1rem;
482
+ right: 1rem;
483
+ }
484
+
485
+ .modal-content {
486
+ width: calc(100% - 2rem);
487
+ margin: 1rem;
488
+ }
489
+ }