@jjlmoya/utils-developer 1.18.0 → 1.20.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 (30) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +4 -1
  3. package/src/index.ts +1 -0
  4. package/src/tests/locale_completeness.test.ts +2 -2
  5. package/src/tests/tool_validation.test.ts +2 -2
  6. package/src/tool/cssBoxShadowGenerator/bibliography.astro +14 -0
  7. package/src/tool/cssBoxShadowGenerator/bibliography.ts +14 -0
  8. package/src/tool/cssBoxShadowGenerator/box-shadow-generator.css +376 -0
  9. package/src/tool/cssBoxShadowGenerator/component.astro +234 -0
  10. package/src/tool/cssBoxShadowGenerator/entry.ts +29 -0
  11. package/src/tool/cssBoxShadowGenerator/i18n/de.ts +69 -0
  12. package/src/tool/cssBoxShadowGenerator/i18n/en.ts +71 -0
  13. package/src/tool/cssBoxShadowGenerator/i18n/es.ts +69 -0
  14. package/src/tool/cssBoxShadowGenerator/i18n/fr.ts +69 -0
  15. package/src/tool/cssBoxShadowGenerator/i18n/id.ts +69 -0
  16. package/src/tool/cssBoxShadowGenerator/i18n/it.ts +69 -0
  17. package/src/tool/cssBoxShadowGenerator/i18n/ja.ts +69 -0
  18. package/src/tool/cssBoxShadowGenerator/i18n/ko.ts +69 -0
  19. package/src/tool/cssBoxShadowGenerator/i18n/nl.ts +69 -0
  20. package/src/tool/cssBoxShadowGenerator/i18n/pl.ts +69 -0
  21. package/src/tool/cssBoxShadowGenerator/i18n/pt.ts +69 -0
  22. package/src/tool/cssBoxShadowGenerator/i18n/ru.ts +69 -0
  23. package/src/tool/cssBoxShadowGenerator/i18n/sv.ts +69 -0
  24. package/src/tool/cssBoxShadowGenerator/i18n/tr.ts +69 -0
  25. package/src/tool/cssBoxShadowGenerator/i18n/zh.ts +69 -0
  26. package/src/tool/cssBoxShadowGenerator/index.ts +9 -0
  27. package/src/tool/cssBoxShadowGenerator/logic.ts +35 -0
  28. package/src/tool/cssBoxShadowGenerator/seo.astro +15 -0
  29. package/src/tool/cssBoxShadowGenerator/ui.ts +23 -0
  30. package/src/tools.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-developer",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
package/src/entries.ts CHANGED
@@ -52,6 +52,8 @@ export { jwtDecoder } from './tool/jwtDecoder/entry';
52
52
  export type { JwtDecoderLocaleContent } from './tool/jwtDecoder/entry';
53
53
  export { visualCssGridFlexboxGenerator } from './tool/visualCssGridFlexboxGenerator/entry';
54
54
  export type { VisualCssGridFlexboxGeneratorLocaleContent } from './tool/visualCssGridFlexboxGenerator/entry';
55
+ export { cssBoxShadowGenerator } from './tool/cssBoxShadowGenerator/entry';
56
+ export type { CssBoxShadowGeneratorLocaleContent } from './tool/cssBoxShadowGenerator/entry';
55
57
  export { developerCategory } from './category';
56
58
  import { aspectRatio } from './tool/aspectRatio/entry';
57
59
  import { calculadoraTiempoDatos } from './tool/calculadoraTiempoDatos/entry';
@@ -80,4 +82,5 @@ import { utmGenerator } from './tool/utmGenerator/entry';
80
82
  import { serpPixelSimulator } from './tool/serpPixelSimulator/entry';
81
83
  import { jwtDecoder } from './tool/jwtDecoder/entry';
82
84
  import { visualCssGridFlexboxGenerator } from './tool/visualCssGridFlexboxGenerator/entry';
83
- export const ALL_ENTRIES = [aspectRatio, calculadoraTiempoDatos, colorConverter, conversorExcelCsvHtml, cronGenerator, cssSpecificityCalculator, cssToInlineConverter, duplicateCssRemover, generadorSecurityTxt, hashGenerator, inspectorCertificadosSsl, jsonFormatter, keycode, llmCostCalculator, mobileMockupGenerator, musicalTypography, placeholderGenerator, promptLibrary, readabilityCalculator, svgSanitizer, svgToCss, urlCleaner, urlEncoderDecoder, utmGenerator, serpPixelSimulator, jwtDecoder, visualCssGridFlexboxGenerator];
85
+ import { cssBoxShadowGenerator } from './tool/cssBoxShadowGenerator/entry';
86
+ export const ALL_ENTRIES = [aspectRatio, calculadoraTiempoDatos, colorConverter, conversorExcelCsvHtml, cronGenerator, cssSpecificityCalculator, cssToInlineConverter, duplicateCssRemover, generadorSecurityTxt, hashGenerator, inspectorCertificadosSsl, jsonFormatter, keycode, llmCostCalculator, mobileMockupGenerator, musicalTypography, placeholderGenerator, promptLibrary, readabilityCalculator, svgSanitizer, svgToCss, urlCleaner, urlEncoderDecoder, utmGenerator, serpPixelSimulator, jwtDecoder, visualCssGridFlexboxGenerator, cssBoxShadowGenerator];
package/src/index.ts CHANGED
@@ -70,3 +70,4 @@ export { SERP_PIXEL_SIMULATOR_TOOL } from './tool/serpPixelSimulator/index';
70
70
  export { JWT_DECODER_TOOL } from './tool/jwtDecoder/index';
71
71
 
72
72
  export { VISUAL_CSS_GRID_FLEXBOX_GENERATOR_TOOL } from './tool/visualCssGridFlexboxGenerator/index';
73
+ export { CSS_BOX_SHADOW_GENERATOR_TOOL } from './tool/cssBoxShadowGenerator/index';
@@ -35,7 +35,7 @@ describe('Locale Completeness Validation', () => {
35
35
  });
36
36
  });
37
37
 
38
- it('all 27 tools registered', () => {
39
- expect(ALL_TOOLS.length).toBe(27);
38
+ it('all 28 tools registered', () => {
39
+ expect(ALL_TOOLS.length).toBe(28);
40
40
  });
41
41
  });
@@ -4,8 +4,8 @@ import { developerCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 27 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(27);
7
+ it('should have 28 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(28);
9
9
  });
10
10
 
11
11
  it('developerCategory should be defined', () => {
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { cssBoxShadowGenerator } 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 cssBoxShadowGenerator.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,14 @@
1
+ export const bibliography = [
2
+ {
3
+ name: 'MDN: box-shadow',
4
+ url: 'https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow',
5
+ },
6
+ {
7
+ name: 'CSS-Tricks: CSS Box Shadow',
8
+ url: 'https://css-tricks.com/snippets/css/css-box-shadow/',
9
+ },
10
+ {
11
+ name: 'W3C: CSS Backgrounds and Borders Module Level 3',
12
+ url: 'https://www.w3.org/TR/css-backgrounds-3/#box-shadow',
13
+ },
14
+ ];
@@ -0,0 +1,376 @@
1
+ .bsg-root { width: 100%; }
2
+
3
+ .bsg-card {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 1rem;
7
+ width: 100%;
8
+ padding: 1.25rem;
9
+ background:
10
+ radial-gradient(circle at 20% 10%, rgba(59, 130, 246, 0.08), transparent 32%),
11
+ linear-gradient(135deg, rgb(250, 250, 252), rgb(241, 245, 249));
12
+ border: 1px solid rgb(203, 213, 225);
13
+ border-radius: 1.25rem;
14
+ box-shadow: 0 4px 24px rgba(15, 23, 42, 0.06);
15
+ }
16
+
17
+ .theme-dark .bsg-card {
18
+ background:
19
+ radial-gradient(circle at 20% 10%, rgba(59, 130, 246, 0.06), transparent 32%),
20
+ linear-gradient(135deg, rgb(12, 18, 28), rgb(20, 28, 45));
21
+ border-color: rgb(51, 65, 85);
22
+ }
23
+
24
+ /* ---- Preview ---- */
25
+
26
+ .bsg-preview-area {
27
+ display: grid;
28
+ place-items: center;
29
+ min-height: 20rem;
30
+ padding: 2.5rem;
31
+ background:
32
+ linear-gradient(90deg, rgba(148, 163, 184, 0.14) 1px, transparent 1px),
33
+ linear-gradient(rgba(148, 163, 184, 0.14) 1px, transparent 1px);
34
+ background-size: 20px 20px;
35
+ border: 1px dashed rgb(180, 192, 208);
36
+ border-radius: 1rem;
37
+ }
38
+
39
+ .theme-dark .bsg-preview-area {
40
+ background:
41
+ linear-gradient(90deg, rgba(71, 85, 105, 0.3) 1px, transparent 1px),
42
+ linear-gradient(rgba(71, 85, 105, 0.3) 1px, transparent 1px);
43
+ background-size: 20px 20px;
44
+ background-color: rgba(15, 23, 42, 0.6);
45
+ border-color: rgb(71, 85, 105);
46
+ }
47
+
48
+ .bsg-preview-box {
49
+ width: 180px;
50
+ height: 180px;
51
+ background: #fff;
52
+ border-radius: 1.5rem;
53
+ transition: box-shadow 0.15s ease;
54
+ }
55
+
56
+ .theme-dark .bsg-preview-box {
57
+ background: #fff;
58
+ box-shadow: none;
59
+ }
60
+
61
+ /* ---- Presets row ---- */
62
+
63
+ .bsg-presets-row {
64
+ display: flex;
65
+ flex-wrap: wrap;
66
+ align-items: center;
67
+ gap: 0.45rem;
68
+ }
69
+
70
+ .bsg-grow { flex: 1; }
71
+
72
+ .bsg-chip {
73
+ padding: 0.35rem 0.7rem;
74
+ font-size: 0.8rem;
75
+ font-weight: 750;
76
+ color: rgb(71, 85, 105);
77
+ cursor: pointer;
78
+ background: #fff;
79
+ border: 1px solid rgb(203, 213, 225);
80
+ border-radius: 0.5rem;
81
+ transition: all 0.15s;
82
+ }
83
+
84
+ .bsg-chip:hover {
85
+ background: rgb(219, 234, 254);
86
+ color: rgb(37, 99, 235);
87
+ border-color: rgb(147, 197, 253);
88
+ }
89
+
90
+ .bsg-chip.bsg-active {
91
+ background: rgb(59, 130, 246);
92
+ color: #fff;
93
+ border-color: rgb(59, 130, 246);
94
+ }
95
+
96
+ .bsg-icon-sm {
97
+ display: inline-flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ width: 2.1rem;
101
+ height: 2.1rem;
102
+ color: rgb(71, 85, 105);
103
+ cursor: pointer;
104
+ background: #fff;
105
+ border: 1px solid rgb(203, 213, 225);
106
+ border-radius: 0.5rem;
107
+ }
108
+
109
+ .bsg-icon-sm svg {
110
+ width: 0.95rem;
111
+ height: 0.95rem;
112
+ }
113
+
114
+ .bsg-icon-sm:hover {
115
+ background: rgb(219, 234, 254);
116
+ border-color: rgb(147, 197, 253);
117
+ }
118
+
119
+ .theme-dark .bsg-chip,
120
+ .theme-dark .bsg-icon-sm {
121
+ color: rgb(148, 163, 184);
122
+ background: rgb(30, 41, 59);
123
+ border-color: rgb(51, 65, 85);
124
+ }
125
+
126
+ .theme-dark .bsg-chip:hover,
127
+ .theme-dark .bsg-icon-sm:hover {
128
+ background: rgba(59, 130, 246, 0.18);
129
+ border-color: rgb(59, 130, 246);
130
+ color: rgb(147, 197, 253);
131
+ }
132
+
133
+ .theme-dark .bsg-chip.bsg-active {
134
+ background: rgb(59, 130, 246);
135
+ color: #fff;
136
+ border-color: rgb(59, 130, 246);
137
+ }
138
+
139
+ /* ---- Layer tabs ---- */
140
+
141
+ .bsg-layers {
142
+ display: flex;
143
+ gap: 0.4rem;
144
+ }
145
+
146
+ .bsg-layer-tab {
147
+ display: inline-flex;
148
+ align-items: center;
149
+ gap: 0.35rem;
150
+ padding: 0.3rem 0.6rem;
151
+ font-size: 0.78rem;
152
+ font-weight: 700;
153
+ color: rgb(100, 116, 139);
154
+ cursor: pointer;
155
+ background: transparent;
156
+ border: 1.5px solid transparent;
157
+ border-radius: 0.45rem;
158
+ }
159
+
160
+ .bsg-layer-tab:hover {
161
+ background: rgba(59, 130, 246, 0.06);
162
+ color: rgb(59, 130, 246);
163
+ }
164
+
165
+ .bsg-layer-tab.bsg-active {
166
+ background: rgb(219, 234, 254);
167
+ border-color: rgb(147, 197, 253);
168
+ color: rgb(37, 99, 235);
169
+ }
170
+
171
+ .bsg-dot {
172
+ display: block;
173
+ width: 0.6rem;
174
+ height: 0.6rem;
175
+ border-radius: 50%;
176
+ border: 1px solid rgba(0, 0, 0, 0.1);
177
+ }
178
+
179
+ .theme-dark .bsg-layer-tab { color: rgb(148, 163, 184); }
180
+ .theme-dark .bsg-layer-tab:hover {
181
+ color: rgb(147, 197, 253);
182
+ background: rgba(59, 130, 246, 0.1);
183
+ }
184
+ .theme-dark .bsg-layer-tab.bsg-active {
185
+ background: rgba(59, 130, 246, 0.15);
186
+ border-color: rgb(59, 130, 246);
187
+ color: rgb(147, 197, 253);
188
+ }
189
+
190
+ /* ---- Sliders ---- */
191
+
192
+ .bsg-sliders {
193
+ display: grid;
194
+ grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
195
+ gap: 0.65rem;
196
+ }
197
+
198
+ .bsg-slider {
199
+ display: flex;
200
+ flex-direction: column;
201
+ gap: 0.3rem;
202
+ padding: 0.55rem 0.65rem;
203
+ background: rgba(248, 250, 252, 0.8);
204
+ border: 1px solid rgba(203, 213, 225, 0.7);
205
+ border-radius: 0.6rem;
206
+ }
207
+
208
+ .bsg-slider label {
209
+ display: flex;
210
+ justify-content: space-between;
211
+ align-items: center;
212
+ font-size: 0.72rem;
213
+ font-weight: 800;
214
+ color: rgb(100, 116, 139);
215
+ text-transform: uppercase;
216
+ letter-spacing: 0.03em;
217
+ }
218
+
219
+ .bsg-slider output {
220
+ font-size: 0.82rem;
221
+ font-weight: 750;
222
+ color: rgb(30, 41, 59);
223
+ font-variant-numeric: tabular-nums;
224
+ }
225
+
226
+ .bsg-slider input[type="range"] {
227
+ width: 100%;
228
+ height: 0.4rem;
229
+ accent-color: rgb(59, 130, 246);
230
+ cursor: pointer;
231
+ }
232
+
233
+ .theme-dark .bsg-slider {
234
+ background: rgba(30, 41, 59, 0.6);
235
+ border-color: rgba(71, 85, 105, 0.6);
236
+ }
237
+
238
+ .theme-dark .bsg-slider label { color: rgb(148, 163, 184); }
239
+ .theme-dark .bsg-slider output { color: rgb(226, 232, 240); }
240
+
241
+ /* ---- Inline options ---- */
242
+
243
+ .bsg-inline-opts {
244
+ display: flex;
245
+ flex-wrap: wrap;
246
+ align-items: center;
247
+ gap: 0.85rem;
248
+ }
249
+
250
+ .bsg-switch {
251
+ display: flex;
252
+ align-items: center;
253
+ gap: 0.4rem;
254
+ font-size: 0.8rem;
255
+ font-weight: 750;
256
+ color: rgb(71, 85, 105);
257
+ cursor: pointer;
258
+ }
259
+
260
+ .bsg-switch input[type="checkbox"] {
261
+ width: 1.1rem;
262
+ height: 1.1rem;
263
+ accent-color: rgb(59, 130, 246);
264
+ cursor: pointer;
265
+ }
266
+
267
+ .bsg-color-pick {
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 0.4rem;
271
+ padding: 0.2rem 0.5rem;
272
+ background: #fff;
273
+ border: 1px solid rgb(203, 213, 225);
274
+ border-radius: 0.5rem;
275
+ }
276
+
277
+ .bsg-color-pick span {
278
+ font-size: 0.78rem;
279
+ font-weight: 750;
280
+ color: rgb(71, 85, 105);
281
+ }
282
+
283
+ .bsg-color-pick input[type="color"] {
284
+ width: 1.5rem;
285
+ height: 1.5rem;
286
+ padding: 0;
287
+ cursor: pointer;
288
+ border: none;
289
+ border-radius: 0.25rem;
290
+ background: none;
291
+ }
292
+
293
+ .bsg-color-pick code {
294
+ font-size: 0.76rem;
295
+ font-weight: 700;
296
+ color: rgb(30, 41, 59);
297
+ }
298
+
299
+ .theme-dark .bsg-switch { color: rgb(148, 163, 184); }
300
+ .theme-dark .bsg-color-pick {
301
+ background: rgb(30, 41, 59);
302
+ border-color: rgb(51, 65, 85);
303
+ }
304
+ .theme-dark .bsg-color-pick span { color: rgb(148, 163, 184); }
305
+ .theme-dark .bsg-color-pick code { color: rgb(226, 232, 240); }
306
+
307
+ /* ---- Code ---- */
308
+
309
+ .bsg-code-block {
310
+ overflow: hidden;
311
+ background: rgb(15, 23, 42);
312
+ border-radius: 0.8rem;
313
+ }
314
+
315
+ .bsg-code-bar {
316
+ display: flex;
317
+ align-items: center;
318
+ justify-content: space-between;
319
+ padding: 0.5rem 0.75rem;
320
+ border-bottom: 1px solid rgba(148, 163, 184, 0.18);
321
+ }
322
+
323
+ .bsg-code-bar span {
324
+ font-size: 0.72rem;
325
+ font-weight: 850;
326
+ color: rgb(100, 116, 139);
327
+ text-transform: uppercase;
328
+ letter-spacing: 0.05em;
329
+ }
330
+
331
+ .bsg-copy-btn {
332
+ display: inline-flex;
333
+ align-items: center;
334
+ gap: 0.3rem;
335
+ padding: 0.3rem 0.6rem;
336
+ font-size: 0.76rem;
337
+ font-weight: 750;
338
+ color: rgb(148, 163, 184);
339
+ cursor: pointer;
340
+ background: rgba(255, 255, 255, 0.05);
341
+ border: 1px solid rgba(148, 163, 184, 0.25);
342
+ border-radius: 0.4rem;
343
+ transition: all 0.15s;
344
+ }
345
+
346
+ .bsg-copy-btn svg {
347
+ width: 0.8rem;
348
+ height: 0.8rem;
349
+ }
350
+
351
+ .bsg-copy-btn:hover {
352
+ background: rgba(59, 130, 246, 0.2);
353
+ border-color: rgb(59, 130, 246);
354
+ color: rgb(147, 197, 253);
355
+ }
356
+
357
+ .bsg-copy-btn[data-copied] {
358
+ background: rgb(59, 130, 246);
359
+ border-color: rgb(59, 130, 246);
360
+ color: #fff;
361
+ }
362
+
363
+ .bsg-code {
364
+ padding: 0.75rem;
365
+ margin: 0;
366
+ min-height: 2.5rem;
367
+ overflow: auto;
368
+ font-size: 0.82rem;
369
+ line-height: 1.5;
370
+ color: rgb(203, 213, 225);
371
+ white-space: pre-wrap;
372
+ }
373
+
374
+ @media (min-width: 640px) {
375
+ .bsg-sliders { grid-template-columns: repeat(5, 1fr); }
376
+ }
@@ -0,0 +1,234 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { CssBoxShadowGeneratorUI } from './ui';
4
+ import './box-shadow-generator.css';
5
+
6
+ interface Props {
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ const t = (ui ?? {}) as CssBoxShadowGeneratorUI;
12
+ ---
13
+
14
+ <div id="bsg-root" class="bsg-root" data-i18n={JSON.stringify(t)}>
15
+ <div class="bsg-card">
16
+ <div class="bsg-preview-area">
17
+ <div id="bsg-preview" class="bsg-preview-box" aria-label={t.previewLabel}></div>
18
+ </div>
19
+
20
+ <div class="bsg-presets-row">
21
+ <button class="bsg-chip" data-preset="card" type="button">{t.presetCard}</button>
22
+ <button class="bsg-chip" data-preset="float" type="button">{t.presetFloat}</button>
23
+ <button class="bsg-chip" data-preset="inset" type="button">{t.presetInset}</button>
24
+ <button class="bsg-chip" data-preset="glow" type="button">{t.presetGlow}</button>
25
+ <button class="bsg-chip" data-preset="layered" type="button">{t.presetLayered}</button>
26
+ <span class="bsg-grow"></span>
27
+ <button id="bsg-add-layer" class="bsg-icon-sm" type="button" title={t.addLayer}><Icon name="mdi:plus" /></button>
28
+ <button id="bsg-remove-layer" class="bsg-icon-sm" type="button" title={t.removeLayer}><Icon name="mdi:minus" /></button>
29
+ <button id="bsg-reset" class="bsg-icon-sm" type="button" title={t.resetAll}><Icon name="mdi:restore" /></button>
30
+ </div>
31
+
32
+ <div class="bsg-layers" id="bsg-layers">
33
+ <button class="bsg-layer-tab bsg-active" data-layer="0">
34
+ <span class="bsg-dot" style="background:#059669"></span>{t.layerPrefix} 1
35
+ </button>
36
+ </div>
37
+
38
+ <div class="bsg-sliders">
39
+ <div class="bsg-slider">
40
+ <label for="bsg-ox">{t.offsetXLabel} <output id="bsg-ox-val">0</output></label>
41
+ <input id="bsg-ox" type="range" min="-80" max="80" value="0" />
42
+ </div>
43
+ <div class="bsg-slider">
44
+ <label for="bsg-oy">{t.offsetYLabel} <output id="bsg-oy-val">16</output></label>
45
+ <input id="bsg-oy" type="range" min="-80" max="80" value="16" />
46
+ </div>
47
+ <div class="bsg-slider">
48
+ <label for="bsg-blur">{t.blurLabel} <output id="bsg-blur-val">32</output></label>
49
+ <input id="bsg-blur" type="range" min="0" max="120" value="32" />
50
+ </div>
51
+ <div class="bsg-slider">
52
+ <label for="bsg-spread">{t.spreadLabel} <output id="bsg-spread-val">-8</output></label>
53
+ <input id="bsg-spread" type="range" min="-40" max="40" value="-8" />
54
+ </div>
55
+ <div class="bsg-slider">
56
+ <label for="bsg-opacity">{t.opacityLabel} <output id="bsg-opacity-val">0.28</output></label>
57
+ <input id="bsg-opacity" type="range" min="0" max="100" value="28" />
58
+ </div>
59
+ </div>
60
+
61
+ <div class="bsg-inline-opts">
62
+ <label class="bsg-switch">
63
+ <input id="bsg-inset" type="checkbox" />
64
+ <span>{t.insetLabel}</span>
65
+ </label>
66
+ <label class="bsg-color-pick">
67
+ <input id="bsg-color" type="color" value="#059669" />
68
+ <code id="bsg-color-hex">#059669</code>
69
+ </label>
70
+ <label class="bsg-color-pick">
71
+ <span>{t.bgColorLabel}</span>
72
+ <input id="bsg-bg" type="color" value="#ffffff" />
73
+ </label>
74
+ </div>
75
+
76
+ <div class="bsg-code-block">
77
+ <div class="bsg-code-bar">
78
+ <span>CSS</span>
79
+ <button id="bsg-copy" class="bsg-copy-btn" type="button"><Icon name="mdi:content-copy" /> {t.copyCode}</button>
80
+ </div>
81
+ <pre id="bsg-code" class="bsg-code"></pre>
82
+ </div>
83
+ </div>
84
+ </div>
85
+
86
+ <script>
87
+ import { buildShadowCss, buildShadowValue } from './logic';
88
+ import type { ShadowLayer } from './logic';
89
+ import type { CssBoxShadowGeneratorUI } from './ui';
90
+
91
+ const root = document.getElementById('bsg-root') as HTMLElement;
92
+ const t = JSON.parse(root.dataset.i18n ?? '{}') as CssBoxShadowGeneratorUI;
93
+
94
+ const preview = document.getElementById('bsg-preview') as HTMLElement;
95
+ const code = document.getElementById('bsg-code') as HTMLElement;
96
+ const ox = document.getElementById('bsg-ox') as HTMLInputElement;
97
+ const oy = document.getElementById('bsg-oy') as HTMLInputElement;
98
+ const blur = document.getElementById('bsg-blur') as HTMLInputElement;
99
+ const spread = document.getElementById('bsg-spread') as HTMLInputElement;
100
+ const opacity = document.getElementById('bsg-opacity') as HTMLInputElement;
101
+ const color = document.getElementById('bsg-color') as HTMLInputElement;
102
+ const bg = document.getElementById('bsg-bg') as HTMLInputElement;
103
+ const inset = document.getElementById('bsg-inset') as HTMLInputElement;
104
+ const layersEl = document.getElementById('bsg-layers') as HTMLElement;
105
+
106
+ let activeLayer = 0;
107
+ const layers: ShadowLayer[] = [
108
+ { offsetX: 0, offsetY: 16, blur: 32, spread: -8, color: '#059669', opacity: 0.28, inset: false },
109
+ ];
110
+
111
+ const PRESETS: Record<string, ShadowLayer[]> = {
112
+ card: [{ offsetX: 0, offsetY: 6, blur: 16, spread: -2, color: '#0f172a', opacity: 0.15, inset: false }],
113
+ float: [{ offsetX: 0, offsetY: 16, blur: 40, spread: -8, color: '#0f172a', opacity: 0.2, inset: false }],
114
+ inset: [{ offsetX: 0, offsetY: 3, blur: 10, spread: -1, color: '#0f172a', opacity: 0.18, inset: true }],
115
+ glow: [{ offsetX: 0, offsetY: 0, blur: 30, spread: 6, color: '#3b82f6', opacity: 0.45, inset: false }],
116
+ layered: [
117
+ { offsetX: 0, offsetY: 1, blur: 2, spread: 0, color: '#0f172a', opacity: 0.12, inset: false },
118
+ { offsetX: 0, offsetY: 10, blur: 28, spread: -6, color: '#0f172a', opacity: 0.14, inset: false },
119
+ { offsetX: 0, offsetY: 28, blur: 60, spread: -12, color: '#0f172a', opacity: 0.08, inset: false },
120
+ ],
121
+ };
122
+
123
+ function readLayer(): ShadowLayer {
124
+ return {
125
+ offsetX: Number(ox.value), offsetY: Number(oy.value),
126
+ blur: Number(blur.value), spread: Number(spread.value),
127
+ color: color.value, opacity: Number(opacity.value) / 100,
128
+ inset: inset.checked,
129
+ };
130
+ }
131
+
132
+ function showLayer(l: ShadowLayer) {
133
+ ox.value = String(l.offsetX); oy.value = String(l.offsetY);
134
+ blur.value = String(l.blur); spread.value = String(l.spread);
135
+ color.value = l.color; opacity.value = String(Math.round(l.opacity * 100));
136
+ inset.checked = l.inset;
137
+ }
138
+
139
+ function sync() {
140
+ const l = layers[activeLayer];
141
+ (document.getElementById('bsg-ox-val')!).textContent = String(l.offsetX);
142
+ (document.getElementById('bsg-oy-val')!).textContent = String(l.offsetY);
143
+ (document.getElementById('bsg-blur-val')!).textContent = String(l.blur);
144
+ (document.getElementById('bsg-spread-val')!).textContent = String(l.spread);
145
+ (document.getElementById('bsg-opacity-val')!).textContent = l.opacity.toFixed(2);
146
+ (document.getElementById('bsg-color-hex')!).textContent = l.color;
147
+ preview.style.boxShadow = buildShadowValue(layers);
148
+ preview.style.backgroundColor = bg.value;
149
+ code.textContent = buildShadowCss(layers);
150
+ }
151
+
152
+ function renderTabs() {
153
+ layersEl.innerHTML = '';
154
+ layers.forEach((l, i) => {
155
+ const btn = document.createElement('button');
156
+ btn.className = 'bsg-layer-tab' + (i === activeLayer ? ' bsg-active' : '');
157
+ const dot = document.createElement('span');
158
+ dot.className = 'bsg-dot';
159
+ dot.style.backgroundColor = l.color;
160
+ btn.append(dot, `${t.layerPrefix} ${i + 1}`);
161
+ btn.addEventListener('click', () => {
162
+ layers[activeLayer] = readLayer();
163
+ activeLayer = i;
164
+ showLayer(layers[i]);
165
+ renderTabs();
166
+ sync();
167
+ });
168
+ layersEl.append(btn);
169
+ });
170
+ }
171
+
172
+ function onInput() { layers[activeLayer] = readLayer(); sync(); }
173
+
174
+ function addLayer() {
175
+ if (layers.length >= 5) return;
176
+ layers[activeLayer] = readLayer();
177
+ layers.push({ offsetX: 4, offsetY: 4, blur: 10, spread: 0, color: '#6366f1', opacity: 0.35, inset: false });
178
+ activeLayer = layers.length - 1;
179
+ showLayer(layers[activeLayer]);
180
+ renderTabs();
181
+ sync();
182
+ }
183
+
184
+ function removeLayer() {
185
+ if (layers.length <= 1) return;
186
+ layers.splice(activeLayer, 1);
187
+ if (activeLayer >= layers.length) activeLayer = layers.length - 1;
188
+ showLayer(layers[activeLayer]);
189
+ renderTabs();
190
+ sync();
191
+ }
192
+
193
+ function resetAll() {
194
+ layers.splice(0, layers.length, { offsetX: 0, offsetY: 16, blur: 32, spread: -8, color: '#059669', opacity: 0.28, inset: false });
195
+ activeLayer = 0;
196
+ bg.value = '#ffffff';
197
+ showLayer(layers[0]);
198
+ document.querySelectorAll('.bsg-chip').forEach((b) => b.classList.remove('bsg-active'));
199
+ renderTabs();
200
+ sync();
201
+ }
202
+
203
+ [ox, oy, blur, spread, opacity, color, inset, bg].forEach((el) => {
204
+ el.addEventListener('input', onInput);
205
+ });
206
+
207
+ document.getElementById('bsg-add-layer')!.addEventListener('click', addLayer);
208
+ document.getElementById('bsg-remove-layer')!.addEventListener('click', removeLayer);
209
+ document.getElementById('bsg-reset')!.addEventListener('click', resetAll);
210
+
211
+ document.querySelectorAll('.bsg-chip').forEach((btn) => {
212
+ btn.addEventListener('click', () => {
213
+ const name = (btn as HTMLElement).dataset.preset;
214
+ if (!name || !PRESETS[name]) return;
215
+ document.querySelectorAll('.bsg-chip').forEach((b) => b.classList.remove('bsg-active'));
216
+ btn.classList.add('bsg-active');
217
+ layers.splice(0, layers.length, ...PRESETS[name].map((l) => ({ ...l })));
218
+ activeLayer = 0;
219
+ showLayer(layers[0]);
220
+ renderTabs();
221
+ sync();
222
+ });
223
+ });
224
+
225
+ const copyBtn = document.getElementById('bsg-copy')!;
226
+ copyBtn.addEventListener('click', async () => {
227
+ await navigator.clipboard.writeText(code.textContent ?? '');
228
+ copyBtn.dataset.copied = t.copied;
229
+ window.setTimeout(() => { delete copyBtn.dataset.copied; }, 1400);
230
+ });
231
+
232
+ renderTabs();
233
+ sync();
234
+ </script>