@jjlmoya/utils-developer 1.16.0 → 1.18.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 (63) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +7 -1
  4. package/src/index.ts +4 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/calculadoraTiempoDatos/data-time-calculator-web-speed-impact.css +498 -499
  8. package/src/tool/jwtDecoder/bibliography.astro +14 -0
  9. package/src/tool/jwtDecoder/bibliography.ts +12 -0
  10. package/src/tool/jwtDecoder/component.astro +209 -0
  11. package/src/tool/jwtDecoder/entry.ts +30 -0
  12. package/src/tool/jwtDecoder/i18n/de.ts +248 -0
  13. package/src/tool/jwtDecoder/i18n/en.ts +247 -0
  14. package/src/tool/jwtDecoder/i18n/es.ts +248 -0
  15. package/src/tool/jwtDecoder/i18n/fr.ts +248 -0
  16. package/src/tool/jwtDecoder/i18n/id.ts +248 -0
  17. package/src/tool/jwtDecoder/i18n/it.ts +248 -0
  18. package/src/tool/jwtDecoder/i18n/ja.ts +248 -0
  19. package/src/tool/jwtDecoder/i18n/ko.ts +248 -0
  20. package/src/tool/jwtDecoder/i18n/nl.ts +247 -0
  21. package/src/tool/jwtDecoder/i18n/pl.ts +247 -0
  22. package/src/tool/jwtDecoder/i18n/pt.ts +247 -0
  23. package/src/tool/jwtDecoder/i18n/ru.ts +248 -0
  24. package/src/tool/jwtDecoder/i18n/sv.ts +248 -0
  25. package/src/tool/jwtDecoder/i18n/tr.ts +248 -0
  26. package/src/tool/jwtDecoder/i18n/zh.ts +248 -0
  27. package/src/tool/jwtDecoder/index.ts +11 -0
  28. package/src/tool/jwtDecoder/jwt-decoder-parser-and-claims-inspector.css +358 -0
  29. package/src/tool/jwtDecoder/logic.ts +96 -0
  30. package/src/tool/jwtDecoder/seo.astro +15 -0
  31. package/src/tool/jwtDecoder/ui.ts +38 -0
  32. package/src/tool/serpPixelSimulator/bibliography.astro +11 -4
  33. package/src/tool/serpPixelSimulator/index.ts +0 -4
  34. package/src/tool/serpPixelSimulator/seo.astro +8 -5
  35. package/src/tool/visualCssGridFlexboxGenerator/bibliography.astro +14 -0
  36. package/src/tool/visualCssGridFlexboxGenerator/bibliography.ts +20 -0
  37. package/src/tool/visualCssGridFlexboxGenerator/canvas.ts +103 -0
  38. package/src/tool/visualCssGridFlexboxGenerator/component.astro +218 -0
  39. package/src/tool/visualCssGridFlexboxGenerator/entry.ts +30 -0
  40. package/src/tool/visualCssGridFlexboxGenerator/i18n/de.ts +110 -0
  41. package/src/tool/visualCssGridFlexboxGenerator/i18n/en.ts +256 -0
  42. package/src/tool/visualCssGridFlexboxGenerator/i18n/es.ts +110 -0
  43. package/src/tool/visualCssGridFlexboxGenerator/i18n/fr.ts +110 -0
  44. package/src/tool/visualCssGridFlexboxGenerator/i18n/id.ts +110 -0
  45. package/src/tool/visualCssGridFlexboxGenerator/i18n/it.ts +110 -0
  46. package/src/tool/visualCssGridFlexboxGenerator/i18n/ja.ts +110 -0
  47. package/src/tool/visualCssGridFlexboxGenerator/i18n/ko.ts +110 -0
  48. package/src/tool/visualCssGridFlexboxGenerator/i18n/nl.ts +110 -0
  49. package/src/tool/visualCssGridFlexboxGenerator/i18n/pl.ts +110 -0
  50. package/src/tool/visualCssGridFlexboxGenerator/i18n/pt.ts +110 -0
  51. package/src/tool/visualCssGridFlexboxGenerator/i18n/ru.ts +110 -0
  52. package/src/tool/visualCssGridFlexboxGenerator/i18n/sv.ts +110 -0
  53. package/src/tool/visualCssGridFlexboxGenerator/i18n/tr.ts +110 -0
  54. package/src/tool/visualCssGridFlexboxGenerator/i18n/zh.ts +110 -0
  55. package/src/tool/visualCssGridFlexboxGenerator/index.ts +11 -0
  56. package/src/tool/visualCssGridFlexboxGenerator/interactions.ts +45 -0
  57. package/src/tool/visualCssGridFlexboxGenerator/logic.ts +49 -0
  58. package/src/tool/visualCssGridFlexboxGenerator/presets.ts +63 -0
  59. package/src/tool/visualCssGridFlexboxGenerator/seo.astro +15 -0
  60. package/src/tool/visualCssGridFlexboxGenerator/ui.ts +58 -0
  61. package/src/tool/visualCssGridFlexboxGenerator/visual-css-grid-flexbox-generator.css +816 -0
  62. package/src/tool/visualCssGridFlexboxGenerator/workspace.ts +192 -0
  63. package/src/tools.ts +3 -1
@@ -0,0 +1,358 @@
1
+ .jwt-root {
2
+ width: 100%;
3
+ }
4
+
5
+ .jwt-card {
6
+ display: grid;
7
+ gap: 1rem;
8
+ width: 100%;
9
+ padding: 1rem;
10
+ overflow: hidden;
11
+ background:
12
+ linear-gradient(135deg, rgba(14, 165, 233, 0.14), transparent 34%),
13
+ linear-gradient(315deg, rgba(22, 163, 74, 0.13), transparent 36%),
14
+ #fff;
15
+ border: 1px solid rgb(219, 234, 254);
16
+ border-radius: 1.5rem;
17
+ box-shadow: 0 22px 60px rgba(15, 23, 42, 0.08);
18
+ }
19
+
20
+ .theme-dark .jwt-card {
21
+ background:
22
+ linear-gradient(135deg, rgba(14, 165, 233, 0.18), transparent 34%),
23
+ linear-gradient(315deg, rgba(34, 197, 94, 0.15), transparent 36%),
24
+ rgb(9, 12, 22);
25
+ border-color: rgb(30, 41, 59);
26
+ box-shadow: none;
27
+ }
28
+
29
+ .jwt-input-panel,
30
+ .jwt-json-panel,
31
+ .jwt-claims-panel {
32
+ background: rgba(255, 255, 255, 0.82);
33
+ border: 1px solid rgba(148, 163, 184, 0.28);
34
+ border-radius: 1rem;
35
+ backdrop-filter: blur(16px);
36
+ }
37
+
38
+ .theme-dark .jwt-input-panel,
39
+ .theme-dark .jwt-json-panel,
40
+ .theme-dark .jwt-claims-panel {
41
+ background: rgba(15, 23, 42, 0.78);
42
+ border-color: rgba(71, 85, 105, 0.62);
43
+ }
44
+
45
+ .jwt-input-panel {
46
+ display: grid;
47
+ gap: 0.85rem;
48
+ padding: 1rem;
49
+ }
50
+
51
+ .jwt-token-field {
52
+ display: grid;
53
+ gap: 0.55rem;
54
+ }
55
+
56
+ .jwt-label {
57
+ display: flex;
58
+ gap: 0.5rem;
59
+ align-items: center;
60
+ font-size: 0.9rem;
61
+ font-weight: 750;
62
+ color: rgb(15, 23, 42);
63
+ }
64
+
65
+ .theme-dark .jwt-label {
66
+ color: rgb(226, 232, 240);
67
+ }
68
+
69
+ .jwt-label-icon,
70
+ .jwt-button-icon {
71
+ width: 1rem;
72
+ height: 1rem;
73
+ }
74
+
75
+ .jwt-label-icon {
76
+ color: rgb(2, 132, 199);
77
+ }
78
+
79
+ .jwt-token-input {
80
+ box-sizing: border-box;
81
+ width: 100%;
82
+ min-height: 8.5rem;
83
+ padding: 0.9rem;
84
+ font-size: 0.82rem;
85
+ line-height: 1.55;
86
+ color: rgb(15, 23, 42);
87
+ resize: vertical;
88
+ background: rgb(248, 250, 252);
89
+ border: 1px solid rgb(203, 213, 225);
90
+ border-radius: 0.85rem;
91
+ }
92
+
93
+ .jwt-token-input:focus {
94
+ border-color: rgb(14, 165, 233);
95
+ outline: none;
96
+ box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.14);
97
+ }
98
+
99
+ .theme-dark .jwt-token-input {
100
+ color: rgb(226, 232, 240);
101
+ background: rgb(15, 23, 42);
102
+ border-color: rgb(51, 65, 85);
103
+ }
104
+
105
+ .jwt-action-row {
106
+ display: flex;
107
+ flex-wrap: wrap;
108
+ gap: 0.55rem;
109
+ }
110
+
111
+ .jwt-action,
112
+ .jwt-copy {
113
+ display: inline-flex;
114
+ gap: 0.45rem;
115
+ align-items: center;
116
+ justify-content: center;
117
+ min-height: 2.3rem;
118
+ color: rgb(15, 23, 42);
119
+ cursor: pointer;
120
+ background: #fff;
121
+ border: 1px solid rgb(203, 213, 225);
122
+ border-radius: 0.7rem;
123
+ }
124
+
125
+ .jwt-action {
126
+ padding: 0 0.8rem;
127
+ font-weight: 750;
128
+ }
129
+
130
+ .jwt-action-primary {
131
+ color: #fff;
132
+ background: rgb(2, 132, 199);
133
+ border-color: rgb(2, 132, 199);
134
+ }
135
+
136
+ .theme-dark .jwt-action,
137
+ .theme-dark .jwt-copy {
138
+ color: rgb(226, 232, 240);
139
+ background: rgb(15, 23, 42);
140
+ border-color: rgb(51, 65, 85);
141
+ }
142
+
143
+ .theme-dark .jwt-action-primary {
144
+ color: #fff;
145
+ background: rgb(14, 165, 233);
146
+ border-color: rgb(14, 165, 233);
147
+ }
148
+
149
+ .jwt-status {
150
+ padding: 0.7rem 0.85rem;
151
+ margin: 0;
152
+ font-weight: 750;
153
+ border-radius: 0.8rem;
154
+ }
155
+
156
+ .jwt-status-waiting {
157
+ color: rgb(51, 65, 85);
158
+ background: rgb(241, 245, 249);
159
+ }
160
+
161
+ .jwt-status-valid {
162
+ color: rgb(20, 83, 45);
163
+ background: rgb(220, 252, 231);
164
+ }
165
+
166
+ .jwt-status-invalid,
167
+ .jwt-status-expired {
168
+ color: rgb(127, 29, 29);
169
+ background: rgb(254, 226, 226);
170
+ }
171
+
172
+ .jwt-status-unsigned {
173
+ color: rgb(120, 53, 15);
174
+ background: rgb(254, 243, 199);
175
+ }
176
+
177
+ .theme-dark .jwt-status-waiting {
178
+ color: rgb(203, 213, 225);
179
+ background: rgb(30, 41, 59);
180
+ }
181
+
182
+ .jwt-privacy {
183
+ margin: 0;
184
+ font-size: 0.82rem;
185
+ line-height: 1.45;
186
+ color: rgb(71, 85, 105);
187
+ }
188
+
189
+ .theme-dark .jwt-privacy {
190
+ color: rgb(148, 163, 184);
191
+ }
192
+
193
+ .jwt-output-grid {
194
+ display: grid;
195
+ gap: 1rem;
196
+ }
197
+
198
+ .jwt-json-panel {
199
+ min-width: 0;
200
+ overflow: hidden;
201
+ }
202
+
203
+ .jwt-panel-heading {
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: space-between;
207
+ padding: 0.85rem 0.95rem;
208
+ border-bottom: 1px solid rgba(148, 163, 184, 0.25);
209
+ }
210
+
211
+ .jwt-panel-heading h2,
212
+ .jwt-claims-panel h2 {
213
+ margin: 0;
214
+ font-size: 0.95rem;
215
+ color: rgb(15, 23, 42);
216
+ }
217
+
218
+ .theme-dark .jwt-panel-heading h2,
219
+ .theme-dark .jwt-claims-panel h2 {
220
+ color: rgb(226, 232, 240);
221
+ }
222
+
223
+ .jwt-copy {
224
+ position: relative;
225
+ width: 2.15rem;
226
+ height: 2.15rem;
227
+ }
228
+
229
+ .jwt-copy svg {
230
+ width: 1rem;
231
+ height: 1rem;
232
+ }
233
+
234
+ .jwt-copy[data-copied]::after {
235
+ position: absolute;
236
+ right: 0;
237
+ bottom: calc(100% + 0.3rem);
238
+ padding: 0.25rem 0.45rem;
239
+ font-size: 0.72rem;
240
+ color: #fff;
241
+ white-space: nowrap;
242
+ content: attr(data-copied);
243
+ background: rgb(15, 23, 42);
244
+ border-radius: 0.45rem;
245
+ }
246
+
247
+ .jwt-code {
248
+ min-height: 9rem;
249
+ max-height: 21rem;
250
+ padding: 1rem;
251
+ margin: 0;
252
+ overflow: auto;
253
+ font-size: 0.78rem;
254
+ line-height: 1.55;
255
+ color: rgb(30, 41, 59);
256
+ white-space: pre-wrap;
257
+ overflow-wrap: anywhere;
258
+ }
259
+
260
+ .jwt-signature-code {
261
+ color: rgb(7, 89, 133);
262
+ }
263
+
264
+ .theme-dark .jwt-code {
265
+ color: rgb(203, 213, 225);
266
+ }
267
+
268
+ .theme-dark .jwt-signature-code {
269
+ color: rgb(125, 211, 252);
270
+ }
271
+
272
+ .jwt-claims-panel {
273
+ padding: 1rem;
274
+ }
275
+
276
+ .jwt-claims {
277
+ display: grid;
278
+ grid-template-columns: 1fr;
279
+ gap: 0.55rem;
280
+ margin: 0.85rem 0 0;
281
+ }
282
+
283
+ .jwt-claims div {
284
+ display: grid;
285
+ grid-template-columns: minmax(7rem, 0.55fr) 1fr;
286
+ gap: 0.65rem;
287
+ padding: 0.65rem 0.75rem;
288
+ background: rgba(248, 250, 252, 0.82);
289
+ border-radius: 0.75rem;
290
+ }
291
+
292
+ .theme-dark .jwt-claims div {
293
+ background: rgba(30, 41, 59, 0.78);
294
+ }
295
+
296
+ .jwt-claims dt {
297
+ font-size: 0.76rem;
298
+ font-weight: 800;
299
+ color: rgb(71, 85, 105);
300
+ }
301
+
302
+ .jwt-claims dd {
303
+ min-width: 0;
304
+ margin: 0;
305
+ font-size: 0.82rem;
306
+ color: rgb(15, 23, 42);
307
+ overflow-wrap: anywhere;
308
+ }
309
+
310
+ .theme-dark .jwt-claims dt {
311
+ color: rgb(148, 163, 184);
312
+ }
313
+
314
+ .theme-dark .jwt-claims dd {
315
+ color: rgb(226, 232, 240);
316
+ }
317
+
318
+ @media (min-width: 860px) {
319
+ .jwt-card {
320
+ grid-template-columns: minmax(20rem, 0.92fr) minmax(0, 1.08fr);
321
+ padding: 1.25rem;
322
+ }
323
+
324
+ .jwt-input-panel,
325
+ .jwt-claims-panel {
326
+ grid-column: 1;
327
+ }
328
+
329
+ .jwt-output-grid {
330
+ grid-row: span 2;
331
+ grid-column: 2;
332
+ }
333
+ }
334
+
335
+ @media (min-width: 1180px) {
336
+ .jwt-output-grid {
337
+ grid-template-columns: repeat(2, minmax(0, 1fr));
338
+ }
339
+
340
+ .jwt-json-panel-main {
341
+ grid-column: span 2;
342
+ }
343
+ }
344
+
345
+ @media (max-width: 520px) {
346
+ .jwt-card {
347
+ padding: 0.7rem;
348
+ border-radius: 1rem;
349
+ }
350
+
351
+ .jwt-action {
352
+ flex: 1 1 100%;
353
+ }
354
+
355
+ .jwt-claims div {
356
+ grid-template-columns: 1fr;
357
+ }
358
+ }
@@ -0,0 +1,96 @@
1
+ export interface DecodedJwt {
2
+ header: Record<string, unknown>;
3
+ payload: Record<string, unknown>;
4
+ signature: string;
5
+ valid: boolean;
6
+ unsigned: boolean;
7
+ expired: boolean;
8
+ error?: 'invalid-segments' | 'invalid-json';
9
+ }
10
+
11
+ const textDecoder = new TextDecoder();
12
+
13
+ function decodeBase64Url(segment: string): string {
14
+ const normalized = segment.replace(/-/g, '+').replace(/_/g, '/');
15
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
16
+ const binary = atob(padded);
17
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
18
+
19
+ return textDecoder.decode(bytes);
20
+ }
21
+
22
+ function parseJsonSegment(segment: string): Record<string, unknown> {
23
+ const decoded = decodeBase64Url(segment);
24
+ const parsed = JSON.parse(decoded) as unknown;
25
+
26
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
27
+ throw new Error('invalid-json');
28
+ }
29
+
30
+ return parsed as Record<string, unknown>;
31
+ }
32
+
33
+ function invalidJwt(error: DecodedJwt['error']): DecodedJwt {
34
+ return {
35
+ header: {},
36
+ payload: {},
37
+ signature: '',
38
+ valid: false,
39
+ unsigned: false,
40
+ expired: false,
41
+ error,
42
+ };
43
+ }
44
+
45
+ function hasValidSegments(parts: string[]): boolean {
46
+ return parts.length === 3 && Boolean(parts[0]) && Boolean(parts[1]);
47
+ }
48
+
49
+ function buildDecodedJwt(header: Record<string, unknown>, payload: Record<string, unknown>, signature: string): DecodedJwt {
50
+ const unsigned = signature.length === 0 || header['alg'] === 'none';
51
+ const exp = typeof payload['exp'] === 'number' ? payload['exp'] : undefined;
52
+
53
+ return {
54
+ header,
55
+ payload,
56
+ signature,
57
+ valid: true,
58
+ unsigned,
59
+ expired: typeof exp === 'number' && exp * 1000 < Date.now(),
60
+ };
61
+ }
62
+
63
+ export function decodeJwt(token: string): DecodedJwt {
64
+ const parts = token.trim().split('.');
65
+
66
+ if (!hasValidSegments(parts)) return invalidJwt('invalid-segments');
67
+
68
+ try {
69
+ const header = parseJsonSegment(parts[0]);
70
+ const payload = parseJsonSegment(parts[1]);
71
+ return buildDecodedJwt(header, payload, parts[2] ?? '');
72
+ } catch {
73
+ return invalidJwt('invalid-json');
74
+ }
75
+ }
76
+
77
+ export function formatJson(value: Record<string, unknown>): string {
78
+ return JSON.stringify(value, null, 2);
79
+ }
80
+
81
+ export function formatClaimDate(value: unknown): string | null {
82
+ if (typeof value !== 'number') return null;
83
+
84
+ return new Intl.DateTimeFormat(undefined, {
85
+ dateStyle: 'medium',
86
+ timeStyle: 'medium',
87
+ }).format(new Date(value * 1000));
88
+ }
89
+
90
+ export function formatClaimValue(value: unknown): string | null {
91
+ if (value === undefined || value === null || value === '') return null;
92
+ if (Array.isArray(value)) return value.map(String).join(', ');
93
+ if (typeof value === 'object') return JSON.stringify(value);
94
+
95
+ return String(value);
96
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { jwtDecoder } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await jwtDecoder.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,38 @@
1
+ export interface JwtDecoderUI extends Record<string, string> {
2
+ tokenLabel: string;
3
+ tokenPlaceholder: string;
4
+ sampleButton: string;
5
+ clearButton: string;
6
+ statusWaiting: string;
7
+ statusValid: string;
8
+ statusInvalid: string;
9
+ statusExpired: string;
10
+ statusUnsigned: string;
11
+ headerTitle: string;
12
+ payloadTitle: string;
13
+ signatureTitle: string;
14
+ claimsTitle: string;
15
+ copyHeader: string;
16
+ copyPayload: string;
17
+ copySignature: string;
18
+ copyAll: string;
19
+ copiedLabel: string;
20
+ invalidTokenTitle: string;
21
+ invalidTokenBody: string;
22
+ invalidSegmentError: string;
23
+ invalidDecodeError: string;
24
+ emptyJson: string;
25
+ signaturePresent: string;
26
+ signatureMissing: string;
27
+ algorithmLabel: string;
28
+ typeLabel: string;
29
+ issuerLabel: string;
30
+ subjectLabel: string;
31
+ audienceLabel: string;
32
+ issuedAtLabel: string;
33
+ notBeforeLabel: string;
34
+ expiresAtLabel: string;
35
+ claimMissing: string;
36
+ privacyNote: string;
37
+ sampleToken: string;
38
+ }
@@ -1,7 +1,14 @@
1
1
  ---
2
- import { bibliography } from './bibliography';
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { serpPixelSimulator } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await serpPixelSimulator.i18n[locale]?.();
3
12
  ---
4
13
 
5
- <ul>
6
- {bibliography.map((item) => <li><a href={item.url} rel="nofollow noopener" target="_blank">{item.name}</a></li>)}
7
- </ul>
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -1,9 +1,5 @@
1
- import type { ToolDefinition } from '../../types';
2
- import type { ToolDefinition } from '../../types';
3
1
  import { serpPixelSimulator } from './entry';
4
-
5
2
  export * from './entry';
6
-
7
3
  export const SERP_PIXEL_SIMULATOR_TOOL: ToolDefinition = {
8
4
  entry: serpPixelSimulator,
9
5
  Component: () => import('./component.astro'),
@@ -1,12 +1,15 @@
1
1
  ---
2
- import type { SerpPixelSimulatorUI } from './ui';
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { serpPixelSimulator } from './index';
4
+ import type { KnownLocale } from '../../types';
3
5
 
4
6
  interface Props {
5
- ui?: Record<string, unknown>;
7
+ locale?: KnownLocale;
6
8
  }
7
9
 
8
- const { ui } = Astro.props;
9
- const t = (ui ?? {}) as SerpPixelSimulatorUI;
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await serpPixelSimulator.i18n[locale]?.();
12
+ if (!content) return null;
10
13
  ---
11
14
 
12
- <section class="tool-seo" aria-label={t.previewLabel}></section>
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { visualCssGridFlexboxGenerator } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await visualCssGridFlexboxGenerator.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,20 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'MDN Web Docs: Basic concepts of flexbox',
6
+ url: 'https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout/Basic_concepts_of_flexbox',
7
+ },
8
+ {
9
+ name: 'MDN Web Docs: Basic concepts of grid layout',
10
+ url: 'https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Basic_concepts_of_grid_layout',
11
+ },
12
+ {
13
+ name: 'CSS-Tricks: A Complete Guide to Flexbox',
14
+ url: 'https://css-tricks.com/snippets/css/a-guide-to-flexbox/',
15
+ },
16
+ {
17
+ name: 'CSS-Tricks: A Complete Guide to CSS Grid',
18
+ url: 'https://css-tricks.com/snippets/css/complete-guide-grid/',
19
+ },
20
+ ];
@@ -0,0 +1,103 @@
1
+ import type { LayoutMode, LayoutState } from './logic';
2
+
3
+ export function getAllItems(canvas: HTMLElement): HTMLElement[] {
4
+ return Array.from(canvas.querySelectorAll<HTMLElement>('.vcg-item'));
5
+ }
6
+
7
+ export function getItemSpans(canvas: HTMLElement): Record<string, number> {
8
+ const spans: Record<string, number> = {};
9
+ for (const item of getAllItems(canvas)) {
10
+ const s = Number(item.dataset.span ?? '1');
11
+ if (s > 1) spans[item.dataset.item ?? ''] = s;
12
+ }
13
+ return spans;
14
+ }
15
+
16
+ export function refreshItemLabels(canvas: HTMLElement, itemPrefix: string) {
17
+ getAllItems(canvas).forEach((item, index) => {
18
+ item.dataset.item = String(index + 1);
19
+ const label = item.querySelector('span');
20
+ if (label) label.textContent = `${itemPrefix} ${index + 1}`;
21
+ });
22
+ }
23
+
24
+ export function makeItem(index: number, itemPrefix: string): HTMLElement {
25
+ const item = document.createElement('div');
26
+ const label = document.createElement('span');
27
+ item.className = 'vcg-item vcg-enter';
28
+ item.dataset.item = String(index);
29
+ item.dataset.span = '1';
30
+ label.textContent = `${itemPrefix} ${index}`;
31
+ item.append(label);
32
+ item.addEventListener('animationend', () => item.classList.remove('vcg-enter'));
33
+ return item;
34
+ }
35
+
36
+ function paintGridItem(item: HTMLElement, state: LayoutState) {
37
+ const span = Number(item.dataset.span ?? '1');
38
+ item.style.minWidth = `${state.itemSize}px`;
39
+ item.style.minHeight = `${state.itemSize}px`;
40
+ item.style.gridColumn = span > 1 ? `span ${span}` : '';
41
+ item.classList.toggle('vcg-spanned', span > 1);
42
+ }
43
+
44
+ function paintFlexItem(item: HTMLElement, state: LayoutState) {
45
+ item.style.minWidth = `${state.itemSize}px`;
46
+ item.style.minHeight = `${state.itemSize}px`;
47
+ item.style.gridColumn = '';
48
+ item.classList.remove('vcg-spanned');
49
+ }
50
+
51
+ export function paintCanvas(canvas: HTMLElement, state: LayoutState) {
52
+ canvas.style.display = state.mode;
53
+ canvas.style.gap = `${state.gap}px`;
54
+ canvas.style.justifyContent = state.justifyContent;
55
+ canvas.style.alignItems = state.alignItems;
56
+ canvas.style.width = `${state.width}px`;
57
+ canvas.style.minHeight = `${state.height}px`;
58
+
59
+ if (state.mode === 'grid') {
60
+ canvas.style.gridTemplateColumns = `repeat(${state.columns}, minmax(0, 1fr))`;
61
+ canvas.style.flexDirection = '';
62
+ canvas.style.flexWrap = '';
63
+ canvas.style.alignContent = '';
64
+ } else {
65
+ canvas.style.gridTemplateColumns = '';
66
+ canvas.style.flexDirection = state.flexDirection;
67
+ canvas.style.flexWrap = state.flexWrap;
68
+ canvas.style.alignContent = state.flexWrap !== 'nowrap' ? state.alignContent : '';
69
+ }
70
+
71
+ const painter = state.mode === 'grid' ? paintGridItem : paintFlexItem;
72
+ getAllItems(canvas).forEach((item) => painter(item, state));
73
+ }
74
+
75
+ export function toggleVisibility(mode: LayoutMode, wrapValue: string) {
76
+ document.querySelectorAll<HTMLElement>('.vcg-grid-only').forEach((el) => {
77
+ el.classList.toggle('vcg-hidden', mode !== 'grid');
78
+ });
79
+ document.querySelectorAll<HTMLElement>('.vcg-flex-only').forEach((el) => {
80
+ el.classList.toggle('vcg-hidden', mode !== 'flex');
81
+ });
82
+ const acw = document.getElementById('vcg-align-content-wrap');
83
+ if (acw && mode === 'flex') {
84
+ acw.classList.toggle('vcg-hidden', wrapValue === 'nowrap');
85
+ }
86
+ }
87
+
88
+ export function setLabelText(id: string, text: string) {
89
+ const el = document.getElementById(id);
90
+ if (el) el.textContent = text;
91
+ }
92
+
93
+ export function updateLabels(state: LayoutState) {
94
+ setLabelText('vcg-gap-value', `${state.gap}px`);
95
+ setLabelText('vcg-columns-value', String(state.columns));
96
+ setLabelText('vcg-width-value', `${state.width}px`);
97
+ setLabelText('vcg-height-value', `${state.height}px`);
98
+ setLabelText('vcg-item-size-value', `${state.itemSize}px`);
99
+ }
100
+
101
+ export function clearPresetActive() {
102
+ document.querySelectorAll('.vcg-preset-button').forEach((b) => b.classList.remove('vcg-preset-active'));
103
+ }