@jjlmoya/utils-developer 1.30.0 → 1.32.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 +70 -70
- package/src/category/i18n/en.ts +1 -1
- package/src/category/i18n/es.ts +1 -1
- package/src/category/index.ts +2 -1
- package/src/category/seo.astro +1 -1
- package/src/components/PreviewNavSidebar.astro +1 -1
- package/src/components/PreviewToolbar.astro +1 -1
- package/src/data.ts +1 -1
- package/src/entries.ts +2 -0
- package/src/env.d.ts +1 -1
- package/src/index.ts +1 -0
- package/src/layouts/PreviewLayout.astro +1 -1
- package/src/pages/[locale]/[slug].astro +161 -161
- package/src/pages/index.astro +1 -1
- package/src/tests/bibliography_wellformed_export.test.ts +46 -0
- package/src/tests/category_seo_quality.test.ts +76 -0
- package/src/tests/entry_contract.test.ts +8 -2
- package/src/tests/faq_count.test.ts +1 -1
- package/src/tests/i18n_coverage.test.ts +2 -6
- package/src/tests/inverted_punctuation.test.ts +113 -113
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/mocks/astro_mock.js +1 -1
- package/src/tests/no_h1_in_components.test.ts +1 -1
- package/src/tests/promote_this_website_i18n.test.ts +33 -0
- package/src/tests/seo_translation_completeness.test.ts +69 -0
- package/src/tests/seo_wellformed_export.test.ts +65 -0
- package/src/tests/slug_language_code_format.test.ts +25 -25
- package/src/tests/slug_uniqueness.test.ts +86 -86
- package/src/tests/tool_validation.test.ts +16 -16
- package/src/tool/aspectRatio/i18n/de.ts +164 -164
- package/src/tool/aspectRatio/i18n/ja.ts +1 -1
- package/src/tool/aspectRatio/i18n/ko.ts +1 -1
- package/src/tool/aspectRatio/i18n/pl.ts +164 -164
- package/src/tool/aspectRatio/i18n/ru.ts +164 -164
- package/src/tool/aspectRatio/i18n/zh.ts +165 -165
- package/src/tool/aspectRatio/seo.astro +15 -15
- package/src/tool/calculadoraTiempoDatos/i18n/de.ts +143 -143
- package/src/tool/calculadoraTiempoDatos/i18n/ja.ts +1 -1
- package/src/tool/calculadoraTiempoDatos/i18n/ko.ts +1 -1
- package/src/tool/calculadoraTiempoDatos/i18n/pl.ts +143 -143
- package/src/tool/calculadoraTiempoDatos/i18n/ru.ts +144 -144
- package/src/tool/calculadoraTiempoDatos/i18n/sv.ts +143 -143
- package/src/tool/calculadoraTiempoDatos/i18n/zh.ts +144 -144
- package/src/tool/calculadoraTiempoDatos/seo.astro +15 -15
- package/src/tool/colorConverter/i18n/de.ts +120 -120
- package/src/tool/colorConverter/i18n/ja.ts +1 -1
- package/src/tool/colorConverter/i18n/ko.ts +1 -1
- package/src/tool/colorConverter/i18n/ru.ts +120 -120
- package/src/tool/colorConverter/i18n/zh.ts +1 -1
- package/src/tool/colorConverter/seo.astro +15 -15
- package/src/tool/conversorExcelCsvHtml/i18n/ja.ts +1 -1
- package/src/tool/conversorExcelCsvHtml/i18n/ko.ts +1 -1
- package/src/tool/conversorExcelCsvHtml/i18n/pl.ts +124 -124
- package/src/tool/conversorExcelCsvHtml/i18n/ru.ts +124 -124
- package/src/tool/conversorExcelCsvHtml/i18n/zh.ts +125 -125
- package/src/tool/conversorExcelCsvHtml/seo.astro +15 -15
- package/src/tool/cronGenerator/i18n/ja.ts +1 -1
- package/src/tool/cronGenerator/i18n/ko.ts +1 -1
- package/src/tool/cronGenerator/i18n/zh.ts +183 -183
- package/src/tool/cronGenerator/seo.astro +15 -15
- package/src/tool/cssSpecificityCalculator/i18n/ja.ts +1 -1
- package/src/tool/cssSpecificityCalculator/i18n/ko.ts +1 -1
- package/src/tool/cssSpecificityCalculator/i18n/zh.ts +1 -1
- package/src/tool/cssSpecificityCalculator/seo.astro +15 -15
- package/src/tool/cssToInlineConverter/i18n/ja.ts +1 -1
- package/src/tool/cssToInlineConverter/i18n/ko.ts +1 -1
- package/src/tool/cssToInlineConverter/i18n/zh.ts +1 -1
- package/src/tool/cssToInlineConverter/seo.astro +15 -15
- package/src/tool/dualOsIconPreview/assets/day-check.webp +0 -0
- package/src/tool/dualOsIconPreview/assets/fast-task.webp +0 -0
- package/src/tool/dualOsIconPreview/assets/fortune-cookie.webp +0 -0
- package/src/tool/dualOsIconPreview/assets/lexi-crash.webp +0 -0
- package/src/tool/dualOsIconPreview/assets/pizzametrics.webp +0 -0
- package/src/tool/dualOsIconPreview/assets/sauce-lab.webp +0 -0
- package/src/tool/dualOsIconPreview/assets/vesp.webp +0 -0
- package/src/tool/dualOsIconPreview/bibliography.astro +6 -0
- package/src/tool/dualOsIconPreview/bibliography.ts +7 -0
- package/src/tool/dualOsIconPreview/component.astro +117 -0
- package/src/tool/dualOsIconPreview/controller.ts +157 -0
- package/src/tool/dualOsIconPreview/dom-views.ts +82 -0
- package/src/tool/dualOsIconPreview/dual-ios-android-app-icon-preview.css +861 -0
- package/src/tool/dualOsIconPreview/entry.ts +31 -0
- package/src/tool/dualOsIconPreview/evaluator.ts +20 -0
- package/src/tool/dualOsIconPreview/i18n/de.ts +56 -0
- package/src/tool/dualOsIconPreview/i18n/en.ts +46 -0
- package/src/tool/dualOsIconPreview/i18n/es.ts +56 -0
- package/src/tool/dualOsIconPreview/i18n/fr.ts +56 -0
- package/src/tool/dualOsIconPreview/i18n/id.ts +56 -0
- package/src/tool/dualOsIconPreview/i18n/it.ts +56 -0
- package/src/tool/dualOsIconPreview/i18n/ja.ts +56 -0
- package/src/tool/dualOsIconPreview/i18n/ko.ts +56 -0
- package/src/tool/dualOsIconPreview/i18n/nl.ts +56 -0
- package/src/tool/dualOsIconPreview/i18n/pl.ts +56 -0
- package/src/tool/dualOsIconPreview/i18n/pt.ts +56 -0
- package/src/tool/dualOsIconPreview/i18n/ru.ts +56 -0
- package/src/tool/dualOsIconPreview/i18n/sv.ts +56 -0
- package/src/tool/dualOsIconPreview/i18n/tr.ts +43 -0
- package/src/tool/dualOsIconPreview/i18n/zh.ts +43 -0
- package/src/tool/dualOsIconPreview/index.ts +11 -0
- package/src/tool/dualOsIconPreview/logic.test.ts +63 -0
- package/src/tool/dualOsIconPreview/logic.ts +135 -0
- package/src/tool/dualOsIconPreview/seo.astro +15 -0
- package/src/tool/dualOsIconPreview/storage.ts +18 -0
- package/src/tool/dualOsIconPreview/ui.ts +52 -0
- package/src/tool/duplicateCssRemover/i18n/ja.ts +1 -1
- package/src/tool/duplicateCssRemover/i18n/ko.ts +1 -1
- package/src/tool/duplicateCssRemover/i18n/zh.ts +1 -1
- package/src/tool/duplicateCssRemover/seo.astro +15 -15
- package/src/tool/generadorSecurityTxt/i18n/ja.ts +1 -1
- package/src/tool/generadorSecurityTxt/i18n/ko.ts +1 -1
- package/src/tool/generadorSecurityTxt/i18n/zh.ts +1 -1
- package/src/tool/generadorSecurityTxt/seo.astro +15 -15
- package/src/tool/hashGenerator/i18n/ja.ts +1 -1
- package/src/tool/hashGenerator/i18n/ko.ts +1 -1
- package/src/tool/hashGenerator/seo.astro +15 -15
- package/src/tool/inspectorCertificadosSsl/i18n/ja.ts +1 -1
- package/src/tool/inspectorCertificadosSsl/i18n/ko.ts +1 -1
- package/src/tool/inspectorCertificadosSsl/i18n/zh.ts +1 -1
- package/src/tool/inspectorCertificadosSsl/seo.astro +15 -15
- package/src/tool/jsonFormatter/seo.astro +15 -15
- package/src/tool/jwtDecoder/i18n/es.ts +248 -248
- package/src/tool/jwtDecoder/i18n/fr.ts +248 -248
- package/src/tool/jwtDecoder/i18n/tr.ts +248 -248
- package/src/tool/keycode/seo.astro +15 -15
- package/src/tool/llmCostCalculator/i18n/fr.ts +149 -149
- package/src/tool/llmCostCalculator/i18n/ja.ts +1 -1
- package/src/tool/llmCostCalculator/i18n/ko.ts +1 -1
- package/src/tool/llmCostCalculator/seo.astro +15 -15
- package/src/tool/mobileMockupGenerator/i18n/ja.ts +1 -1
- package/src/tool/mobileMockupGenerator/i18n/ko.ts +1 -1
- package/src/tool/mobileMockupGenerator/i18n/zh.ts +1 -1
- package/src/tool/mobileMockupGenerator/seo.astro +15 -15
- package/src/tool/musicalTypography/i18n/ja.ts +1 -1
- package/src/tool/musicalTypography/i18n/ko.ts +1 -1
- package/src/tool/musicalTypography/i18n/zh.ts +1 -1
- package/src/tool/musicalTypography/seo.astro +15 -15
- package/src/tool/placeholderGenerator/i18n/ja.ts +1 -1
- package/src/tool/placeholderGenerator/i18n/ko.ts +1 -1
- package/src/tool/placeholderGenerator/i18n/zh.ts +1 -1
- package/src/tool/placeholderGenerator/seo.astro +15 -15
- package/src/tool/promoteThisWebsite/entry.ts +13 -0
- package/src/tool/promoteThisWebsite/i18n/de.ts +95 -0
- package/src/tool/promoteThisWebsite/i18n/fr.ts +95 -0
- package/src/tool/promoteThisWebsite/i18n/id.ts +95 -0
- package/src/tool/promoteThisWebsite/i18n/it.ts +95 -0
- package/src/tool/promoteThisWebsite/i18n/ja.ts +95 -0
- package/src/tool/promoteThisWebsite/i18n/ko.ts +95 -0
- package/src/tool/promoteThisWebsite/i18n/nl.ts +95 -0
- package/src/tool/promoteThisWebsite/i18n/pl.ts +95 -0
- package/src/tool/promoteThisWebsite/i18n/pt.ts +95 -0
- package/src/tool/promoteThisWebsite/i18n/ru.ts +95 -0
- package/src/tool/promoteThisWebsite/i18n/sv.ts +95 -0
- package/src/tool/promoteThisWebsite/i18n/tr.ts +95 -0
- package/src/tool/promoteThisWebsite/i18n/zh.ts +95 -0
- package/src/tool/promptLibrary/i18n/ja.ts +1 -1
- package/src/tool/promptLibrary/i18n/ko.ts +1 -1
- package/src/tool/promptLibrary/i18n/zh.ts +1 -1
- package/src/tool/promptLibrary/seo.astro +15 -15
- package/src/tool/readabilityCalculator/i18n/ja.ts +1 -1
- package/src/tool/readabilityCalculator/i18n/ko.ts +1 -1
- package/src/tool/readabilityCalculator/i18n/zh.ts +1 -1
- package/src/tool/readabilityCalculator/seo.astro +15 -15
- package/src/tool/serpPixelSimulator/entry.ts +30 -30
- package/src/tool/serpPixelSimulator/i18n/en.ts +211 -211
- package/src/tool/serpPixelSimulator/i18n/fr.ts +214 -214
- package/src/tool/serpPixelSimulator/i18n/ru.ts +214 -214
- package/src/tool/serpPixelSimulator/logic.ts +18 -18
- package/src/tool/serpPixelSimulator/serp-pixel-simulator.css +195 -195
- package/src/tool/svgSanitizer/i18n/ja.ts +1 -1
- package/src/tool/svgSanitizer/i18n/ko.ts +1 -1
- package/src/tool/svgSanitizer/seo.astro +15 -15
- package/src/tool/svgToCss/i18n/ja.ts +1 -1
- package/src/tool/svgToCss/i18n/ko.ts +1 -1
- package/src/tool/svgToCss/seo.astro +15 -15
- package/src/tool/urlCleaner/i18n/ja.ts +1 -1
- package/src/tool/urlCleaner/i18n/ko.ts +1 -1
- package/src/tool/urlCleaner/i18n/zh.ts +1 -1
- package/src/tool/urlCleaner/seo.astro +15 -15
- package/src/tool/urlEncoderDecoder/i18n/es.ts +146 -146
- package/src/tool/urlEncoderDecoder/i18n/ko.ts +1 -1
- package/src/tool/urlEncoderDecoder/seo.astro +15 -15
- package/src/tool/utmGenerator/i18n/ja.ts +1 -1
- package/src/tool/utmGenerator/i18n/ko.ts +1 -1
- package/src/tool/utmGenerator/i18n/zh.ts +1 -1
- package/src/tool/utmGenerator/seo.astro +15 -15
- package/src/tools.ts +2 -1
- package/src/types.ts +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { DualOsIconPreviewUI } from './ui';
|
|
3
|
+
import dayCheckIcon from './assets/day-check.webp';
|
|
4
|
+
import fastTaskIcon from './assets/fast-task.webp';
|
|
5
|
+
import fortuneCookieIcon from './assets/fortune-cookie.webp';
|
|
6
|
+
import lexiCrashIcon from './assets/lexi-crash.webp';
|
|
7
|
+
import pizzaMetricsIcon from './assets/pizzametrics.webp';
|
|
8
|
+
import sauceLabIcon from './assets/sauce-lab.webp';
|
|
9
|
+
import vespIcon from './assets/vesp.webp';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
ui: DualOsIconPreviewUI;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { ui } = Astro.props;
|
|
16
|
+
|
|
17
|
+
const appCatalog = [
|
|
18
|
+
{ name: 'Day Check', src: dayCheckIcon.src },
|
|
19
|
+
{ name: 'Fast Task', src: fastTaskIcon.src },
|
|
20
|
+
{ name: 'Fortune Cookie', src: fortuneCookieIcon.src },
|
|
21
|
+
{ name: 'LexiCrash', src: lexiCrashIcon.src },
|
|
22
|
+
{ name: 'Pizzametrics', src: pizzaMetricsIcon.src },
|
|
23
|
+
{ name: 'SauceLab', src: sauceLabIcon.src },
|
|
24
|
+
{ name: 'VESP', src: vespIcon.src },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const iosApps = [...appCatalog, ...appCatalog.slice(0, 4)];
|
|
28
|
+
const androidApps = [...appCatalog, ...appCatalog.slice(0, 3)];
|
|
29
|
+
const dockApps = appCatalog.slice(0, 4);
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
<div class="dual-os-icon-preview" data-icon-preview data-ios-appearance="light" data-android-shape="squircle" data-android-themed="false">
|
|
33
|
+
<section class="preview-controls" aria-label="Icon audit controls">
|
|
34
|
+
<div class="control-block control-upload">
|
|
35
|
+
<div class="control-heading"><span class="step-number">01</span><div><strong>{ui.labelUpload}</strong><small>{ui.uploadHint}</small></div></div>
|
|
36
|
+
<input class="visually-hidden" type="file" accept="image/png,image/jpeg,image/webp,image/svg+xml" data-logo-input aria-label={ui.labelUpload} />
|
|
37
|
+
<button class="upload-button" type="button" data-upload-action>
|
|
38
|
+
<span class="upload-glyph" aria-hidden="true">+</span>
|
|
39
|
+
<span><strong>{ui.uploadAction}</strong><small>{ui.dropHint}</small></span>
|
|
40
|
+
</button>
|
|
41
|
+
<p class="field-error" data-file-error role="status"></p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="control-block">
|
|
45
|
+
<div class="control-heading"><span class="step-number">02</span><div><strong>{ui.labelAppName}</strong><small>{ui.appNamePlaceholder}</small></div></div>
|
|
46
|
+
<input class="text-input" data-app-name-input type="text" maxlength="24" autocomplete="off" aria-label={ui.labelAppName} />
|
|
47
|
+
<label class="color-row"><span>{ui.labelBrandColor}</span><input data-brand-input type="color" value="#f4b942" /></label>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="control-block">
|
|
51
|
+
<div class="control-heading"><span class="step-number">03</span><div><strong>{ui.labelIosAppearance}</strong><small>{ui.iosAppearanceHint}</small></div></div>
|
|
52
|
+
<div class="choice-grid" role="group" aria-label={ui.labelIosAppearance}>
|
|
53
|
+
<button type="button" data-ios-value="default">{ui.iosDefault}</button><button type="button" data-ios-value="dark">{ui.iosDark}</button><button type="button" data-ios-value="clear">{ui.iosClear}</button><button type="button" data-ios-value="tinted">{ui.iosTinted}</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="control-block">
|
|
58
|
+
<div class="control-heading"><span class="step-number">04</span><div><strong>{ui.labelAndroidShape}</strong><small>{ui.androidShapeHint}</small></div></div>
|
|
59
|
+
<div class="choice-grid" role="group" aria-label={ui.labelAndroidShape}>
|
|
60
|
+
<button type="button" data-android-value="circle">{ui.androidCircle}</button><button type="button" data-android-value="squircle">{ui.androidSquircle}</button><button type="button" data-android-value="rounded">{ui.androidRounded}</button><button type="button" data-android-value="teardrop">{ui.androidTeardrop}</button>
|
|
61
|
+
</div>
|
|
62
|
+
<label class="toggle-row"><input data-themed-input type="checkbox" /><span><strong>{ui.labelAndroidTheme}</strong><small>{ui.androidThemeHint}</small></span></label>
|
|
63
|
+
</div>
|
|
64
|
+
</section>
|
|
65
|
+
|
|
66
|
+
<section class="preview-stage" aria-live="polite">
|
|
67
|
+
<div class="stage-topline"><span class="stage-kicker">{ui.stageKicker}</span><span class="stage-note">{ui.stageNote}</span></div>
|
|
68
|
+
<div class="device-pair">
|
|
69
|
+
<article class="device-card device-ios">
|
|
70
|
+
<div class="device-shell"><div class="device-island"></div><div class="device-screen">
|
|
71
|
+
<div class="system-status"><span>9:41</span><span>|||</span></div>
|
|
72
|
+
<div class="home-wallpaper ios-wallpaper">
|
|
73
|
+
<div class="wallpaper-glow glow-one"></div><div class="wallpaper-glow glow-two"></div>
|
|
74
|
+
<div class="home-grid ios-grid">
|
|
75
|
+
<div class="launcher-item target-item"><span class="app-tile audited-icon ios-icon launcher-icon"><img data-logo-image alt="" /><span data-empty-logo>{ui.emptyLogo}</span></span><small data-app-name></small></div>
|
|
76
|
+
{iosApps.map((app) => <div class="launcher-item"><span class="app-tile ios-icon launcher-icon"><img src={app.src} alt="" /></span><small>{app.name}</small></div>)}
|
|
77
|
+
</div>
|
|
78
|
+
<div class="device-dock">{dockApps.map((app) => <span class="app-tile ios-icon launcher-icon"><img src={app.src} alt="" /></span>)}</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="home-indicator"></div>
|
|
81
|
+
</div></div>
|
|
82
|
+
<div class="device-caption"><strong><span class="platform-dot dot-ios"></span>{ui.iosDeviceLabel}</strong><span>{ui.iosHomeLabel}</span></div>
|
|
83
|
+
</article>
|
|
84
|
+
|
|
85
|
+
<article class="device-card device-android">
|
|
86
|
+
<div class="device-shell"><div class="device-camera"></div><div class="device-screen">
|
|
87
|
+
<div class="system-status"><span>9:41</span><span>|||</span></div>
|
|
88
|
+
<div class="home-wallpaper android-wallpaper">
|
|
89
|
+
<div class="android-search"><span>G</span><small>Search</small><b>?</b></div>
|
|
90
|
+
<div class="home-grid android-grid">
|
|
91
|
+
{androidApps.slice(0, 4).map((app) => <div class="launcher-item"><span class="app-tile android-icon launcher-icon"><img src={app.src} alt="" /></span><small>{app.name}</small></div>)}
|
|
92
|
+
<div class="launcher-item target-item"><span class="app-tile audited-icon android-icon launcher-icon"><img data-logo-image alt="" /><span data-empty-logo>{ui.emptyLogo}</span></span><small data-app-name></small></div>
|
|
93
|
+
{androidApps.slice(4).map((app) => <div class="launcher-item"><span class="app-tile android-icon launcher-icon"><img src={app.src} alt="" /></span><small>{app.name}</small></div>)}
|
|
94
|
+
</div>
|
|
95
|
+
<div class="android-dock">{appCatalog.slice(0, 5).map((app) => <span class="app-tile android-icon launcher-icon"><img src={app.src} alt="" /></span>)}</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="android-navigation"><span></span><span></span><span></span></div>
|
|
98
|
+
</div></div>
|
|
99
|
+
<div class="device-caption"><strong><span class="platform-dot dot-android"></span>{ui.androidDeviceLabel}</strong><span>{ui.androidHomeLabel}</span></div>
|
|
100
|
+
</article>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<section class="audit-report" data-audit-report data-audit-state="waiting">
|
|
104
|
+
<div class="audit-heading"><div><strong>{ui.auditTitle}</strong><small>{ui.auditHint}</small></div><span class="audit-score" data-audit-score>{ui.auditWaiting}</span></div>
|
|
105
|
+
<div class="audit-grid">
|
|
106
|
+
<div class="audit-metric"><small>{ui.auditFile}</small><strong data-audit-file>{ui.auditNotChecked}</strong></div><div class="audit-metric"><small>{ui.auditAspect}</small><strong data-audit-aspect>{ui.auditNotChecked}</strong></div><div class="audit-metric"><small>{ui.auditResolution}</small><strong data-audit-resolution>{ui.auditNotChecked}</strong></div><div class="audit-metric"><small>{ui.auditMargin}</small><strong data-audit-margin>{ui.auditNotChecked}</strong></div><div class="audit-metric"><small>{ui.auditIosMask}</small><strong data-audit-ios>{ui.auditNotChecked}</strong></div><div class="audit-metric"><small>{ui.auditAndroidZone}</small><strong data-audit-android>{ui.auditNotChecked}</strong></div><div class="audit-metric"><small>{ui.auditTransparency}</small><strong data-audit-transparency>{ui.auditNotChecked}</strong></div>
|
|
107
|
+
</div>
|
|
108
|
+
</section>
|
|
109
|
+
</section>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<script is:inline type="application/json" id="dual-os-icon-preview-ui" set:html={JSON.stringify(ui)}></script>
|
|
113
|
+
<script>
|
|
114
|
+
import { initDualOsIconPreview } from './controller';
|
|
115
|
+
const payload = document.querySelector('#dual-os-icon-preview-ui')?.textContent;
|
|
116
|
+
if (payload) initDualOsIconPreview({ ui: JSON.parse(payload) });
|
|
117
|
+
</script>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { auditLogo, buildPreviewState, DEFAULT_SETTINGS, sanitiseSettings, type AndroidShape, type IconPreviewSettings, type IosAppearance, type LogoAudit, type LogoAuditMetrics } from './logic';
|
|
2
|
+
import { renderPreview } from './dom-views';
|
|
3
|
+
import { loadStoredSettings, saveStoredSettings } from './storage';
|
|
4
|
+
import type { DualOsIconPreviewUI } from './ui';
|
|
5
|
+
|
|
6
|
+
interface ControllerOptions {
|
|
7
|
+
ui: DualOsIconPreviewUI;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ControllerState {
|
|
11
|
+
settings: IconPreviewSettings;
|
|
12
|
+
logoUrl: string;
|
|
13
|
+
audit: LogoAudit | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getElement<T extends HTMLElement>(selector: string): T | null {
|
|
17
|
+
return document.querySelector<T>(selector);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readSettings(): IconPreviewSettings {
|
|
21
|
+
const stored = loadStoredSettings();
|
|
22
|
+
return sanitiseSettings({ ...DEFAULT_SETTINGS, ...stored });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function updateView(state: ControllerState, ui: DualOsIconPreviewUI): void {
|
|
26
|
+
renderPreview({ state: buildPreviewState(state.settings, Boolean(state.logoUrl), state.audit), ui, logoUrl: state.logoUrl });
|
|
27
|
+
saveStoredSettings(state.settings);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function setPressed(selector: string, attribute: string, value: string): void {
|
|
31
|
+
document.querySelectorAll<HTMLButtonElement>(selector).forEach((button) => {
|
|
32
|
+
button.setAttribute('aria-pressed', String(button.getAttribute(attribute) === value));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function bindSettingButtons(state: ControllerState, ui: DualOsIconPreviewUI): void {
|
|
37
|
+
document.querySelectorAll<HTMLButtonElement>('[data-ios-value]').forEach((button) => {
|
|
38
|
+
button.addEventListener('click', () => {
|
|
39
|
+
state.settings.iosAppearance = button.dataset.iosValue as IosAppearance;
|
|
40
|
+
setPressed('[data-ios-value]', 'data-ios-value', state.settings.iosAppearance);
|
|
41
|
+
updateView(state, ui);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
document.querySelectorAll<HTMLButtonElement>('[data-android-value]').forEach((button) => {
|
|
45
|
+
button.addEventListener('click', () => {
|
|
46
|
+
state.settings.androidShape = button.dataset.androidValue as AndroidShape;
|
|
47
|
+
setPressed('[data-android-value]', 'data-android-value', state.settings.androidShape);
|
|
48
|
+
updateView(state, ui);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function bindFileInput(state: ControllerState, ui: DualOsIconPreviewUI): void {
|
|
54
|
+
const input = getElement<HTMLInputElement>('[data-logo-input]');
|
|
55
|
+
const error = getElement<HTMLElement>('[data-file-error]');
|
|
56
|
+
input?.addEventListener('change', () => {
|
|
57
|
+
const file = input.files?.[0];
|
|
58
|
+
if (!file) return;
|
|
59
|
+
if (!file.type.startsWith('image/')) {
|
|
60
|
+
if (error) error.textContent = ui.fileError;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
readLogoFile(file).then((logo) => {
|
|
64
|
+
state.logoUrl = logo.url;
|
|
65
|
+
state.audit = logo.audit;
|
|
66
|
+
if (error) error.textContent = '';
|
|
67
|
+
updateView(state, ui);
|
|
68
|
+
}).catch(() => { if (error) error.textContent = ui.fileError; });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readDataUrl(file: File): Promise<string> {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const reader = new FileReader();
|
|
75
|
+
reader.addEventListener('load', () => typeof reader.result === 'string' ? resolve(reader.result) : reject(new Error('image-read')));
|
|
76
|
+
reader.addEventListener('error', () => reject(new Error('image-read')));
|
|
77
|
+
reader.readAsDataURL(file);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findOpaqueBounds(data: Uint8ClampedArray, width: number, height: number): LogoAuditMetrics {
|
|
82
|
+
let minX = width;
|
|
83
|
+
let minY = height;
|
|
84
|
+
let maxX = -1;
|
|
85
|
+
let maxY = -1;
|
|
86
|
+
let visiblePixels = 0;
|
|
87
|
+
for (let alphaIndex = 3; alphaIndex < data.length; alphaIndex += 4) {
|
|
88
|
+
if ((data[alphaIndex] ?? 0) <= 12) continue;
|
|
89
|
+
const pixel = (alphaIndex - 3) / 4;
|
|
90
|
+
const x = pixel % width;
|
|
91
|
+
const y = Math.floor(pixel / width);
|
|
92
|
+
minX = Math.min(minX, x);
|
|
93
|
+
minY = Math.min(minY, y);
|
|
94
|
+
maxX = Math.max(maxX, x);
|
|
95
|
+
maxY = Math.max(maxY, y);
|
|
96
|
+
visiblePixels += 1;
|
|
97
|
+
}
|
|
98
|
+
return { width, height, leftMargin: minX, rightMargin: maxX < 0 ? 0 : width - maxX - 1, topMargin: minY, bottomMargin: maxY < 0 ? 0 : height - maxY - 1, visiblePixels };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function measureLogo(url: string): Promise<LogoAuditMetrics> {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const image = new Image();
|
|
104
|
+
image.addEventListener('load', () => {
|
|
105
|
+
const canvas = document.createElement('canvas');
|
|
106
|
+
canvas.width = image.naturalWidth;
|
|
107
|
+
canvas.height = image.naturalHeight;
|
|
108
|
+
const context = canvas.getContext('2d');
|
|
109
|
+
if (!context) { reject(new Error('canvas-unavailable')); return; }
|
|
110
|
+
context.drawImage(image, 0, 0);
|
|
111
|
+
resolve(findOpaqueBounds(context.getImageData(0, 0, canvas.width, canvas.height).data, canvas.width, canvas.height));
|
|
112
|
+
});
|
|
113
|
+
image.addEventListener('error', () => reject(new Error('image-load')));
|
|
114
|
+
image.src = url;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function readLogoFile(file: File): Promise<{ url: string; audit: LogoAudit }> {
|
|
119
|
+
const url = await readDataUrl(file);
|
|
120
|
+
return { url, audit: auditLogo(await measureLogo(url)) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function bindTextInput(state: ControllerState, ui: DualOsIconPreviewUI): void {
|
|
124
|
+
getElement<HTMLInputElement>('[data-app-name-input]')?.addEventListener('input', (event) => {
|
|
125
|
+
state.settings.appName = (event.target as HTMLInputElement).value;
|
|
126
|
+
updateView(state, ui);
|
|
127
|
+
});
|
|
128
|
+
getElement<HTMLInputElement>('[data-brand-input]')?.addEventListener('input', (event) => {
|
|
129
|
+
state.settings.brandColor = (event.target as HTMLInputElement).value;
|
|
130
|
+
updateView(state, ui);
|
|
131
|
+
});
|
|
132
|
+
getElement<HTMLInputElement>('[data-themed-input]')?.addEventListener('change', (event) => {
|
|
133
|
+
state.settings.androidThemed = (event.target as HTMLInputElement).checked;
|
|
134
|
+
updateView(state, ui);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function syncInputs(settings: IconPreviewSettings): void {
|
|
139
|
+
const appName = getElement<HTMLInputElement>('[data-app-name-input]');
|
|
140
|
+
const brand = getElement<HTMLInputElement>('[data-brand-input]');
|
|
141
|
+
const themed = getElement<HTMLInputElement>('[data-themed-input]');
|
|
142
|
+
if (appName) appName.value = settings.appName;
|
|
143
|
+
if (brand) brand.value = settings.brandColor;
|
|
144
|
+
if (themed) themed.checked = settings.androidThemed;
|
|
145
|
+
setPressed('[data-ios-value]', 'data-ios-value', settings.iosAppearance);
|
|
146
|
+
setPressed('[data-android-value]', 'data-android-value', settings.androidShape);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function initDualOsIconPreview(options: ControllerOptions): void {
|
|
150
|
+
const state: ControllerState = { settings: readSettings(), logoUrl: '', audit: null };
|
|
151
|
+
syncInputs(state.settings);
|
|
152
|
+
bindSettingButtons(state, options.ui);
|
|
153
|
+
bindFileInput(state, options.ui);
|
|
154
|
+
bindTextInput(state, options.ui);
|
|
155
|
+
getElement<HTMLButtonElement>('[data-upload-action]')?.addEventListener('click', () => getElement<HTMLInputElement>('[data-logo-input]')?.click());
|
|
156
|
+
updateView(state, options.ui);
|
|
157
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { evaluatePreview } from './evaluator';
|
|
2
|
+
import { getIosSurface, type IconPreviewState, type LogoAudit } from './logic';
|
|
3
|
+
import type { DualOsIconPreviewUI } from './ui';
|
|
4
|
+
|
|
5
|
+
interface RenderPayload {
|
|
6
|
+
state: IconPreviewState;
|
|
7
|
+
ui: DualOsIconPreviewUI;
|
|
8
|
+
logoUrl: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function setText(selector: string, text: string): void {
|
|
12
|
+
document.querySelectorAll<HTMLElement>(selector).forEach((node) => { node.textContent = text; });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function setLogo(logoUrl: string, hasLogo: boolean): void {
|
|
16
|
+
document.querySelectorAll<HTMLImageElement>('[data-logo-image]').forEach((image) => {
|
|
17
|
+
image.src = logoUrl;
|
|
18
|
+
image.hidden = !hasLogo;
|
|
19
|
+
});
|
|
20
|
+
document.querySelectorAll<HTMLElement>('[data-empty-logo]').forEach((node) => { node.hidden = hasLogo; });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function renderBadges(payload: RenderPayload): void {
|
|
24
|
+
const target = document.querySelector<HTMLElement>('[data-preview-badges]');
|
|
25
|
+
if (!target) return;
|
|
26
|
+
target.replaceChildren(...evaluatePreview(payload.state, payload.ui).map((badge) => {
|
|
27
|
+
const node = document.createElement('span');
|
|
28
|
+
node.className = `preview-badge preview-badge-${badge.tone}`;
|
|
29
|
+
node.textContent = `${badge.label} / ${badge.detail}`;
|
|
30
|
+
return node;
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function setAuditValue(selector: string, value: string, status: string | null): void {
|
|
35
|
+
const node = document.querySelector<HTMLElement>(selector);
|
|
36
|
+
if (!node) return;
|
|
37
|
+
node.textContent = value;
|
|
38
|
+
if (status) {
|
|
39
|
+
node.dataset.auditStatus = status;
|
|
40
|
+
} else {
|
|
41
|
+
delete node.dataset.auditStatus;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderAuditScore(audit: LogoAudit, ui: DualOsIconPreviewUI): void {
|
|
46
|
+
setAuditValue('[data-audit-score]', `${audit.score}/100`, audit.score === 100 ? 'pass' : 'review');
|
|
47
|
+
setAuditValue('[data-audit-resolution]', audit.resolution === 'pass' ? ui.statusPass : ui.statusReview, audit.resolution);
|
|
48
|
+
setAuditValue('[data-audit-ios]', audit.iosMask === 'pass' ? ui.statusPass : ui.statusReview, audit.iosMask);
|
|
49
|
+
setAuditValue('[data-audit-android]', audit.androidSafeZone === 'pass' ? ui.statusPass : ui.statusReview, audit.androidSafeZone);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderAuditMetrics(audit: LogoAudit, ui: DualOsIconPreviewUI): void {
|
|
53
|
+
setAuditValue('[data-audit-file]', audit.dimensions, null);
|
|
54
|
+
setAuditValue('[data-audit-aspect]', audit.aspect, audit.aspect === '1:1' ? 'pass' : 'review');
|
|
55
|
+
setAuditValue('[data-audit-margin]', `${Math.round(audit.minimumMargin * 100)}%`, audit.androidSafeZone);
|
|
56
|
+
setAuditValue('[data-audit-transparency]', audit.transparency === 'transparent' ? ui.auditTransparent : ui.auditFullBleed, null);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderAudit(audit: LogoAudit | null, ui: DualOsIconPreviewUI): void {
|
|
60
|
+
const report = document.querySelector<HTMLElement>('[data-audit-report]');
|
|
61
|
+
if (!report) return;
|
|
62
|
+
report.dataset.auditState = audit ? 'ready' : 'waiting';
|
|
63
|
+
if (!audit) {
|
|
64
|
+
setAuditValue('[data-audit-score]', ui.auditWaiting, null);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
renderAuditScore(audit, ui);
|
|
68
|
+
renderAuditMetrics(audit, ui);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function renderPreview(payload: RenderPayload): void {
|
|
72
|
+
const root = document.querySelector<HTMLElement>('[data-icon-preview]');
|
|
73
|
+
if (!root) return;
|
|
74
|
+
root.dataset.iosAppearance = getIosSurface(payload.state.iosAppearance);
|
|
75
|
+
root.dataset.androidShape = payload.state.androidShape;
|
|
76
|
+
root.dataset.androidThemed = String(payload.state.androidThemed);
|
|
77
|
+
root.style.setProperty('--n-brand', payload.state.brandColor);
|
|
78
|
+
setText('[data-app-name]', payload.state.appName || payload.ui.nameFallback);
|
|
79
|
+
setLogo(payload.logoUrl, payload.state.hasLogo);
|
|
80
|
+
renderBadges(payload);
|
|
81
|
+
renderAudit(payload.state.audit, payload.ui);
|
|
82
|
+
}
|