@jjlmoya/utils-tools 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/package.json +63 -0
  2. package/src/category/i18n/en.ts +172 -0
  3. package/src/category/i18n/es.ts +172 -0
  4. package/src/category/i18n/fr.ts +172 -0
  5. package/src/category/index.ts +23 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +90 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/schemas_fulfillment.test.ts +23 -0
  21. package/src/tests/seo_length.test.ts +23 -0
  22. package/src/tests/title_quality.test.ts +56 -0
  23. package/src/tests/tool_validation.test.ts +17 -0
  24. package/src/tool/date-diff-calculator/bibliography.astro +14 -0
  25. package/src/tool/date-diff-calculator/component.astro +370 -0
  26. package/src/tool/date-diff-calculator/i18n/en.ts +132 -0
  27. package/src/tool/date-diff-calculator/i18n/es.ts +132 -0
  28. package/src/tool/date-diff-calculator/i18n/fr.ts +132 -0
  29. package/src/tool/date-diff-calculator/index.ts +22 -0
  30. package/src/tool/date-diff-calculator/seo.astro +14 -0
  31. package/src/tool/date-diff-calculator/ui.ts +17 -0
  32. package/src/tool/drive-direct-link/bibliography.astro +14 -0
  33. package/src/tool/drive-direct-link/component.astro +280 -0
  34. package/src/tool/drive-direct-link/i18n/en.ts +118 -0
  35. package/src/tool/drive-direct-link/i18n/es.ts +118 -0
  36. package/src/tool/drive-direct-link/i18n/fr.ts +118 -0
  37. package/src/tool/drive-direct-link/index.ts +22 -0
  38. package/src/tool/drive-direct-link/seo.astro +14 -0
  39. package/src/tool/drive-direct-link/ui.ts +10 -0
  40. package/src/tool/email-list-cleaner/bibliography.astro +14 -0
  41. package/src/tool/email-list-cleaner/component.astro +375 -0
  42. package/src/tool/email-list-cleaner/i18n/en.ts +140 -0
  43. package/src/tool/email-list-cleaner/i18n/es.ts +140 -0
  44. package/src/tool/email-list-cleaner/i18n/fr.ts +140 -0
  45. package/src/tool/email-list-cleaner/index.ts +22 -0
  46. package/src/tool/email-list-cleaner/seo.astro +14 -0
  47. package/src/tool/email-list-cleaner/ui.ts +15 -0
  48. package/src/tool/env-badge-spain/bibliography.astro +14 -0
  49. package/src/tool/env-badge-spain/component.astro +303 -0
  50. package/src/tool/env-badge-spain/components/BadgeForm.astro +243 -0
  51. package/src/tool/env-badge-spain/components/BadgeResult.astro +151 -0
  52. package/src/tool/env-badge-spain/i18n/en.ts +153 -0
  53. package/src/tool/env-badge-spain/i18n/es.ts +153 -0
  54. package/src/tool/env-badge-spain/i18n/fr.ts +153 -0
  55. package/src/tool/env-badge-spain/index.ts +22 -0
  56. package/src/tool/env-badge-spain/seo.astro +14 -0
  57. package/src/tool/env-badge-spain/ui.ts +53 -0
  58. package/src/tool/morse-beacon/bibliography.astro +14 -0
  59. package/src/tool/morse-beacon/component.astro +534 -0
  60. package/src/tool/morse-beacon/i18n/en.ts +157 -0
  61. package/src/tool/morse-beacon/i18n/es.ts +157 -0
  62. package/src/tool/morse-beacon/i18n/fr.ts +157 -0
  63. package/src/tool/morse-beacon/index.ts +22 -0
  64. package/src/tool/morse-beacon/logic/MorseEngine.ts +124 -0
  65. package/src/tool/morse-beacon/seo.astro +14 -0
  66. package/src/tool/morse-beacon/ui.ts +18 -0
  67. package/src/tool/password-generator/bibliography.astro +14 -0
  68. package/src/tool/password-generator/component.astro +259 -0
  69. package/src/tool/password-generator/components/Config.astro +227 -0
  70. package/src/tool/password-generator/components/Display.astro +147 -0
  71. package/src/tool/password-generator/components/Strength.astro +70 -0
  72. package/src/tool/password-generator/i18n/en.ts +166 -0
  73. package/src/tool/password-generator/i18n/es.ts +166 -0
  74. package/src/tool/password-generator/i18n/fr.ts +166 -0
  75. package/src/tool/password-generator/index.ts +22 -0
  76. package/src/tool/password-generator/seo.astro +14 -0
  77. package/src/tool/password-generator/ui.ts +16 -0
  78. package/src/tool/routes/bibliography.astro +14 -0
  79. package/src/tool/routes/component.astro +543 -0
  80. package/src/tool/routes/i18n/en.ts +157 -0
  81. package/src/tool/routes/i18n/es.ts +157 -0
  82. package/src/tool/routes/i18n/fr.ts +157 -0
  83. package/src/tool/routes/index.ts +22 -0
  84. package/src/tool/routes/logic/GeocodingService.ts +60 -0
  85. package/src/tool/routes/logic/RouteManager.ts +192 -0
  86. package/src/tool/routes/logic/RouteService.ts +66 -0
  87. package/src/tool/routes/seo.astro +14 -0
  88. package/src/tool/routes/ui.ts +16 -0
  89. package/src/tool/rule-of-three/bibliography.astro +14 -0
  90. package/src/tool/rule-of-three/component.astro +369 -0
  91. package/src/tool/rule-of-three/i18n/en.ts +171 -0
  92. package/src/tool/rule-of-three/i18n/es.ts +171 -0
  93. package/src/tool/rule-of-three/i18n/fr.ts +171 -0
  94. package/src/tool/rule-of-three/index.ts +22 -0
  95. package/src/tool/rule-of-three/seo.astro +14 -0
  96. package/src/tool/rule-of-three/ui.ts +13 -0
  97. package/src/tool/seo-content-optimizer/bibliography.astro +14 -0
  98. package/src/tool/seo-content-optimizer/component.astro +552 -0
  99. package/src/tool/seo-content-optimizer/i18n/en.ts +136 -0
  100. package/src/tool/seo-content-optimizer/i18n/es.ts +136 -0
  101. package/src/tool/seo-content-optimizer/i18n/fr.ts +136 -0
  102. package/src/tool/seo-content-optimizer/index.ts +22 -0
  103. package/src/tool/seo-content-optimizer/seo.astro +14 -0
  104. package/src/tool/seo-content-optimizer/ui.ts +29 -0
  105. package/src/tool/speed-reader/bibliography.astro +14 -0
  106. package/src/tool/speed-reader/component.astro +586 -0
  107. package/src/tool/speed-reader/i18n/en.ts +152 -0
  108. package/src/tool/speed-reader/i18n/es.ts +152 -0
  109. package/src/tool/speed-reader/i18n/fr.ts +152 -0
  110. package/src/tool/speed-reader/index.ts +22 -0
  111. package/src/tool/speed-reader/logic/RSVPEngine.ts +106 -0
  112. package/src/tool/speed-reader/seo.astro +14 -0
  113. package/src/tool/speed-reader/ui.ts +14 -0
  114. package/src/tool/text-pixel-calculator/bibliography.astro +14 -0
  115. package/src/tool/text-pixel-calculator/component.astro +315 -0
  116. package/src/tool/text-pixel-calculator/components/Editor.astro +240 -0
  117. package/src/tool/text-pixel-calculator/components/Preview.astro +155 -0
  118. package/src/tool/text-pixel-calculator/components/Stats.astro +87 -0
  119. package/src/tool/text-pixel-calculator/i18n/en.ts +133 -0
  120. package/src/tool/text-pixel-calculator/i18n/es.ts +133 -0
  121. package/src/tool/text-pixel-calculator/i18n/fr.ts +133 -0
  122. package/src/tool/text-pixel-calculator/index.ts +22 -0
  123. package/src/tool/text-pixel-calculator/seo.astro +14 -0
  124. package/src/tool/text-pixel-calculator/ui.ts +15 -0
  125. package/src/tool/whatsapp-link/bibliography.astro +14 -0
  126. package/src/tool/whatsapp-link/component.astro +455 -0
  127. package/src/tool/whatsapp-link/i18n/en.ts +128 -0
  128. package/src/tool/whatsapp-link/i18n/es.ts +128 -0
  129. package/src/tool/whatsapp-link/i18n/fr.ts +128 -0
  130. package/src/tool/whatsapp-link/index.ts +22 -0
  131. package/src/tool/whatsapp-link/seo.astro +14 -0
  132. package/src/tool/whatsapp-link/ui.ts +15 -0
  133. package/src/tools.ts +15 -0
  134. package/src/types.ts +72 -0
@@ -0,0 +1,315 @@
1
+ ---
2
+ import type { TextPixelCalculatorUI } from './ui';
3
+ import Editor from './components/Editor.astro';
4
+ import Stats from './components/Stats.astro';
5
+ import Preview from './components/Preview.astro';
6
+
7
+ interface Props {
8
+ ui?: Record<string, unknown>;
9
+ }
10
+
11
+ const { ui } = Astro.props;
12
+ const t = (ui ?? {}) as TextPixelCalculatorUI;
13
+ ---
14
+
15
+ <div class="tpc-root" data-ui={JSON.stringify(t)}>
16
+ <div class="tpc-main-card">
17
+ <div class="tpc-left">
18
+ <Editor
19
+ textLabel={t.textLabel}
20
+ textPlaceholder={t.textPlaceholder}
21
+ fontLabel={t.fontLabel}
22
+ sizeLabel={t.sizeLabel}
23
+ weightLabel={t.weightLabel}
24
+ italicLabel={t.italicLabel}
25
+ />
26
+ <Stats
27
+ widthLabel={t.widthLabel}
28
+ charsLabel={t.charsLabel}
29
+ />
30
+ </div>
31
+
32
+ <div class="tpc-right">
33
+ <Preview
34
+ previewLabel={t.previewLabel}
35
+ copyBtn={t.copyBtn}
36
+ resetBtn={t.resetBtn}
37
+ />
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ <div class="tpc-toast" id="tpc-toast">{t.copyDone}</div>
43
+
44
+ <style>
45
+ .tpc-root {
46
+ --tpc-cyan: #06b6d4;
47
+ --tpc-cyan-dark: #0891b2;
48
+ --tpc-cyan-focus: rgba(6, 182, 212, 0.15);
49
+ --tpc-purple: #8b5cf6;
50
+ --tpc-card-bg: #fff;
51
+ --tpc-card-border: rgba(0, 0, 0, 0.05);
52
+ --tpc-field-bg: #f8fafc;
53
+ --tpc-field-border: #e2e8f0;
54
+ --tpc-text-main: #1e293b;
55
+ --tpc-text-label: #64748b;
56
+ --tpc-canvas-bg: #f1f5f9;
57
+ --tpc-btn-secondary-bg: #f1f5f9;
58
+ --tpc-btn-secondary-color: #475569;
59
+ --tpc-checkbox-border: #cbd5e1;
60
+ --tpc-checkbox-bg: #fff;
61
+ --tpc-toast-bg: #1e293b;
62
+
63
+ width: 100%;
64
+ max-width: 1000px;
65
+ margin: 0 auto;
66
+ }
67
+
68
+ :global(.theme-dark) .tpc-root {
69
+ --tpc-card-bg: #1e293b;
70
+ --tpc-card-border: rgba(255, 255, 255, 0.05);
71
+ --tpc-field-bg: #0f172a;
72
+ --tpc-field-border: #334155;
73
+ --tpc-text-main: #f1f5f9;
74
+ --tpc-text-label: #94a3b8;
75
+ --tpc-canvas-bg: #0f172a;
76
+ --tpc-btn-secondary-bg: #334155;
77
+ --tpc-btn-secondary-color: #e2e8f0;
78
+ --tpc-checkbox-border: #475569;
79
+ --tpc-checkbox-bg: #0f172a;
80
+ --tpc-toast-bg: #06b6d4;
81
+ }
82
+
83
+ .tpc-main-card {
84
+ background: var(--tpc-card-bg);
85
+ border: 1px solid var(--tpc-card-border);
86
+ border-radius: 2.5rem;
87
+ padding: 2.5rem;
88
+ display: flex;
89
+ gap: 3rem;
90
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.1);
91
+ }
92
+
93
+ .tpc-left {
94
+ flex: 1.2;
95
+ display: flex;
96
+ flex-direction: column;
97
+ }
98
+
99
+ .tpc-right {
100
+ flex: 1;
101
+ border-left: 1px solid var(--tpc-field-border);
102
+ padding-left: 3rem;
103
+ }
104
+
105
+ @media (max-width: 900px) {
106
+ .tpc-main-card {
107
+ flex-direction: column;
108
+ padding: 1.5rem;
109
+ gap: 2rem;
110
+ }
111
+ .tpc-right {
112
+ border-left: none;
113
+ padding-left: 0;
114
+ border-top: 1px solid var(--tpc-field-border);
115
+ padding-top: 2rem;
116
+ }
117
+ }
118
+
119
+ .tpc-toast {
120
+ position: fixed;
121
+ bottom: 2rem;
122
+ left: 50%;
123
+ transform: translateX(-50%) translateY(100px);
124
+ background: var(--tpc-toast-bg);
125
+ color: #fff;
126
+ padding: 1rem 2rem;
127
+ border-radius: 100px;
128
+ font-size: 0.875rem;
129
+ font-weight: 700;
130
+ transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
131
+ z-index: 1000;
132
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
133
+ pointer-events: none;
134
+ }
135
+
136
+ .tpc-toast-show {
137
+ transform: translateX(-50%) translateY(0);
138
+ }
139
+ </style>
140
+
141
+ <script>
142
+ import type { TextPixelCalculatorUI } from './ui';
143
+
144
+ const root = document.querySelector('.tpc-root') as HTMLElement | null;
145
+ const t = JSON.parse(root?.dataset.ui ?? '{}') as TextPixelCalculatorUI;
146
+
147
+ const textInput = document.getElementById('tpc-text') as HTMLTextAreaElement | null;
148
+ const fontSelect = document.getElementById('tpc-font') as HTMLSelectElement | null;
149
+ const sizeInput = document.getElementById('tpc-size') as HTMLInputElement | null;
150
+ const weightSelect = document.getElementById('tpc-weight') as HTMLSelectElement | null;
151
+ const italicCheck = document.getElementById('tpc-italic') as HTMLInputElement | null;
152
+ const italicLabel = italicCheck?.closest('label');
153
+ const widthSpan = document.getElementById('tpc-width');
154
+ const charsSpan = document.getElementById('tpc-chars');
155
+ const canvas = document.getElementById('tpc-canvas') as HTMLCanvasElement | null;
156
+ const copyBtn = document.getElementById('tpc-copy');
157
+ const resetBtn = document.getElementById('tpc-reset');
158
+ const toast = document.getElementById('tpc-toast');
159
+
160
+ const STORAGE_KEY = 'tpc_preferences';
161
+ const systemFonts = ["system-ui", "serif", "sans-serif", "monospace", "cursive", "fantasy"];
162
+ const loadedFonts = new Set<string>();
163
+
164
+ function saveSettings() {
165
+ const settings = {
166
+ text: textInput?.value,
167
+ font: fontSelect?.value,
168
+ size: sizeInput?.value,
169
+ weight: weightSelect?.value,
170
+ italic: italicCheck?.checked
171
+ };
172
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
173
+ }
174
+
175
+ function loadSettings() {
176
+ const saved = localStorage.getItem(STORAGE_KEY);
177
+ if (!saved) return;
178
+ try {
179
+ const settings = JSON.parse(saved);
180
+ if (textInput && settings.text !== undefined) textInput.value = settings.text;
181
+ if (fontSelect && settings.font !== undefined) fontSelect.value = settings.font;
182
+ if (sizeInput && settings.size !== undefined) sizeInput.value = settings.size;
183
+ if (weightSelect && settings.weight !== undefined) weightSelect.value = settings.weight;
184
+ if (italicCheck && settings.italic !== undefined) {
185
+ italicCheck.checked = settings.italic;
186
+ italicLabel?.classList.toggle('tpc-checked', italicCheck.checked);
187
+ }
188
+ } catch {}
189
+ }
190
+
191
+ const gFontsHost = ['fonts', 'googleapis', 'com'].join('.');
192
+
193
+ function loadGoogleFont(family: string) {
194
+ if (!family || systemFonts.includes(family) || loadedFonts.has(family)) return;
195
+
196
+ const link = document.createElement('link');
197
+ link.rel = 'stylesheet';
198
+ link.href = `https://${gFontsHost}/css2?family=${family.replace(/ /g, '+')}:wght@100;200;300;400;500;600;700;800;900&display=swap`;
199
+ link.onload = () => {
200
+ loadedFonts.add(family);
201
+ document.fonts.ready.then(() => measure());
202
+ };
203
+ document.head.appendChild(link);
204
+ }
205
+
206
+ function buildFont(): string {
207
+ const style = italicCheck?.checked ? 'italic ' : '';
208
+ const weight = weightSelect?.value ?? '400';
209
+ const size = sizeInput?.value || '16';
210
+ const family = fontSelect?.value ?? 'sans-serif';
211
+ return `${style}${weight} ${size}px "${family}", sans-serif`;
212
+ }
213
+
214
+ function measure(): void {
215
+ if (!textInput || !canvas) return;
216
+ const text = textInput.value;
217
+ const font = buildFont();
218
+ const ctx = canvas.getContext('2d');
219
+ if (!ctx) return;
220
+
221
+ ctx.font = font;
222
+ const metrics = ctx.measureText(text);
223
+ const px = Math.round(metrics.width * 100) / 100;
224
+
225
+ if (widthSpan) widthSpan.textContent = String(px);
226
+ if (charsSpan) charsSpan.textContent = String(text.length);
227
+
228
+ drawPreview(text, font, px, ctx);
229
+ saveSettings();
230
+ }
231
+
232
+ function drawPreview(text: string, font: string, width: number, ctx: CanvasRenderingContext2D): void {
233
+ if (!canvas) return;
234
+ const wrap = canvas.parentElement;
235
+ if (wrap) {
236
+ canvas.width = wrap.clientWidth;
237
+ canvas.height = wrap.clientHeight || 150;
238
+ }
239
+
240
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
241
+ const isDark = document.documentElement.classList.contains('theme-dark');
242
+
243
+ ctx.fillStyle = isDark ? '#f1f5f9' : '#0f172a';
244
+ ctx.font = font;
245
+ ctx.textBaseline = 'middle';
246
+
247
+ const x = 20;
248
+ const y = canvas.height / 2;
249
+ ctx.fillText(text, x, y);
250
+
251
+ ctx.strokeStyle = '#06b6d4';
252
+ ctx.setLineDash([5, 5]);
253
+ ctx.lineWidth = 2;
254
+ ctx.beginPath();
255
+ ctx.moveTo(x, y + 40);
256
+ ctx.lineTo(x + width, y + 40);
257
+ ctx.stroke();
258
+
259
+ ctx.setLineDash([]);
260
+ ctx.lineWidth = 2;
261
+ ctx.beginPath();
262
+ ctx.moveTo(x, y + 35);
263
+ ctx.lineTo(x, y + 45);
264
+ ctx.stroke();
265
+ ctx.beginPath();
266
+ ctx.moveTo(x + width, y + 35);
267
+ ctx.lineTo(x + width, y + 45);
268
+ ctx.stroke();
269
+ }
270
+
271
+ function showToast(): void {
272
+ if (!toast) return;
273
+ toast.classList.add('tpc-toast-show');
274
+ setTimeout(() => { toast.classList.remove('tpc-toast-show'); }, 2000);
275
+ }
276
+
277
+ italicCheck?.addEventListener('change', () => {
278
+ italicLabel?.classList.toggle('tpc-checked', italicCheck.checked);
279
+ measure();
280
+ });
281
+
282
+ fontSelect?.addEventListener('change', () => {
283
+ if (fontSelect.value) loadGoogleFont(fontSelect.value);
284
+ measure();
285
+ });
286
+
287
+ [textInput, sizeInput, weightSelect].forEach((el) => {
288
+ el?.addEventListener('input', measure);
289
+ });
290
+
291
+ window.addEventListener('resize', measure);
292
+
293
+ copyBtn?.addEventListener('click', () => {
294
+ const text = widthSpan?.textContent ?? '0';
295
+ navigator.clipboard.writeText(text).then(showToast).catch(() => {});
296
+ });
297
+
298
+ resetBtn?.addEventListener('click', () => {
299
+ if (textInput) textInput.value = t.defaultText ?? '';
300
+ if (fontSelect) fontSelect.value = 'Inter';
301
+ if (sizeInput) sizeInput.value = '16';
302
+ if (weightSelect) weightSelect.value = '400';
303
+ if (italicCheck) {
304
+ italicCheck.checked = false;
305
+ italicLabel?.classList.remove('tpc-checked');
306
+ }
307
+ localStorage.removeItem(STORAGE_KEY);
308
+ measure();
309
+ });
310
+
311
+ loadSettings();
312
+ if (textInput && !textInput.value) textInput.value = t.defaultText ?? '';
313
+ if (fontSelect?.value) loadGoogleFont(fontSelect.value);
314
+ measure();
315
+ </script>
@@ -0,0 +1,240 @@
1
+ ---
2
+ interface Props {
3
+ textLabel: string;
4
+ textPlaceholder: string;
5
+ fontLabel: string;
6
+ sizeLabel: string;
7
+ weightLabel: string;
8
+ italicLabel: string;
9
+ }
10
+
11
+ const { textLabel, textPlaceholder, fontLabel, sizeLabel, weightLabel, italicLabel } = Astro.props;
12
+
13
+ const googleFonts = [
14
+ "Inter", "Roboto", "Open Sans", "Montserrat", "Lato", "Poppins",
15
+ "Playfair Display", "Oswald", "Raleway", "Ubuntu", "Merriweather",
16
+ "Lora", "Nunito", "Rubik", "Kanit", "Work Sans", "DM Sans",
17
+ "Fira Sans", "Quicksand", "Mulish", "Cabin", "Jost", "Titillium Web",
18
+ "PT Sans", "Source Sans 3", "Manrope", "Heebo", "IBM Plex Sans",
19
+ "Outfit", "Space Grotesk", "Dancing Script", "Pacifico", "Caveat",
20
+ "Indie Flower", "Shadows Into Light", "Abril Fatface", "Cinzel",
21
+ "Bebas Neue", "Anton", "Archivo", "Lexend", "EB Garamond"
22
+ ].sort();
23
+
24
+ const systemFonts = [
25
+ "system-ui", "serif", "sans-serif", "monospace", "cursive", "fantasy"
26
+ ];
27
+ ---
28
+
29
+ <div class="tpc-editor">
30
+ <div class="tpc-field">
31
+ <label class="tpc-label" for="tpc-text">{textLabel}</label>
32
+ <textarea
33
+ id="tpc-text"
34
+ class="tpc-textarea"
35
+ placeholder={textPlaceholder}
36
+ spellcheck="false"
37
+ rows="4"
38
+ ></textarea>
39
+ </div>
40
+
41
+ <div class="tpc-config-grid">
42
+ <div class="tpc-field">
43
+ <label class="tpc-label" for="tpc-font">{fontLabel}</label>
44
+ <div class="tpc-select-wrapper">
45
+ <select id="tpc-font" class="tpc-input">
46
+ <optgroup label="System Fonts">
47
+ {systemFonts.map(font => <option value={font}>{font}</option>)}
48
+ </optgroup>
49
+ <optgroup label="Google Fonts">
50
+ {googleFonts.map(font => <option value={font} selected={font === "Inter"}>{font}</option>)}
51
+ </optgroup>
52
+ </select>
53
+ <div class="tpc-select-arrow">
54
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M6 9l6 6 6-6"/></svg>
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ <div class="tpc-field">
60
+ <label class="tpc-label" for="tpc-size">{sizeLabel}</label>
61
+ <input
62
+ type="number"
63
+ id="tpc-size"
64
+ class="tpc-input"
65
+ value="16"
66
+ min="1"
67
+ max="500"
68
+ />
69
+ </div>
70
+
71
+ <div class="tpc-field">
72
+ <label class="tpc-label" for="tpc-weight">{weightLabel}</label>
73
+ <select id="tpc-weight" class="tpc-input">
74
+ <option value="100">100 Thin</option>
75
+ <option value="200">200 ExtraLight</option>
76
+ <option value="300">300 Light</option>
77
+ <option value="400" selected>400 Regular</option>
78
+ <option value="500">500 Medium</option>
79
+ <option value="600">600 SemiBold</option>
80
+ <option value="700">700 Bold</option>
81
+ <option value="800">800 ExtraBold</option>
82
+ <option value="900">900 Black</option>
83
+ </select>
84
+ </div>
85
+
86
+ <div class="tpc-field tpc-field-italic">
87
+ <label class="tpc-checkbox-label" for="tpc-italic">
88
+ <input type="checkbox" id="tpc-italic" class="tpc-checkbox-input" />
89
+ <span class="tpc-checkbox-box"></span>
90
+ <span class="tpc-italic-text">{italicLabel}</span>
91
+ </label>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <style>
97
+ .tpc-editor {
98
+ display: flex;
99
+ flex-direction: column;
100
+ gap: 1.5rem;
101
+ }
102
+
103
+ .tpc-field {
104
+ display: flex;
105
+ flex-direction: column;
106
+ gap: 0.625rem;
107
+ }
108
+
109
+ .tpc-label {
110
+ font-size: 0.75rem;
111
+ font-weight: 700;
112
+ color: var(--tpc-text-label);
113
+ text-transform: uppercase;
114
+ letter-spacing: 0.06em;
115
+ }
116
+
117
+ .tpc-textarea {
118
+ width: 100%;
119
+ background: var(--tpc-field-bg);
120
+ border: 1px solid var(--tpc-field-border);
121
+ border-radius: 1rem;
122
+ padding: 1.25rem;
123
+ font-size: 1.125rem;
124
+ color: var(--tpc-text-main);
125
+ line-height: 1.6;
126
+ resize: none;
127
+ outline: none;
128
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
129
+ box-sizing: border-box;
130
+ }
131
+
132
+ .tpc-textarea:focus {
133
+ border-color: var(--tpc-cyan);
134
+ box-shadow: 0 0 0 4px var(--tpc-cyan-focus);
135
+ background: #fff;
136
+ }
137
+
138
+ :global(.theme-dark) .tpc-textarea:focus {
139
+ background: var(--tpc-field-bg);
140
+ }
141
+
142
+ .tpc-config-grid {
143
+ display: grid;
144
+ grid-template-columns: repeat(2, 1fr);
145
+ gap: 1.25rem;
146
+ }
147
+
148
+ .tpc-input {
149
+ width: 100%;
150
+ background: var(--tpc-field-bg);
151
+ border: 1px solid var(--tpc-field-border);
152
+ border-radius: 0.875rem;
153
+ padding: 0.75rem 1rem;
154
+ font-size: 0.9375rem;
155
+ color: var(--tpc-text-main);
156
+ outline: none;
157
+ transition: all 0.2s ease;
158
+ box-sizing: border-box;
159
+ appearance: none;
160
+ }
161
+
162
+ .tpc-input:focus {
163
+ border-color: var(--tpc-cyan);
164
+ box-shadow: 0 0 0 4px var(--tpc-cyan-focus);
165
+ }
166
+
167
+ .tpc-select-wrapper {
168
+ position: relative;
169
+ width: 100%;
170
+ }
171
+
172
+ .tpc-select-arrow {
173
+ position: absolute;
174
+ right: 1rem;
175
+ top: 50%;
176
+ transform: translateY(-50%);
177
+ pointer-events: none;
178
+ color: var(--tpc-text-label);
179
+ width: 1.25rem;
180
+ height: 1.25rem;
181
+ }
182
+
183
+ .tpc-checkbox-label {
184
+ display: flex;
185
+ align-items: center;
186
+ gap: 0.75rem;
187
+ cursor: pointer;
188
+ user-select: none;
189
+ height: 100%;
190
+ padding-top: 1.5rem;
191
+ }
192
+
193
+ .tpc-checkbox-input {
194
+ display: none;
195
+ }
196
+
197
+ .tpc-checkbox-box {
198
+ width: 1.5rem;
199
+ height: 1.5rem;
200
+ border: 2px solid var(--tpc-checkbox-border);
201
+ background: var(--tpc-checkbox-bg);
202
+ border-radius: 0.5rem;
203
+ position: relative;
204
+ flex-shrink: 0;
205
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
206
+ }
207
+
208
+ :global(.tpc-checked) .tpc-checkbox-box {
209
+ background: var(--tpc-cyan);
210
+ border-color: var(--tpc-cyan);
211
+ transform: scale(1.05);
212
+ }
213
+
214
+ :global(.tpc-checked) .tpc-checkbox-box::after {
215
+ content: '';
216
+ position: absolute;
217
+ left: 50%;
218
+ top: 45%;
219
+ width: 6px;
220
+ height: 11px;
221
+ border: solid #fff;
222
+ border-width: 0 2.5px 2.5px 0;
223
+ transform: translate(-50%, -50%) rotate(45deg);
224
+ }
225
+
226
+ .tpc-italic-text {
227
+ font-size: 0.9375rem;
228
+ font-weight: 600;
229
+ color: var(--tpc-text-main);
230
+ }
231
+
232
+ @media (max-width: 480px) {
233
+ .tpc-config-grid {
234
+ grid-template-columns: 1fr;
235
+ }
236
+ .tpc-field-italic {
237
+ padding-top: 0;
238
+ }
239
+ }
240
+ </style>
@@ -0,0 +1,155 @@
1
+ ---
2
+ interface Props {
3
+ previewLabel: string;
4
+ copyBtn: string;
5
+ resetBtn: string;
6
+ }
7
+
8
+ const { previewLabel, copyBtn, resetBtn } = Astro.props;
9
+ ---
10
+
11
+ <div class="tpc-preview-section">
12
+ <div class="tpc-preview-header">
13
+ <span class="tpc-label">{previewLabel}</span>
14
+ </div>
15
+
16
+ <div class="tpc-canvas-container">
17
+ <div class="tpc-canvas-overlay"></div>
18
+ <div class="tpc-canvas-wrap">
19
+ <canvas id="tpc-canvas" class="tpc-canvas"></canvas>
20
+ </div>
21
+ </div>
22
+
23
+ <div class="tpc-actions">
24
+ <button type="button" id="tpc-copy" class="tpc-btn tpc-btn-primary">
25
+ <svg class="tpc-btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
26
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
27
+ <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path>
28
+ </svg>
29
+ <span>{copyBtn}</span>
30
+ </button>
31
+ <button type="button" id="tpc-reset" class="tpc-btn tpc-btn-secondary">
32
+ <svg class="tpc-btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
33
+ <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
34
+ <path d="M3 3v5h5"></path>
35
+ </svg>
36
+ <span>{resetBtn}</span>
37
+ </button>
38
+ </div>
39
+ </div>
40
+
41
+ <style>
42
+ .tpc-preview-section {
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: 1.25rem;
46
+ height: 100%;
47
+ }
48
+
49
+ .tpc-preview-header {
50
+ display: flex;
51
+ justify-content: space-between;
52
+ align-items: center;
53
+ }
54
+
55
+ .tpc-label {
56
+ font-size: 0.75rem;
57
+ font-weight: 700;
58
+ color: var(--tpc-text-label);
59
+ text-transform: uppercase;
60
+ letter-spacing: 0.06em;
61
+ }
62
+
63
+ .tpc-canvas-container {
64
+ position: relative;
65
+ background: var(--tpc-canvas-bg);
66
+ border-radius: 1.25rem;
67
+ border: 1px solid var(--tpc-field-border);
68
+ min-height: 12rem;
69
+ overflow: hidden;
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ }
74
+
75
+ .tpc-canvas-overlay {
76
+ position: absolute;
77
+ inset: 0;
78
+ background-image: radial-gradient(rgba(0,0,0,0.05) 1px, transparent 1px);
79
+ background-size: 20px 20px;
80
+ pointer-events: none;
81
+ }
82
+
83
+ :global(.theme-dark) .tpc-canvas-overlay {
84
+ background-image: radial-gradient(rgba(255,255,255,0.03) 1px, transparent 1px);
85
+ }
86
+
87
+ .tpc-canvas-wrap {
88
+ width: 100%;
89
+ height: 100%;
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: center;
93
+ }
94
+
95
+ .tpc-canvas {
96
+ width: 100%;
97
+ height: 12rem;
98
+ display: block;
99
+ }
100
+
101
+ .tpc-actions {
102
+ display: flex;
103
+ gap: 1rem;
104
+ margin-top: auto;
105
+ }
106
+
107
+ .tpc-btn {
108
+ flex: 1;
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ gap: 0.75rem;
113
+ padding: 1rem;
114
+ border-radius: 1rem;
115
+ font-size: 0.9375rem;
116
+ font-weight: 700;
117
+ cursor: pointer;
118
+ border: none;
119
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
120
+ }
121
+
122
+ .tpc-btn-icon {
123
+ width: 1.25rem;
124
+ height: 1.25rem;
125
+ }
126
+
127
+ .tpc-btn-primary {
128
+ background: linear-gradient(135deg, var(--tpc-cyan) 0%, var(--tpc-cyan-dark) 100%);
129
+ color: #fff;
130
+ box-shadow: 0 4px 12px rgba(6, 182, 212, 0.2);
131
+ }
132
+
133
+ .tpc-btn-primary:hover {
134
+ transform: translateY(-2px);
135
+ box-shadow: 0 8px 20px rgba(6, 182, 212, 0.3);
136
+ }
137
+
138
+ .tpc-btn-primary:active {
139
+ transform: translateY(0);
140
+ }
141
+
142
+ .tpc-btn-secondary {
143
+ background: var(--tpc-btn-secondary-bg);
144
+ color: var(--tpc-btn-secondary-color);
145
+ }
146
+
147
+ .tpc-btn-secondary:hover {
148
+ background: rgba(0, 0, 0, 0.1);
149
+ transform: translateY(-2px);
150
+ }
151
+
152
+ :global(.theme-dark) .tpc-btn-secondary:hover {
153
+ background: rgba(255, 255, 255, 0.1);
154
+ }
155
+ </style>