@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.
- package/package.json +63 -0
- package/src/category/i18n/en.ts +172 -0
- package/src/category/i18n/es.ts +172 -0
- package/src/category/i18n/fr.ts +172 -0
- package/src/category/index.ts +23 -0
- package/src/category/seo.astro +15 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +11 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +90 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +146 -0
- package/src/pages/[locale].astro +251 -0
- package/src/pages/index.astro +4 -0
- package/src/tests/faq_count.test.ts +19 -0
- package/src/tests/locale_completeness.test.ts +42 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/no_h1_in_components.test.ts +48 -0
- package/src/tests/schemas_fulfillment.test.ts +23 -0
- package/src/tests/seo_length.test.ts +23 -0
- package/src/tests/title_quality.test.ts +56 -0
- package/src/tests/tool_validation.test.ts +17 -0
- package/src/tool/date-diff-calculator/bibliography.astro +14 -0
- package/src/tool/date-diff-calculator/component.astro +370 -0
- package/src/tool/date-diff-calculator/i18n/en.ts +132 -0
- package/src/tool/date-diff-calculator/i18n/es.ts +132 -0
- package/src/tool/date-diff-calculator/i18n/fr.ts +132 -0
- package/src/tool/date-diff-calculator/index.ts +22 -0
- package/src/tool/date-diff-calculator/seo.astro +14 -0
- package/src/tool/date-diff-calculator/ui.ts +17 -0
- package/src/tool/drive-direct-link/bibliography.astro +14 -0
- package/src/tool/drive-direct-link/component.astro +280 -0
- package/src/tool/drive-direct-link/i18n/en.ts +118 -0
- package/src/tool/drive-direct-link/i18n/es.ts +118 -0
- package/src/tool/drive-direct-link/i18n/fr.ts +118 -0
- package/src/tool/drive-direct-link/index.ts +22 -0
- package/src/tool/drive-direct-link/seo.astro +14 -0
- package/src/tool/drive-direct-link/ui.ts +10 -0
- package/src/tool/email-list-cleaner/bibliography.astro +14 -0
- package/src/tool/email-list-cleaner/component.astro +375 -0
- package/src/tool/email-list-cleaner/i18n/en.ts +140 -0
- package/src/tool/email-list-cleaner/i18n/es.ts +140 -0
- package/src/tool/email-list-cleaner/i18n/fr.ts +140 -0
- package/src/tool/email-list-cleaner/index.ts +22 -0
- package/src/tool/email-list-cleaner/seo.astro +14 -0
- package/src/tool/email-list-cleaner/ui.ts +15 -0
- package/src/tool/env-badge-spain/bibliography.astro +14 -0
- package/src/tool/env-badge-spain/component.astro +303 -0
- package/src/tool/env-badge-spain/components/BadgeForm.astro +243 -0
- package/src/tool/env-badge-spain/components/BadgeResult.astro +151 -0
- package/src/tool/env-badge-spain/i18n/en.ts +153 -0
- package/src/tool/env-badge-spain/i18n/es.ts +153 -0
- package/src/tool/env-badge-spain/i18n/fr.ts +153 -0
- package/src/tool/env-badge-spain/index.ts +22 -0
- package/src/tool/env-badge-spain/seo.astro +14 -0
- package/src/tool/env-badge-spain/ui.ts +53 -0
- package/src/tool/morse-beacon/bibliography.astro +14 -0
- package/src/tool/morse-beacon/component.astro +534 -0
- package/src/tool/morse-beacon/i18n/en.ts +157 -0
- package/src/tool/morse-beacon/i18n/es.ts +157 -0
- package/src/tool/morse-beacon/i18n/fr.ts +157 -0
- package/src/tool/morse-beacon/index.ts +22 -0
- package/src/tool/morse-beacon/logic/MorseEngine.ts +124 -0
- package/src/tool/morse-beacon/seo.astro +14 -0
- package/src/tool/morse-beacon/ui.ts +18 -0
- package/src/tool/password-generator/bibliography.astro +14 -0
- package/src/tool/password-generator/component.astro +259 -0
- package/src/tool/password-generator/components/Config.astro +227 -0
- package/src/tool/password-generator/components/Display.astro +147 -0
- package/src/tool/password-generator/components/Strength.astro +70 -0
- package/src/tool/password-generator/i18n/en.ts +166 -0
- package/src/tool/password-generator/i18n/es.ts +166 -0
- package/src/tool/password-generator/i18n/fr.ts +166 -0
- package/src/tool/password-generator/index.ts +22 -0
- package/src/tool/password-generator/seo.astro +14 -0
- package/src/tool/password-generator/ui.ts +16 -0
- package/src/tool/routes/bibliography.astro +14 -0
- package/src/tool/routes/component.astro +543 -0
- package/src/tool/routes/i18n/en.ts +157 -0
- package/src/tool/routes/i18n/es.ts +157 -0
- package/src/tool/routes/i18n/fr.ts +157 -0
- package/src/tool/routes/index.ts +22 -0
- package/src/tool/routes/logic/GeocodingService.ts +60 -0
- package/src/tool/routes/logic/RouteManager.ts +192 -0
- package/src/tool/routes/logic/RouteService.ts +66 -0
- package/src/tool/routes/seo.astro +14 -0
- package/src/tool/routes/ui.ts +16 -0
- package/src/tool/rule-of-three/bibliography.astro +14 -0
- package/src/tool/rule-of-three/component.astro +369 -0
- package/src/tool/rule-of-three/i18n/en.ts +171 -0
- package/src/tool/rule-of-three/i18n/es.ts +171 -0
- package/src/tool/rule-of-three/i18n/fr.ts +171 -0
- package/src/tool/rule-of-three/index.ts +22 -0
- package/src/tool/rule-of-three/seo.astro +14 -0
- package/src/tool/rule-of-three/ui.ts +13 -0
- package/src/tool/seo-content-optimizer/bibliography.astro +14 -0
- package/src/tool/seo-content-optimizer/component.astro +552 -0
- package/src/tool/seo-content-optimizer/i18n/en.ts +136 -0
- package/src/tool/seo-content-optimizer/i18n/es.ts +136 -0
- package/src/tool/seo-content-optimizer/i18n/fr.ts +136 -0
- package/src/tool/seo-content-optimizer/index.ts +22 -0
- package/src/tool/seo-content-optimizer/seo.astro +14 -0
- package/src/tool/seo-content-optimizer/ui.ts +29 -0
- package/src/tool/speed-reader/bibliography.astro +14 -0
- package/src/tool/speed-reader/component.astro +586 -0
- package/src/tool/speed-reader/i18n/en.ts +152 -0
- package/src/tool/speed-reader/i18n/es.ts +152 -0
- package/src/tool/speed-reader/i18n/fr.ts +152 -0
- package/src/tool/speed-reader/index.ts +22 -0
- package/src/tool/speed-reader/logic/RSVPEngine.ts +106 -0
- package/src/tool/speed-reader/seo.astro +14 -0
- package/src/tool/speed-reader/ui.ts +14 -0
- package/src/tool/text-pixel-calculator/bibliography.astro +14 -0
- package/src/tool/text-pixel-calculator/component.astro +315 -0
- package/src/tool/text-pixel-calculator/components/Editor.astro +240 -0
- package/src/tool/text-pixel-calculator/components/Preview.astro +155 -0
- package/src/tool/text-pixel-calculator/components/Stats.astro +87 -0
- package/src/tool/text-pixel-calculator/i18n/en.ts +133 -0
- package/src/tool/text-pixel-calculator/i18n/es.ts +133 -0
- package/src/tool/text-pixel-calculator/i18n/fr.ts +133 -0
- package/src/tool/text-pixel-calculator/index.ts +22 -0
- package/src/tool/text-pixel-calculator/seo.astro +14 -0
- package/src/tool/text-pixel-calculator/ui.ts +15 -0
- package/src/tool/whatsapp-link/bibliography.astro +14 -0
- package/src/tool/whatsapp-link/component.astro +455 -0
- package/src/tool/whatsapp-link/i18n/en.ts +128 -0
- package/src/tool/whatsapp-link/i18n/es.ts +128 -0
- package/src/tool/whatsapp-link/i18n/fr.ts +128 -0
- package/src/tool/whatsapp-link/index.ts +22 -0
- package/src/tool/whatsapp-link/seo.astro +14 -0
- package/src/tool/whatsapp-link/ui.ts +15 -0
- package/src/tools.ts +15 -0
- package/src/types.ts +72 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { KnownLocale } from '../../types';
|
|
3
|
+
import type { MorseBeaconUI } from './ui';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
locale?: KnownLocale;
|
|
7
|
+
ui?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { ui } = Astro.props;
|
|
11
|
+
const t = (ui ?? {}) as MorseBeaconUI;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div class="mb-root" data-ui={JSON.stringify(t)}>
|
|
15
|
+
<div class="mb-card">
|
|
16
|
+
<div class="mb-display">
|
|
17
|
+
<div id="mb-bulb" class="mb-bulb">
|
|
18
|
+
<svg id="mb-bulb-icon" width="40" height="40" viewBox="0 0 24 24" fill="currentColor">
|
|
19
|
+
<path d="M11 21h2v1h-2zm1-19C7.86 2 4 5.86 4 10c0 2.76 1.4 5.19 3.54 6.66V19h9v-2.34C18.6 15.19 20 12.76 20 10c0-4.14-3.86-8-8-8zm1 17h-2v-1h2v1zm2.12-4.29A5.96 5.96 0 0115 10c0-1.65-.67-3.14-1.76-4.24L12 7l-1.24-1.24C9.67 6.86 9 8.35 9 10c0 1.45.52 2.78 1.38 3.81L11 15h2l.62-.81c.44-.52.82-1.1 1.1-1.74z"/>
|
|
20
|
+
</svg>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="mb-ticker-wrap">
|
|
24
|
+
<div id="mb-ticker" class="mb-ticker"></div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="mb-status-badge">
|
|
28
|
+
<span id="mb-status-led" class="mb-status-led"></span>
|
|
29
|
+
<span id="mb-status-text" class="mb-status-text">{t.statusStandby}</span>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="mb-controls">
|
|
34
|
+
<div class="mb-field">
|
|
35
|
+
<div class="mb-field-header">
|
|
36
|
+
<label for="mb-input" class="mb-field-label">{t.labelMessage}</label>
|
|
37
|
+
<span id="mb-char-count" class="mb-char-count">0/100</span>
|
|
38
|
+
</div>
|
|
39
|
+
<textarea
|
|
40
|
+
id="mb-input"
|
|
41
|
+
class="mb-textarea"
|
|
42
|
+
rows="2"
|
|
43
|
+
placeholder={t.placeholder}
|
|
44
|
+
maxlength="100"
|
|
45
|
+
></textarea>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="mb-btn-row">
|
|
49
|
+
<button id="mb-transmit-btn" class="mb-btn mb-btn-transmit">
|
|
50
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
|
51
|
+
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
|
52
|
+
</svg>
|
|
53
|
+
{t.btnTransmit}
|
|
54
|
+
</button>
|
|
55
|
+
<button id="mb-sos-btn" class="mb-btn mb-btn-sos">
|
|
56
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
|
57
|
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
|
58
|
+
</svg>
|
|
59
|
+
{t.btnSosLoop}
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="mb-extras">
|
|
64
|
+
<label class="mb-toggle-label" for="mb-torch">
|
|
65
|
+
<div class="mb-toggle-wrap">
|
|
66
|
+
<input type="checkbox" id="mb-torch" class="mb-toggle" checked />
|
|
67
|
+
<div class="mb-toggle-track"></div>
|
|
68
|
+
</div>
|
|
69
|
+
<span class="mb-toggle-text">{t.labelTorch}</span>
|
|
70
|
+
</label>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<style>
|
|
77
|
+
.mb-root {
|
|
78
|
+
--mb-accent: #16a34a;
|
|
79
|
+
--mb-accent-hover: #15803d;
|
|
80
|
+
--mb-sos-bg: #fff1f2;
|
|
81
|
+
--mb-sos-color: #e11d48;
|
|
82
|
+
--mb-sos-border: #fecdd3;
|
|
83
|
+
--mb-sos-hover-bg: #ffe4e6;
|
|
84
|
+
--mb-display-bg: #111827;
|
|
85
|
+
--mb-bulb-off: #1e293b;
|
|
86
|
+
--mb-bulb-border-off: #334155;
|
|
87
|
+
--mb-ticker-color: #4ade80;
|
|
88
|
+
--mb-status-bg: rgba(30, 41, 59, 0.8);
|
|
89
|
+
--mb-status-border: #334155;
|
|
90
|
+
--mb-card-bg: #fff;
|
|
91
|
+
--mb-card-border: #f1f5f9;
|
|
92
|
+
--mb-text-main: #1e293b;
|
|
93
|
+
--mb-text-label: #64748b;
|
|
94
|
+
--mb-text-hint: #94a3b8;
|
|
95
|
+
--mb-field-border: #e2e8f0;
|
|
96
|
+
--mb-field-focus: #16a34a;
|
|
97
|
+
--mb-extras-border: #f1f5f9;
|
|
98
|
+
--mb-toggle-off: #cbd5e1;
|
|
99
|
+
|
|
100
|
+
width: 100%;
|
|
101
|
+
max-width: 42rem;
|
|
102
|
+
margin: 0 auto;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
:global(.theme-dark) .mb-root {
|
|
106
|
+
--mb-card-bg: #0f172a;
|
|
107
|
+
--mb-card-border: #1e293b;
|
|
108
|
+
--mb-text-main: #e2e8f0;
|
|
109
|
+
--mb-text-label: #94a3b8;
|
|
110
|
+
--mb-text-hint: #475569;
|
|
111
|
+
--mb-field-border: #334155;
|
|
112
|
+
--mb-sos-bg: rgba(225, 29, 72, 0.1);
|
|
113
|
+
--mb-sos-border: rgba(225, 29, 72, 0.2);
|
|
114
|
+
--mb-sos-hover-bg: rgba(225, 29, 72, 0.15);
|
|
115
|
+
--mb-extras-border: #1e293b;
|
|
116
|
+
--mb-toggle-off: #475569;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.mb-card {
|
|
120
|
+
border-radius: 1.5rem;
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
border: 1px solid var(--mb-card-border);
|
|
123
|
+
box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.08);
|
|
124
|
+
background: var(--mb-card-bg);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.mb-display {
|
|
128
|
+
background: var(--mb-display-bg);
|
|
129
|
+
padding: 2rem;
|
|
130
|
+
position: relative;
|
|
131
|
+
display: flex;
|
|
132
|
+
flex-direction: column;
|
|
133
|
+
align-items: center;
|
|
134
|
+
justify-content: center;
|
|
135
|
+
min-height: 220px;
|
|
136
|
+
gap: 1.25rem;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.mb-bulb {
|
|
140
|
+
width: 6rem;
|
|
141
|
+
height: 6rem;
|
|
142
|
+
border-radius: 50%;
|
|
143
|
+
background: var(--mb-bulb-off);
|
|
144
|
+
border: 4px solid var(--mb-bulb-border-off);
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
148
|
+
color: #475569;
|
|
149
|
+
position: relative;
|
|
150
|
+
z-index: 1;
|
|
151
|
+
transition: background 0.05s, border-color 0.05s, box-shadow 0.05s, color 0.05s;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.mb-ticker-wrap {
|
|
155
|
+
height: 2rem;
|
|
156
|
+
overflow: hidden;
|
|
157
|
+
width: 100%;
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
justify-content: center;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.mb-ticker {
|
|
164
|
+
color: var(--mb-ticker-color);
|
|
165
|
+
font-size: 1.5rem;
|
|
166
|
+
font-weight: 700;
|
|
167
|
+
letter-spacing: 0.2em;
|
|
168
|
+
white-space: nowrap;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.mb-status-badge {
|
|
172
|
+
position: absolute;
|
|
173
|
+
top: 1rem;
|
|
174
|
+
right: 1rem;
|
|
175
|
+
display: flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
gap: 0.5rem;
|
|
178
|
+
background: var(--mb-status-bg);
|
|
179
|
+
border: 1px solid var(--mb-status-border);
|
|
180
|
+
padding: 0.375rem 0.75rem;
|
|
181
|
+
border-radius: 9999px;
|
|
182
|
+
backdrop-filter: blur(4px);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.mb-status-led {
|
|
186
|
+
width: 0.5rem;
|
|
187
|
+
height: 0.5rem;
|
|
188
|
+
border-radius: 50%;
|
|
189
|
+
background: #64748b;
|
|
190
|
+
transition: background 0.2s;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.mb-status-text {
|
|
194
|
+
font-size: 0.6875rem;
|
|
195
|
+
font-weight: 700;
|
|
196
|
+
color: #94a3b8;
|
|
197
|
+
letter-spacing: 0.08em;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.mb-controls {
|
|
201
|
+
padding: 2rem;
|
|
202
|
+
display: flex;
|
|
203
|
+
flex-direction: column;
|
|
204
|
+
gap: 2rem;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.mb-field-header {
|
|
208
|
+
display: flex;
|
|
209
|
+
justify-content: space-between;
|
|
210
|
+
align-items: center;
|
|
211
|
+
margin-bottom: 0.75rem;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.mb-field-label {
|
|
215
|
+
font-size: 0.8125rem;
|
|
216
|
+
font-weight: 700;
|
|
217
|
+
color: var(--mb-text-label);
|
|
218
|
+
text-transform: uppercase;
|
|
219
|
+
letter-spacing: 0.05em;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.mb-char-count {
|
|
223
|
+
font-size: 0.8125rem;
|
|
224
|
+
font-weight: 600;
|
|
225
|
+
color: var(--mb-accent);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.mb-textarea {
|
|
229
|
+
width: 100%;
|
|
230
|
+
font-size: 1.125rem;
|
|
231
|
+
font-weight: 700;
|
|
232
|
+
color: var(--mb-text-main);
|
|
233
|
+
background: transparent;
|
|
234
|
+
border: none;
|
|
235
|
+
border-bottom: 2px solid var(--mb-field-border);
|
|
236
|
+
outline: none;
|
|
237
|
+
padding: 0.5rem 0;
|
|
238
|
+
resize: none;
|
|
239
|
+
text-transform: uppercase;
|
|
240
|
+
transition: border-color 0.2s;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.mb-textarea::placeholder {
|
|
244
|
+
color: var(--mb-text-hint);
|
|
245
|
+
text-transform: none;
|
|
246
|
+
font-weight: 400;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.mb-textarea:focus {
|
|
250
|
+
border-color: var(--mb-field-focus);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.mb-btn-row {
|
|
254
|
+
display: grid;
|
|
255
|
+
grid-template-columns: 1fr 1fr;
|
|
256
|
+
gap: 0.75rem;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.mb-btn {
|
|
260
|
+
display: flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
justify-content: center;
|
|
263
|
+
gap: 0.5rem;
|
|
264
|
+
padding: 1rem 1.25rem;
|
|
265
|
+
border-radius: 0.75rem;
|
|
266
|
+
font-weight: 700;
|
|
267
|
+
font-size: 0.875rem;
|
|
268
|
+
letter-spacing: 0.04em;
|
|
269
|
+
text-transform: uppercase;
|
|
270
|
+
border: none;
|
|
271
|
+
cursor: pointer;
|
|
272
|
+
transition: background 0.15s, transform 0.1s, opacity 0.15s;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.mb-btn:active {
|
|
276
|
+
transform: scale(0.97);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.mb-btn:disabled {
|
|
280
|
+
opacity: 0.5;
|
|
281
|
+
cursor: not-allowed;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.mb-btn-transmit {
|
|
285
|
+
background: var(--mb-accent);
|
|
286
|
+
color: #fff;
|
|
287
|
+
box-shadow: 0 4px 14px rgba(22, 163, 74, 0.35);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.mb-btn-transmit:hover:not(:disabled) {
|
|
291
|
+
background: var(--mb-accent-hover);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.mb-btn-sos {
|
|
295
|
+
background: var(--mb-sos-bg);
|
|
296
|
+
color: var(--mb-sos-color);
|
|
297
|
+
border: 2px solid var(--mb-sos-border);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.mb-btn-sos:hover:not(:disabled) {
|
|
301
|
+
background: var(--mb-sos-hover-bg);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.mb-extras {
|
|
305
|
+
display: flex;
|
|
306
|
+
align-items: center;
|
|
307
|
+
justify-content: center;
|
|
308
|
+
padding-top: 1rem;
|
|
309
|
+
border-top: 1px solid var(--mb-extras-border);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.mb-toggle-label {
|
|
313
|
+
display: flex;
|
|
314
|
+
align-items: center;
|
|
315
|
+
gap: 0.75rem;
|
|
316
|
+
cursor: pointer;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.mb-toggle-wrap {
|
|
320
|
+
position: relative;
|
|
321
|
+
display: flex;
|
|
322
|
+
align-items: center;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.mb-toggle {
|
|
326
|
+
position: absolute;
|
|
327
|
+
opacity: 0;
|
|
328
|
+
width: 0;
|
|
329
|
+
height: 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.mb-toggle-track {
|
|
333
|
+
width: 2.75rem;
|
|
334
|
+
height: 1.5rem;
|
|
335
|
+
background: var(--mb-toggle-off);
|
|
336
|
+
border-radius: 9999px;
|
|
337
|
+
cursor: pointer;
|
|
338
|
+
transition: background 0.2s;
|
|
339
|
+
position: relative;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.mb-toggle-track::after {
|
|
343
|
+
content: '';
|
|
344
|
+
position: absolute;
|
|
345
|
+
top: 2px;
|
|
346
|
+
left: 2px;
|
|
347
|
+
width: 1.25rem;
|
|
348
|
+
height: 1.25rem;
|
|
349
|
+
border-radius: 50%;
|
|
350
|
+
background: #fff;
|
|
351
|
+
transition: transform 0.2s;
|
|
352
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.mb-toggle:checked + .mb-toggle-track {
|
|
356
|
+
background: var(--mb-accent);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.mb-toggle:checked + .mb-toggle-track::after {
|
|
360
|
+
transform: translateX(1.25rem);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.mb-toggle-text {
|
|
364
|
+
font-size: 0.9375rem;
|
|
365
|
+
font-weight: 500;
|
|
366
|
+
color: var(--mb-text-label);
|
|
367
|
+
transition: color 0.15s;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.mb-toggle-label:hover .mb-toggle-text {
|
|
371
|
+
color: var(--mb-accent);
|
|
372
|
+
}
|
|
373
|
+
</style>
|
|
374
|
+
|
|
375
|
+
<script>
|
|
376
|
+
import { MorseEngine } from './logic/MorseEngine';
|
|
377
|
+
import type { MorseBit } from './logic/MorseEngine';
|
|
378
|
+
|
|
379
|
+
interface TorchCapabilities { torch?: boolean; }
|
|
380
|
+
interface TorchConstraintSet extends MediaTrackConstraintSet { torch?: boolean; }
|
|
381
|
+
|
|
382
|
+
const root = document.querySelector('.mb-root') as HTMLElement | null;
|
|
383
|
+
const t = JSON.parse(root?.dataset.ui ?? '{}') as Record<string, string>;
|
|
384
|
+
|
|
385
|
+
const inputEl = document.getElementById('mb-input') as HTMLTextAreaElement;
|
|
386
|
+
const transmitBtn = document.getElementById('mb-transmit-btn') as HTMLButtonElement;
|
|
387
|
+
const sosBtn = document.getElementById('mb-sos-btn') as HTMLButtonElement;
|
|
388
|
+
const bulb = document.getElementById('mb-bulb') as HTMLElement;
|
|
389
|
+
const bulbIcon = document.getElementById('mb-bulb-icon') as HTMLElement;
|
|
390
|
+
const ticker = document.getElementById('mb-ticker') as HTMLElement;
|
|
391
|
+
const statusLed = document.getElementById('mb-status-led') as HTMLElement;
|
|
392
|
+
const statusText = document.getElementById('mb-status-text') as HTMLElement;
|
|
393
|
+
const charCount = document.getElementById('mb-char-count') as HTMLElement;
|
|
394
|
+
const torchToggle = document.getElementById('mb-torch') as HTMLInputElement;
|
|
395
|
+
|
|
396
|
+
let currentEngine: MorseEngine | null = null;
|
|
397
|
+
let torchTrack: MediaStreamTrack | null = null;
|
|
398
|
+
let isTorchCapable = false;
|
|
399
|
+
|
|
400
|
+
const LED_COLORS: Record<string, string> = {
|
|
401
|
+
green: '#22c55e', amber: '#f59e0b', red: '#ef4444', slate: '#64748b',
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
function setStatus(text: string, color: 'green' | 'amber' | 'red' | 'slate') {
|
|
405
|
+
statusText.textContent = text;
|
|
406
|
+
statusLed.style.background = LED_COLORS[color] ?? '#64748b';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function setTorchState(on: boolean) {
|
|
410
|
+
if (!isTorchCapable || !torchTrack || !torchToggle.checked) return;
|
|
411
|
+
try {
|
|
412
|
+
await torchTrack.applyConstraints({ advanced: [{ torch: on } as TorchConstraintSet] });
|
|
413
|
+
} catch (e) {
|
|
414
|
+
console.warn('Torch toggle failed', e);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function onSignalChange(active: boolean, bit: MorseBit | null) {
|
|
419
|
+
if (active) {
|
|
420
|
+
bulb.style.backgroundColor = '#4ade80';
|
|
421
|
+
bulb.style.boxShadow = '0 0 60px #4ade80';
|
|
422
|
+
bulb.style.borderColor = '#ffffff';
|
|
423
|
+
bulbIcon.style.color = '#ffffff';
|
|
424
|
+
} else {
|
|
425
|
+
bulb.style.backgroundColor = '';
|
|
426
|
+
bulb.style.boxShadow = '';
|
|
427
|
+
bulb.style.borderColor = '';
|
|
428
|
+
bulbIcon.style.color = '';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
setTorchState(active);
|
|
432
|
+
|
|
433
|
+
if (bit) {
|
|
434
|
+
statusText.textContent = `${t.statusSending ?? 'SENDING:'} ${bit.type.toUpperCase()}`;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function initTorch() {
|
|
439
|
+
if (!torchToggle.checked) return;
|
|
440
|
+
|
|
441
|
+
if (!navigator.mediaDevices?.getUserMedia) {
|
|
442
|
+
isTorchCapable = false;
|
|
443
|
+
const isHttp = location.protocol === 'http:' && location.hostname !== 'localhost';
|
|
444
|
+
setStatus(isHttp ? (t.statusReqHttps ?? 'REQ. HTTPS') : (t.statusNotSupported ?? 'NOT SUPPORTED'), 'amber');
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
|
|
450
|
+
const track = stream.getVideoTracks()[0];
|
|
451
|
+
if (!track) { isTorchCapable = false; return; }
|
|
452
|
+
|
|
453
|
+
if (track.getCapabilities) {
|
|
454
|
+
const caps = track.getCapabilities() as unknown as TorchCapabilities;
|
|
455
|
+
if (caps?.torch) {
|
|
456
|
+
torchTrack = track;
|
|
457
|
+
isTorchCapable = true;
|
|
458
|
+
setStatus(t.statusReady ?? 'READY', 'green');
|
|
459
|
+
} else {
|
|
460
|
+
track.stop();
|
|
461
|
+
isTorchCapable = false;
|
|
462
|
+
setStatus(t.statusNoTorch ?? 'NO TORCH', 'amber');
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
torchTrack = track;
|
|
466
|
+
isTorchCapable = true;
|
|
467
|
+
setStatus(t.statusReady ?? 'READY', 'green');
|
|
468
|
+
}
|
|
469
|
+
} catch {
|
|
470
|
+
isTorchCapable = false;
|
|
471
|
+
setStatus(t.statusNoPermission ?? 'NO PERMISSION', 'red');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function stopTransmission() {
|
|
476
|
+
currentEngine?.stop();
|
|
477
|
+
setStatus(t.statusStopping ?? 'STOPPING...', 'amber');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function resetButtons() {
|
|
481
|
+
inputEl.disabled = false;
|
|
482
|
+
transmitBtn.disabled = false;
|
|
483
|
+
|
|
484
|
+
sosBtn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>${t.btnSosLoop ?? 'SOS'}`;
|
|
485
|
+
sosBtn.onclick = () => startTransmission('SOS', true);
|
|
486
|
+
transmitBtn.onclick = () => startTransmission(inputEl.value, false);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function startTransmission(text: string, loop: boolean) {
|
|
490
|
+
if (!text.trim()) return;
|
|
491
|
+
|
|
492
|
+
if (currentEngine) {
|
|
493
|
+
currentEngine.stop();
|
|
494
|
+
await new Promise<void>((r) => setTimeout(r, 100));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
currentEngine = new MorseEngine(15, onSignalChange);
|
|
498
|
+
const sequence = MorseEngine.compileSequence(text, 15);
|
|
499
|
+
ticker.textContent = MorseEngine.textToMorse(text);
|
|
500
|
+
|
|
501
|
+
inputEl.disabled = true;
|
|
502
|
+
transmitBtn.disabled = true;
|
|
503
|
+
|
|
504
|
+
const stopIcon = `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z"/></svg>${t.btnStop ?? 'STOP'}`;
|
|
505
|
+
sosBtn.innerHTML = stopIcon;
|
|
506
|
+
sosBtn.onclick = (e) => { e.preventDefault(); stopTransmission(); };
|
|
507
|
+
if (!loop) {
|
|
508
|
+
transmitBtn.onclick = (e) => { e.preventDefault(); stopTransmission(); };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
setStatus(t.statusTransmitting ?? 'TRANSMITTING', 'green');
|
|
512
|
+
|
|
513
|
+
if (!torchTrack && torchToggle.checked) await initTorch();
|
|
514
|
+
|
|
515
|
+
await currentEngine.transmit(sequence, loop);
|
|
516
|
+
|
|
517
|
+
setTorchState(false);
|
|
518
|
+
setStatus(t.statusWaiting ?? 'WAITING', 'slate');
|
|
519
|
+
onSignalChange(false, null);
|
|
520
|
+
currentEngine = null;
|
|
521
|
+
resetButtons();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
transmitBtn.onclick = () => startTransmission(inputEl.value, false);
|
|
525
|
+
sosBtn.onclick = () => startTransmission('SOS', true);
|
|
526
|
+
|
|
527
|
+
inputEl.addEventListener('input', () => {
|
|
528
|
+
charCount.textContent = `${inputEl.value.length}/100`;
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
torchToggle.addEventListener('change', async () => {
|
|
532
|
+
if (torchToggle.checked && !torchTrack) await initTorch();
|
|
533
|
+
});
|
|
534
|
+
</script>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
3
|
+
import type { MorseBeaconUI } from '../ui';
|
|
4
|
+
|
|
5
|
+
const faqData = [
|
|
6
|
+
{
|
|
7
|
+
question: 'What is the SOS distress signal in Morse code?',
|
|
8
|
+
answer: "The signal is '... --- ...' (three dots, three dashes, three dots). It is transmitted continuously without spaces between the letters to indicate an immediate emergency.",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
question: 'Why is the torch not working in my browser?',
|
|
12
|
+
answer: 'Activating the torch requires the browser to have camera permissions. Some mobile browsers or older desktop versions do not support this API for privacy reasons.',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
question: 'Is the SOS screen signal visible?',
|
|
16
|
+
answer: 'Yes, in conditions of total darkness, the maximum brightness of a white screen flashing in Morse can be seen from several hundred metres away, making it a useful alternative if the torch fails.',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
question: 'What is the international Morse code?',
|
|
20
|
+
answer: 'It is a communication system that uses sequences of short (dots) and long (dashes) signals to represent letters and numbers, standardised by the ITU for radio communications and optical signals.',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const howToData = [
|
|
25
|
+
{
|
|
26
|
+
name: 'Write the message',
|
|
27
|
+
text: "Enter the text you want to transmit or use the pre-configured 'SOS' button for emergencies.",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'Configure speed',
|
|
31
|
+
text: 'Adjust the WPM (words per minute) to make the signal faster or slower depending on visibility.',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'Choose light source',
|
|
35
|
+
text: 'Enable full-screen flashing or allow access to the camera torch for a stronger signal.',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'Start transmission',
|
|
39
|
+
text: 'Press Transmit to have the system convert the text into automatic visual pulses following the Morse standard.',
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
44
|
+
'@context': 'https://schema.org',
|
|
45
|
+
'@type': 'FAQPage',
|
|
46
|
+
mainEntity: faqData.map((item) => ({
|
|
47
|
+
'@type': 'Question',
|
|
48
|
+
name: item.question,
|
|
49
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
50
|
+
})),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const howToSchema: WithContext<HowTo> = {
|
|
54
|
+
'@context': 'https://schema.org',
|
|
55
|
+
'@type': 'HowTo',
|
|
56
|
+
name: 'How to use the Morse beacon to transmit messages',
|
|
57
|
+
step: howToData.map((s) => ({ '@type': 'HowToStep', name: s.name, text: s.text })),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
61
|
+
'@context': 'https://schema.org',
|
|
62
|
+
'@type': 'SoftwareApplication',
|
|
63
|
+
name: 'Morse Beacon: Tactical SOS Transmitter',
|
|
64
|
+
applicationCategory: 'UtilitiesApplication',
|
|
65
|
+
operatingSystem: 'Web',
|
|
66
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
|
|
67
|
+
description: 'Turn your device into a Morse transmission station. Use flash and screen as emergency light signals and tactical communication.',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const ui: MorseBeaconUI = {
|
|
71
|
+
labelMessage: 'Message to Transmit',
|
|
72
|
+
placeholder: 'Type your message here (SOS, HELLO, HELP)...',
|
|
73
|
+
btnTransmit: 'Transmit',
|
|
74
|
+
btnSosLoop: 'SOS Loop',
|
|
75
|
+
btnStop: 'Stop',
|
|
76
|
+
labelTorch: 'Flash/Torch',
|
|
77
|
+
statusStandby: 'STANDBY',
|
|
78
|
+
statusTransmitting: 'TRANSMITTING',
|
|
79
|
+
statusStopping: 'STOPPING...',
|
|
80
|
+
statusWaiting: 'WAITING',
|
|
81
|
+
statusReady: 'HARDWARE READY',
|
|
82
|
+
statusNoTorch: 'NO TORCH',
|
|
83
|
+
statusNoPermission: 'NO PERMISSION',
|
|
84
|
+
statusNotSupported: 'NOT SUPPORTED',
|
|
85
|
+
statusReqHttps: 'REQ. HTTPS',
|
|
86
|
+
statusSending: 'SENDING:',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const content: ToolLocaleContent<MorseBeaconUI> = {
|
|
90
|
+
slug: 'morse-beacon',
|
|
91
|
+
title: 'Morse Beacon: Tactical SOS Transmitter',
|
|
92
|
+
description: 'Turn your device into a Morse transmission station. Use flash and screen as emergency light signals and tactical communication.',
|
|
93
|
+
ui,
|
|
94
|
+
faqTitle: 'Frequently Asked Questions',
|
|
95
|
+
faq: faqData,
|
|
96
|
+
howTo: howToData,
|
|
97
|
+
bibliographyTitle: 'References',
|
|
98
|
+
bibliography: [
|
|
99
|
+
{ name: 'ITU-R M.1677-1 — International Morse Code', url: 'https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.1677-1-200910-I!!PDF-E.pdf' },
|
|
100
|
+
{ name: 'W3C MediaCapture Image — Torch', url: 'https://w3c.github.io/mediacapture-image/#torch' },
|
|
101
|
+
{ name: 'Morse code — Wikipedia', url: 'https://en.wikipedia.org/wiki/Morse_code' },
|
|
102
|
+
],
|
|
103
|
+
schemas: [faqSchema, howToSchema, appSchema],
|
|
104
|
+
seo: [
|
|
105
|
+
{
|
|
106
|
+
type: 'title',
|
|
107
|
+
text: 'The Language of Light',
|
|
108
|
+
level: 2,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
type: 'paragraph',
|
|
112
|
+
html: 'This tool turns your device into an optical signalling beacon capable of transmitting messages visible from kilometres away. Using the international Morse Code standard, it enables silent or emergency communication via light pulses (torch and screen).',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: 'comparative',
|
|
116
|
+
columns: 2,
|
|
117
|
+
items: [
|
|
118
|
+
{
|
|
119
|
+
icon: 'mdi:history',
|
|
120
|
+
title: 'A Universal Standard',
|
|
121
|
+
description: 'Developed in 1830 by Samuel Morse, this binary system of dots and dashes revolutionised telecommunications. Its simplicity makes it extremely robust: it can be transmitted by sound, radio, electricity or light, and remains readable even under severe interference.',
|
|
122
|
+
points: [],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
icon: 'mdi:flash-alert',
|
|
126
|
+
title: 'SOS Emergency Mode',
|
|
127
|
+
description: "The 'SOS Loop' button continuously transmits the sequence ··· --- ···. This signal is universally recognised as a distress call and, thanks to the high contrast of the LED torch, is visible from a great distance even in daylight under certain conditions.",
|
|
128
|
+
points: [],
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
type: 'title',
|
|
134
|
+
text: 'ITU-R M.1677-1 Standard',
|
|
135
|
+
level: 2,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
type: 'paragraph',
|
|
139
|
+
html: 'This tool strictly respects the <strong>regulatory timings</strong> of the international Morse code as defined by the International Telecommunication Union.',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
type: 'list',
|
|
143
|
+
items: [
|
|
144
|
+
'1 dot = 1 time unit',
|
|
145
|
+
'1 dash = 3 time units',
|
|
146
|
+
'Space between elements = 1 unit',
|
|
147
|
+
'Space between letters = 3 units',
|
|
148
|
+
'Space between words = 7 units',
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: 'tip',
|
|
153
|
+
title: 'Standard speed',
|
|
154
|
+
html: 'The default speed is <strong>15 WPM</strong> (words per minute), which corresponds to a professional transmission pace. At 15 WPM, 1 unit = 80 ms.',
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
};
|