@tomako/tools-runtime 0.1.9 → 0.1.11
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 +1 -1
- package/src/components/tools/engine/result-view.tsx +9 -9
- package/src/features/tools/cold-start-channel-selector/widget/preview-panels.tsx +8 -4
- package/src/features/tools/gtm-readiness-checklist/gtm-readiness-checklist.container.tsx +2 -2
- package/src/features/tools/gtm-readiness-checklist/widget/gtm-readiness-checklist.tsx +24 -0
- package/src/features/tools/og-image-generator/og-image-generator.container.tsx +2 -8
- package/src/features/tools/og-image-generator/widget/og-image-generator.tsx +1 -1
- package/src/features/tools/product-name-generator/widget/candidate-card.tsx +4 -4
- package/src/features/tools/startup-idea-research/startup-idea-research.container.tsx +11 -21
- package/src/features/tools/startup-idea-research/widget/result-shell.tsx +5 -5
- package/src/i18n/messages/en/tools/app-icon-resizer.ts +8 -3
- package/src/i18n/messages/en/tools/competitor-analysis.ts +4 -2
- package/src/i18n/messages/en/tools/twitter-gif-downloader.ts +1 -1
package/package.json
CHANGED
|
@@ -78,7 +78,7 @@ function SectionHeading({
|
|
|
78
78
|
<Target aria-hidden className="size-4" />
|
|
79
79
|
</span>
|
|
80
80
|
) : null}
|
|
81
|
-
<h3 className={cn("font-semibold text-[#111111]", isWorkspace ? "text-base" : "text-lg")}>
|
|
81
|
+
<h3 className={cn("min-w-0 break-words font-semibold text-[#111111]", isWorkspace ? "text-base" : "text-lg")}>
|
|
82
82
|
{title}
|
|
83
83
|
</h3>
|
|
84
84
|
</div>
|
|
@@ -166,7 +166,7 @@ function BulletList({
|
|
|
166
166
|
) : (
|
|
167
167
|
<CheckCircle2 aria-hidden className="mt-0.5 size-4 shrink-0 text-[#7A9B69]" />
|
|
168
168
|
)}
|
|
169
|
-
<span>{item}</span>
|
|
169
|
+
<span className="min-w-0 break-words">{item}</span>
|
|
170
170
|
</li>
|
|
171
171
|
))}
|
|
172
172
|
</ul>
|
|
@@ -242,14 +242,14 @@ function CardListView({
|
|
|
242
242
|
<article
|
|
243
243
|
key={`${title}-${index}`}
|
|
244
244
|
className={cn(
|
|
245
|
-
"border border-[#D8D4CA] bg-white p-5",
|
|
245
|
+
"min-w-0 border border-[#D8D4CA] bg-white p-5",
|
|
246
246
|
isWorkspace
|
|
247
247
|
? "rounded-lg"
|
|
248
248
|
: "rounded-[1.25rem] shadow-[0_12px_28px_rgba(17,17,17,0.05)]",
|
|
249
249
|
)}
|
|
250
250
|
>
|
|
251
|
-
<div className="flex items-start justify-between gap-3">
|
|
252
|
-
<h4 className="text-base font-semibold leading-tight text-[#111111]">{title}</h4>
|
|
251
|
+
<div className="flex min-w-0 items-start justify-between gap-3">
|
|
252
|
+
<h4 className="min-w-0 break-words text-base font-semibold leading-tight text-[#111111]">{title}</h4>
|
|
253
253
|
<div className="flex shrink-0 items-center gap-2">
|
|
254
254
|
{meta != null && meta !== "" ? (
|
|
255
255
|
<span
|
|
@@ -270,7 +270,7 @@ function CardListView({
|
|
|
270
270
|
</div>
|
|
271
271
|
</div>
|
|
272
272
|
{typeof body === "string" && body.trim() ? (
|
|
273
|
-
<p className="mt-2 text-sm leading-6 text-[#555555]">
|
|
273
|
+
<p className="mt-2 break-words text-sm leading-6 text-[#555555]">
|
|
274
274
|
{block.card.body?.label ? (
|
|
275
275
|
<span className="font-semibold text-[#111111]">
|
|
276
276
|
{pickL10n(block.card.body.label, locale)}:{" "}
|
|
@@ -288,7 +288,7 @@ function CardListView({
|
|
|
288
288
|
{!isWorkspace ? <ListChecks aria-hidden className="size-3.5" /> : null}
|
|
289
289
|
{pickL10n(listSpec.label, locale)}
|
|
290
290
|
</p>
|
|
291
|
-
<ul className="mt-1.5 list-disc space-y-1 pl-5 text-sm leading-6 text-[#555555]">
|
|
291
|
+
<ul className="mt-1.5 list-disc space-y-1 break-words pl-5 text-sm leading-6 text-[#555555]">
|
|
292
292
|
{items.map((item) => (
|
|
293
293
|
<li key={item}>{item}</li>
|
|
294
294
|
))}
|
|
@@ -297,7 +297,7 @@ function CardListView({
|
|
|
297
297
|
);
|
|
298
298
|
})}
|
|
299
299
|
{typeof footer === "string" && footer.trim() ? (
|
|
300
|
-
<p className="mt-3 rounded-2xl border border-[#F2D2B8] bg-[#FFF7EE] px-3 py-2 text-sm leading-6 text-[#7A4B22]">
|
|
300
|
+
<p className="mt-3 break-words rounded-2xl border border-[#F2D2B8] bg-[#FFF7EE] px-3 py-2 text-sm leading-6 text-[#7A4B22]">
|
|
301
301
|
{block.card.footer?.label ? (
|
|
302
302
|
<span className="font-semibold">
|
|
303
303
|
{pickL10n(block.card.footer.label, locale)}:{" "}
|
|
@@ -503,7 +503,7 @@ export function ResultView({
|
|
|
503
503
|
variant={variant}
|
|
504
504
|
/>
|
|
505
505
|
) : (
|
|
506
|
-
<p key={entry.label} className="text-sm leading-6 text-[#3D3D3D]">
|
|
506
|
+
<p key={entry.label} className="break-words text-sm leading-6 text-[#3D3D3D]">
|
|
507
507
|
<span className="font-semibold text-[#111111]">{entry.label}: </span>
|
|
508
508
|
{entry.text}
|
|
509
509
|
</p>
|
|
@@ -75,8 +75,8 @@ function TomatoParticleLoader({ statusLabel }: { statusLabel: string }) {
|
|
|
75
75
|
|
|
76
76
|
canvas.width = width * devicePixelRatio;
|
|
77
77
|
canvas.height = height * devicePixelRatio;
|
|
78
|
-
canvas.style.width =
|
|
79
|
-
canvas.style.height =
|
|
78
|
+
canvas.style.width = "100%";
|
|
79
|
+
canvas.style.height = "100%";
|
|
80
80
|
context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
|
|
81
81
|
context.imageSmoothingEnabled = false;
|
|
82
82
|
|
|
@@ -172,8 +172,12 @@ function TomatoParticleLoader({ statusLabel }: { statusLabel: string }) {
|
|
|
172
172
|
}, []);
|
|
173
173
|
|
|
174
174
|
return (
|
|
175
|
-
<div
|
|
176
|
-
|
|
175
|
+
<div
|
|
176
|
+
aria-label={statusLabel}
|
|
177
|
+
className="mx-auto flex aspect-[32/19] w-full max-w-[320px] items-center justify-center overflow-hidden"
|
|
178
|
+
role="status"
|
|
179
|
+
>
|
|
180
|
+
<canvas ref={canvasRef} aria-hidden className="block h-full w-full" />
|
|
177
181
|
</div>
|
|
178
182
|
);
|
|
179
183
|
}
|
|
@@ -24,11 +24,11 @@ export async function GtmReadinessChecklistContainer({ locale, spec }: ToolPageP
|
|
|
24
24
|
"toolsPages.gtmReadinessChecklist.guide",
|
|
25
25
|
);
|
|
26
26
|
return (
|
|
27
|
-
<ToolPageShell locale={locale} spec={spec}>
|
|
27
|
+
<ToolPageShell locale={locale} spec={spec} showDefaultHeader={false}>
|
|
28
28
|
<ToolWorkspaceSection
|
|
29
29
|
heading={t("workspaceTitle")}
|
|
30
30
|
hint={t("workspaceHint")}
|
|
31
|
-
|
|
31
|
+
headingLevel="h1"
|
|
32
32
|
>
|
|
33
33
|
<GtmReadinessChecklistWidget />
|
|
34
34
|
</ToolWorkspaceSection>
|
|
@@ -38,6 +38,30 @@ export function GtmReadinessChecklistWidget() {
|
|
|
38
38
|
|
|
39
39
|
return (
|
|
40
40
|
<section className="overflow-hidden rounded-[1.5rem] border border-[#E8E4DB] bg-white shadow-[0_18px_50px_rgba(17,17,17,0.06)]">
|
|
41
|
+
<div
|
|
42
|
+
aria-live="polite"
|
|
43
|
+
className="sticky top-14 z-20 border-b border-[#CFD9EE] bg-[#DAE3F6]/95 px-4 py-3 backdrop-blur lg:hidden"
|
|
44
|
+
>
|
|
45
|
+
<div className="flex items-center justify-between gap-4">
|
|
46
|
+
<div className="min-w-0">
|
|
47
|
+
<p className="text-xs font-semibold uppercase tracking-[0.06em] text-[#555555]">
|
|
48
|
+
{t("score")}
|
|
49
|
+
</p>
|
|
50
|
+
<p className="mt-1 truncate text-sm font-semibold text-[#111111]">
|
|
51
|
+
{result.tierLabel}
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
<p className="shrink-0 text-3xl font-semibold leading-none text-[#111111]">
|
|
55
|
+
{result.score}<span className="ml-1 text-base text-[#555555]">%</span>
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-white/80">
|
|
59
|
+
<div
|
|
60
|
+
className="h-full rounded-full bg-[#111111] transition-[width]"
|
|
61
|
+
style={{ width: `${result.score}%` }}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
41
65
|
<div className="grid lg:grid-cols-[1.12fr_0.88fr]">
|
|
42
66
|
<div className="bg-white p-5 md:p-7">
|
|
43
67
|
<p className="mb-6 max-w-2xl text-sm leading-6 text-[#555555]">{t("howToUse")}</p>
|
|
@@ -24,17 +24,11 @@ export async function OgImageGeneratorContainer({ locale, spec }: ToolPageProps)
|
|
|
24
24
|
);
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
|
-
<ToolPageShell
|
|
28
|
-
locale={locale}
|
|
29
|
-
spec={spec}
|
|
30
|
-
heroVariant="immersive"
|
|
31
|
-
heroBackgroundImage="/tools/og-image-generator-hero.webp"
|
|
32
|
-
heroBackgroundAlt={t("heroVisualAlt")}
|
|
33
|
-
>
|
|
27
|
+
<ToolPageShell locale={locale} spec={spec} showDefaultHeader={false}>
|
|
34
28
|
<ToolWorkspaceSection
|
|
35
29
|
heading={t("workspaceHeading")}
|
|
36
30
|
hint={t("workspaceHint")}
|
|
37
|
-
|
|
31
|
+
headingLevel="h1"
|
|
38
32
|
>
|
|
39
33
|
<OgImageGenerator />
|
|
40
34
|
</ToolWorkspaceSection>
|
|
@@ -320,7 +320,7 @@ export function OgImageGenerator() {
|
|
|
320
320
|
return (
|
|
321
321
|
<>
|
|
322
322
|
<ToolWorkspaceLayout
|
|
323
|
-
className="rounded-lg border-[#E5E5E5] shadow-[0_24px_70px_rgba(17,17,17,0.08)]"
|
|
323
|
+
className="rounded-lg border-[#E5E5E5] shadow-[0_24px_70px_rgba(17,17,17,0.08)] lg:h-[min(80vh,900px)] lg:min-h-[720px]"
|
|
324
324
|
form={
|
|
325
325
|
<form onSubmit={(event) => void submit(event)} className="grid gap-5">
|
|
326
326
|
<div>
|
|
@@ -49,9 +49,9 @@ export function CandidateCard({
|
|
|
49
49
|
: "";
|
|
50
50
|
|
|
51
51
|
return (
|
|
52
|
-
<article className="rounded-[1.35rem] border border-[#D8D4CA] bg-white p-5 shadow-[0_14px_34px_rgba(17,17,17,0.07)]">
|
|
52
|
+
<article className="min-w-0 rounded-[1.35rem] border border-[#D8D4CA] bg-white p-5 shadow-[0_14px_34px_rgba(17,17,17,0.07)]">
|
|
53
53
|
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
|
54
|
-
<div>
|
|
54
|
+
<div className="min-w-0">
|
|
55
55
|
<div className="flex flex-wrap items-center gap-2">
|
|
56
56
|
<span className="rounded-full bg-[#111111] px-3 py-1.5 text-xs font-semibold text-white">
|
|
57
57
|
{copy.stages[candidate.stage] ??
|
|
@@ -71,7 +71,7 @@ export function CandidateCard({
|
|
|
71
71
|
</span>
|
|
72
72
|
) : null}
|
|
73
73
|
</div>
|
|
74
|
-
<h3 className="mt-4 text-3xl font-semibold leading-none text-[#111111]">
|
|
74
|
+
<h3 className="mt-4 break-words text-3xl font-semibold leading-none text-[#111111]">
|
|
75
75
|
{candidate.name}
|
|
76
76
|
</h3>
|
|
77
77
|
</div>
|
|
@@ -131,7 +131,7 @@ export function CandidateCard({
|
|
|
131
131
|
{candidate.domains.slice(0, 4).map((domain) => (
|
|
132
132
|
<span
|
|
133
133
|
key={domain.domain}
|
|
134
|
-
className="rounded-full border border-[#D8D4CA] bg-[#F7F6F2] px-3 py-1.5 text-xs font-semibold text-[#555555]"
|
|
134
|
+
className="max-w-full break-all rounded-full border border-[#D8D4CA] bg-[#F7F6F2] px-3 py-1.5 text-xs font-semibold text-[#555555]"
|
|
135
135
|
>
|
|
136
136
|
{domain.domain}: {domain.status}
|
|
137
137
|
</span>
|
|
@@ -25,31 +25,13 @@ export async function StartupIdeaResearchContainer({ locale, spec }: ToolPagePro
|
|
|
25
25
|
const faqItems = t.raw("faqItems") as readonly ToolFaqItem[];
|
|
26
26
|
|
|
27
27
|
return (
|
|
28
|
-
<ToolPageShell
|
|
29
|
-
locale={locale}
|
|
30
|
-
spec={spec}
|
|
31
|
-
heroVariant="showcase"
|
|
32
|
-
heroVisual={
|
|
33
|
-
<ToolImageVisual
|
|
34
|
-
src="/tools/startup-idea-research-hero.webp"
|
|
35
|
-
alt={t("visualAlt.hero")}
|
|
36
|
-
priority
|
|
37
|
-
wide
|
|
38
|
-
/>
|
|
39
|
-
}
|
|
40
|
-
>
|
|
28
|
+
<ToolPageShell locale={locale} spec={spec} showDefaultHeader={false}>
|
|
41
29
|
<ToolWorkspaceSection
|
|
42
30
|
heading={t("workspaceHeading")}
|
|
43
31
|
hint={t("workspaceHint")}
|
|
44
|
-
|
|
32
|
+
headingLevel="h1"
|
|
45
33
|
>
|
|
46
|
-
<
|
|
47
|
-
<StartupIdeaResearch />
|
|
48
|
-
<ToolImageVisual
|
|
49
|
-
src="/tools/startup-idea-research-workflow.webp"
|
|
50
|
-
alt={t("visualAlt.workspace")}
|
|
51
|
-
/>
|
|
52
|
-
</div>
|
|
34
|
+
<StartupIdeaResearch />
|
|
53
35
|
</ToolWorkspaceSection>
|
|
54
36
|
|
|
55
37
|
<section className="border-t border-[#E8E4DB] py-12 md:py-16">
|
|
@@ -60,6 +42,14 @@ export async function StartupIdeaResearchContainer({ locale, spec }: ToolPagePro
|
|
|
60
42
|
<div className="mt-7">
|
|
61
43
|
<ToolPlainTextGrid items={values} columns="four" />
|
|
62
44
|
</div>
|
|
45
|
+
<div className="mt-8">
|
|
46
|
+
<ToolImageVisual
|
|
47
|
+
src="/tools/startup-idea-research-workflow.webp"
|
|
48
|
+
alt={t("visualAlt.workspace")}
|
|
49
|
+
priority
|
|
50
|
+
wide
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
63
53
|
</section>
|
|
64
54
|
|
|
65
55
|
<section className="grid gap-8 border-t border-[#E8E4DB] py-12 md:grid-cols-[0.95fr_1.05fr] md:items-center md:py-16">
|
|
@@ -209,9 +209,9 @@ export function Report({
|
|
|
209
209
|
}}
|
|
210
210
|
/>
|
|
211
211
|
</Field>
|
|
212
|
-
<article className="rounded-[1.25rem] border border-[#E2E6EC] bg-[#FAFBFC] p-5">
|
|
213
|
-
<h4 className="text-xl font-semibold text-[#111111]">{postTitle}</h4>
|
|
214
|
-
<p className="mt-3 whitespace-pre-line text-sm leading-6 text-[#3D3D3D]">{postBody}</p>
|
|
212
|
+
<article className="min-w-0 rounded-[1.25rem] border border-[#E2E6EC] bg-[#FAFBFC] p-5">
|
|
213
|
+
<h4 className="break-words text-xl font-semibold text-[#111111]">{postTitle}</h4>
|
|
214
|
+
<p className="mt-3 whitespace-pre-line break-words text-sm leading-6 text-[#3D3D3D]">{postBody}</p>
|
|
215
215
|
<div className="mt-4 flex flex-wrap gap-3">
|
|
216
216
|
<Button variant="outline" onClick={() => void copyRecruitmentText()}>
|
|
217
217
|
<Copy className="size-4" aria-hidden />
|
|
@@ -283,7 +283,7 @@ export function Report({
|
|
|
283
283
|
<h3 className="text-sm font-semibold uppercase tracking-[0.06em] text-[#55708A]">{t("sections.sources")}</h3>
|
|
284
284
|
<div className="mt-4 grid gap-2">
|
|
285
285
|
{data.sources.length ? data.sources.map((source) => (
|
|
286
|
-
<a key={`${source.label}-${source.url ?? ""}`} href={source.url} target="_blank" rel="noreferrer nofollow" className="text-sm font-medium text-[#075DF5] hover:underline">
|
|
286
|
+
<a key={`${source.label}-${source.url ?? ""}`} href={source.url} target="_blank" rel="noreferrer nofollow" className="break-all text-sm font-medium text-[#075DF5] hover:underline">
|
|
287
287
|
{source.label}
|
|
288
288
|
</a>
|
|
289
289
|
)) : <p className="text-sm text-[#777777]">{t("emptyList")}</p>}
|
|
@@ -313,7 +313,7 @@ export function InsightCard({
|
|
|
313
313
|
</h3>
|
|
314
314
|
<ul className="mt-4 grid gap-3">
|
|
315
315
|
{items.length ? items.slice(0, 7).map((item) => (
|
|
316
|
-
<li key={item} className="text-sm leading-6 text-[#3D3D3D]">
|
|
316
|
+
<li key={item} className="break-words text-sm leading-6 text-[#3D3D3D]">
|
|
317
317
|
{item}
|
|
318
318
|
</li>
|
|
319
319
|
)) : <li className="text-sm text-[#777777]">{empty}</li>}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
const appIconResizer = {
|
|
2
2
|
meta: {
|
|
3
|
-
title: "App Icon Resizer for iOS and Android",
|
|
3
|
+
title: "App Icon Generator & Resizer for iOS and Android",
|
|
4
|
+
displayTitle: "App Icon Generator for iOS and Android",
|
|
4
5
|
description:
|
|
5
6
|
"Upload one square app icon and locally generate an Xcode AppIcon, Google Play 512 icon, and Android legacy launcher package, then review and download one ZIP.",
|
|
6
7
|
intro:
|
|
7
8
|
"Upload one 1024px+ square source and locally generate Xcode, Google Play, and Android legacy launcher files.",
|
|
8
9
|
category: "App submission tools",
|
|
9
10
|
keywords: [
|
|
11
|
+
"app icon generator",
|
|
10
12
|
"app icon resizer",
|
|
13
|
+
"iOS app icon generator",
|
|
14
|
+
"Android app icon generator",
|
|
15
|
+
"app icon sizes",
|
|
11
16
|
"App Store icon",
|
|
12
17
|
"Google Play icon",
|
|
13
18
|
"iOS AppIcon",
|
|
@@ -21,13 +26,13 @@ const appIconResizer = {
|
|
|
21
26
|
guide: {
|
|
22
27
|
heroVisualAlt:
|
|
23
28
|
"App icon asset package visual showing one source icon exported into store submission, Xcode, and Android launcher files.",
|
|
24
|
-
workspaceHeading: "App Icon
|
|
29
|
+
workspaceHeading: "App Icon Generator and Asset Package",
|
|
25
30
|
workspaceHint:
|
|
26
31
|
"One source, one local ZIP for store and engineering handoff.",
|
|
27
32
|
workspaceBadge: "PNG + ZIP",
|
|
28
33
|
standard: {
|
|
29
34
|
benefits: {
|
|
30
|
-
title: "Why use this app icon
|
|
35
|
+
title: "Why use this app icon generator",
|
|
31
36
|
cards: [
|
|
32
37
|
{
|
|
33
38
|
title: "Generate the handoff at once",
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
const competitorAnalysis = {
|
|
2
2
|
meta: {
|
|
3
|
-
title: "AI Competitor Analysis Tool
|
|
3
|
+
title: "AI Competitor Analysis Tool with Sources",
|
|
4
|
+
displayTitle: "AI Competitor Analysis Tool",
|
|
4
5
|
description: "Enter a competitor URL and get a sourced profile of its product, traffic, growth, business scale, team, and funding, with confidence shown for each finding.",
|
|
5
6
|
intro: "Turn a single competitor URL into a one-page profile. Built from public web and search signals, every dimension shows its confidence so you can act on positioning, growth, and competitive decisions.",
|
|
6
7
|
category: "Market research tools",
|
|
7
8
|
keywords: [
|
|
8
9
|
"competitor analysis tool",
|
|
10
|
+
"competitor research tool",
|
|
11
|
+
"competitive analysis tool",
|
|
9
12
|
"competitor analysis",
|
|
10
|
-
"competitor research",
|
|
11
13
|
"competitor website traffic",
|
|
12
14
|
"competitor funding lookup",
|
|
13
15
|
],
|
|
@@ -107,7 +107,7 @@ const twitterGifDownloader = {
|
|
|
107
107
|
{
|
|
108
108
|
question: "Why does a Twitter GIF need conversion?",
|
|
109
109
|
answer:
|
|
110
|
-
"Twitter/X usually serves
|
|
110
|
+
"Twitter/X usually serves GIF-style posts as MP4 animation files. This tool re-encodes that MP4 into a real .gif at 12 fps and up to 720px wide, so the GIF is not the original uploaded byte stream or an original-fidelity export.",
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
question: "Does this work with x.com links?",
|