@tomako/tools-runtime 0.1.0 → 0.1.2
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 +13 -6
- package/src/components/tomato-logo.tsx +71 -0
- package/src/components/tools/workspace/tool-workspace-layout.tsx +1 -1
- package/src/constants/related-tools.ts +4 -0
- package/src/features/tools/app-icon-resizer/app-icon-resizer.container.tsx +15 -2
- package/src/features/tools/app-icon-resizer/app-icon-resizer.spec.ts +1 -1
- package/src/features/tools/app-icon-resizer/widget/app-icon-resizer.tsx +54 -11
- package/src/features/tools/app-icon-resizer/widget/platform-icons.tsx +2 -5
- package/src/features/tools/mrr-calculator/mrr-calculator.container.tsx +23 -0
- package/src/features/tools/mrr-calculator/mrr-calculator.spec.ts +15 -0
- package/src/features/tools/mrr-calculator/widget/calculations.ts +74 -0
- package/src/features/tools/mrr-calculator/widget/index.ts +1 -0
- package/src/features/tools/mrr-calculator/widget/mrr-calculator.tsx +581 -0
- package/src/features/tools/registry.ts +6 -0
- package/src/features/tools/runtime-registry.ts +4 -0
- package/src/features/tools/shared/create-standard-landing-container.tsx +35 -5
- package/src/features/tools/shared/tool-page-shell.tsx +10 -137
- package/src/features/tools/shared/tool-standard-landing-sections.tsx +262 -52
- package/src/features/tools/spec-registry.ts +2 -0
- package/src/features/tools/tarot-reading/tarot-reading.container.tsx +1 -22
- package/src/features/tools/widget-registry.ts +2 -0
- package/src/i18n/messages/en/tools/app-icon-resizer.ts +90 -62
- package/src/i18n/messages/en/tools/index.ts +2 -0
- package/src/i18n/messages/en/tools/mrr-calculator.ts +202 -0
- package/src/i18n/messages/en/tools-ui.ts +3 -2
- package/src/i18n/messages/zh/page/tools.ts +2 -3
- package/src/i18n/messages/zh/tools/app-icon-resizer.ts +90 -62
- package/src/i18n/messages/zh/tools/index.ts +2 -0
- package/src/i18n/messages/zh/tools/mrr-calculator.ts +112 -0
- package/src/i18n/messages/zh/tools-ui.ts +3 -2
- package/src/i18n/messages/zh-tw/tools/app-icon-resizer.ts +90 -62
- package/src/i18n/messages/zh-tw/tools/index.ts +2 -0
- package/src/i18n/messages/zh-tw/tools/mrr-calculator.ts +52 -0
- package/src/i18n/messages/zh-tw/tools-ui.ts +3 -2
- package/src/lib/tools/resolve-tool-locale-copy.ts +7 -1
|
@@ -3,9 +3,11 @@ import type { ComponentType } from "react";
|
|
|
3
3
|
|
|
4
4
|
import type {
|
|
5
5
|
ToolStandardFaqItem,
|
|
6
|
+
ToolStandardEditorialSection,
|
|
6
7
|
ToolStandardGenerationStep,
|
|
7
8
|
ToolStandardImageTextSection,
|
|
8
9
|
ToolStandardNarrativeSection,
|
|
10
|
+
ToolStandardReferenceTable,
|
|
9
11
|
ToolStandardReportPreview,
|
|
10
12
|
ToolStandardSectionId,
|
|
11
13
|
} from "../../../features/tools/shared/tool-standard-landing-sections";
|
|
@@ -33,6 +35,10 @@ export type StandardLandingCopyKeys = {
|
|
|
33
35
|
reportPreview?: string;
|
|
34
36
|
/** Optional answer-first sections for decision and strategy tools. */
|
|
35
37
|
narrativeSections?: string;
|
|
38
|
+
/** Optional structured reading sections for rules, limits, and next actions. */
|
|
39
|
+
editorialSections?: string;
|
|
40
|
+
/** Optional searchable specifications table. */
|
|
41
|
+
referenceTable?: string;
|
|
36
42
|
};
|
|
37
43
|
|
|
38
44
|
export type StandardLandingContainerConfig = {
|
|
@@ -50,6 +56,14 @@ export type StandardLandingContainerConfig = {
|
|
|
50
56
|
passFaqToShell?: boolean;
|
|
51
57
|
/** A content CTA may navigate to a real, cost-free action. */
|
|
52
58
|
contentCtaHref?: string;
|
|
59
|
+
/** Hide repeated content CTAs when the primary action is already the workspace above. */
|
|
60
|
+
showContentCtas?: boolean;
|
|
61
|
+
/** Some tools have a useful step list but no extra illustration to repeat it. */
|
|
62
|
+
showGenerationImage?: boolean;
|
|
63
|
+
/** Only render a standalone guide when the tool has a real sequential workflow. */
|
|
64
|
+
showGeneration?: boolean;
|
|
65
|
+
/** Use less vertical padding and a grouped benefit list below mobile breakpoints. */
|
|
66
|
+
compactMobileSections?: boolean;
|
|
53
67
|
/** Override the default section sequence when the tool archetype needs a different reading flow. */
|
|
54
68
|
sectionOrder?: readonly ToolStandardSectionId[];
|
|
55
69
|
};
|
|
@@ -73,6 +87,10 @@ export function createStandardLandingContainer(
|
|
|
73
87
|
workspaceHeadingLevel = "h1",
|
|
74
88
|
passFaqToShell = true,
|
|
75
89
|
contentCtaHref,
|
|
90
|
+
showContentCtas = true,
|
|
91
|
+
showGenerationImage = true,
|
|
92
|
+
showGeneration = true,
|
|
93
|
+
compactMobileSections = false,
|
|
76
94
|
sectionOrder,
|
|
77
95
|
} = config;
|
|
78
96
|
|
|
@@ -96,9 +114,12 @@ export function createStandardLandingContainer(
|
|
|
96
114
|
const narrativeSections = copyKeys?.narrativeSections
|
|
97
115
|
? (t.raw(copyKeys.narrativeSections) as readonly ToolStandardNarrativeSection[])
|
|
98
116
|
: [];
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
const editorialSections = copyKeys?.editorialSections
|
|
118
|
+
? (t.raw(copyKeys.editorialSections) as readonly ToolStandardEditorialSection[])
|
|
119
|
+
: [];
|
|
120
|
+
const generationSteps = showGeneration
|
|
121
|
+
? (t.raw("standard.generation.steps") as readonly ToolStandardGenerationStep[])
|
|
122
|
+
: [];
|
|
102
123
|
const faqRaw = t.raw("faqItems");
|
|
103
124
|
const faqItems = (
|
|
104
125
|
Array.isArray(faqRaw) ? faqRaw : []
|
|
@@ -106,6 +127,9 @@ export function createStandardLandingContainer(
|
|
|
106
127
|
const reportPreview = copyKeys?.reportPreview
|
|
107
128
|
? (t.raw(copyKeys.reportPreview) as ToolStandardReportPreview)
|
|
108
129
|
: undefined;
|
|
130
|
+
const referenceTable = copyKeys?.referenceTable
|
|
131
|
+
? (t.raw(copyKeys.referenceTable) as ToolStandardReferenceTable)
|
|
132
|
+
: undefined;
|
|
109
133
|
const shellFaq = faqItems.filter(
|
|
110
134
|
(item): item is { question: string; answer: string } =>
|
|
111
135
|
typeof item.answer === "string",
|
|
@@ -137,7 +161,8 @@ export function createStandardLandingContainer(
|
|
|
137
161
|
}}
|
|
138
162
|
imageTextSections={imageTextSections}
|
|
139
163
|
narrativeSections={narrativeSections}
|
|
140
|
-
|
|
164
|
+
editorialSections={editorialSections}
|
|
165
|
+
generation={showGeneration ? {
|
|
141
166
|
title: t("standard.generation.title"),
|
|
142
167
|
image: {
|
|
143
168
|
src: generationImageSrc,
|
|
@@ -146,15 +171,20 @@ export function createStandardLandingContainer(
|
|
|
146
171
|
steps: generationSteps,
|
|
147
172
|
ctaLabel: t("standard.generation.ctaLabel"),
|
|
148
173
|
ctaHref: contentCtaHref,
|
|
149
|
-
|
|
174
|
+
showCta: showContentCtas,
|
|
175
|
+
showImage: showGenerationImage,
|
|
176
|
+
} : undefined}
|
|
150
177
|
reportPreview={reportPreview}
|
|
178
|
+
referenceTable={referenceTable}
|
|
151
179
|
faq={{
|
|
152
180
|
title: t(faqTitleKey),
|
|
153
181
|
intro: t("standard.faqIntro"),
|
|
154
182
|
ctaLabel: t("standard.faqCtaLabel"),
|
|
155
183
|
ctaHref: contentCtaHref,
|
|
184
|
+
showCta: showContentCtas,
|
|
156
185
|
items: faqItems,
|
|
157
186
|
}}
|
|
187
|
+
mobileDensity={compactMobileSections ? "compact" : "default"}
|
|
158
188
|
sectionOrder={sectionOrder}
|
|
159
189
|
/>
|
|
160
190
|
</ToolPageShell>
|
|
@@ -3,18 +3,8 @@ import Image from "next/image";
|
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
4
|
|
|
5
5
|
import { relatedToolSlugsByTool } from "../../../constants/related-tools";
|
|
6
|
-
import {
|
|
7
|
-
RelatedTools,
|
|
8
|
-
SiteFooter,
|
|
9
|
-
SiteNavigation,
|
|
10
|
-
getAuthClientMessages,
|
|
11
|
-
siteConfig,
|
|
12
|
-
type SiteNavigationCopy,
|
|
13
|
-
} from "../../../host/server";
|
|
14
|
-
import { bcp47ByLocale } from "../../../i18n/locale-fallback";
|
|
6
|
+
import { RelatedTools } from "../../../host/server";
|
|
15
7
|
import type { AppLocale } from "../../../i18n/locale";
|
|
16
|
-
import { localizedPath } from "../../../lib/routing/locale-paths";
|
|
17
|
-
import { serializeJsonLd } from "../../../lib/seo-meta";
|
|
18
8
|
import { resolveToolLocaleCopy } from "../../../lib/tools/resolve-tool-locale-copy";
|
|
19
9
|
|
|
20
10
|
import type { ToolSpec } from "../types";
|
|
@@ -62,136 +52,20 @@ export async function ToolPageShell({
|
|
|
62
52
|
handoffSlot?: ReactNode;
|
|
63
53
|
children: React.ReactNode;
|
|
64
54
|
}) {
|
|
55
|
+
// ToolMarketingShell 在 SEO route 层输出 JSON-LD / 导航 / 页脚;
|
|
56
|
+
// runtime 仅保留 hero / 内容 / related tools 的布局。
|
|
57
|
+
void faqItems;
|
|
58
|
+
void handoffSlot;
|
|
59
|
+
|
|
65
60
|
const copy = resolveToolLocaleCopy(spec, locale);
|
|
66
61
|
const relatedToolSlugs = relatedToolSlugsByTool[spec.slug];
|
|
67
62
|
const displayTitle = copy.displayTitle ?? copy.title;
|
|
68
63
|
// Full messages: public getRequestConfig omits toolsPages; client tools layout alone is not enough for RSC.
|
|
69
64
|
const messages = await loadToolMessages(locale);
|
|
70
65
|
const t = createToolMessageTranslator(locale, messages, "toolsPages.shared");
|
|
71
|
-
const navT = createToolMessageTranslator(locale, messages, "page.landing.hero");
|
|
72
|
-
const navigationCopy: SiteNavigationCopy = {
|
|
73
|
-
languageLabel: navT("languageLabel"),
|
|
74
|
-
productCapabilities: navT.raw("atomicCapabilities") as SiteNavigationCopy["productCapabilities"],
|
|
75
|
-
nav: {
|
|
76
|
-
tools: navT("nav.tools"),
|
|
77
|
-
viewAllTools: navT("nav.viewAllTools"),
|
|
78
|
-
pricing: navT("nav.pricing"),
|
|
79
|
-
blog: navT("nav.blog"),
|
|
80
|
-
skills: navT("nav.skills"),
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
const authMessages = await getAuthClientMessages(locale);
|
|
84
|
-
const toolUrl = new URL(
|
|
85
|
-
localizedPath(locale, `tools/${spec.slug}`),
|
|
86
|
-
siteConfig.url,
|
|
87
|
-
).href;
|
|
88
|
-
const toolJsonLd = {
|
|
89
|
-
"@context": "https://schema.org",
|
|
90
|
-
"@type": "WebApplication",
|
|
91
|
-
name: copy.title,
|
|
92
|
-
description: copy.description,
|
|
93
|
-
url: toolUrl,
|
|
94
|
-
applicationCategory: "BusinessApplication",
|
|
95
|
-
operatingSystem: "Web",
|
|
96
|
-
inLanguage: bcp47ByLocale[locale],
|
|
97
|
-
isAccessibleForFree: true,
|
|
98
|
-
offers: {
|
|
99
|
-
"@type": "Offer",
|
|
100
|
-
price: "0",
|
|
101
|
-
priceCurrency: "USD",
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const breadcrumbJsonLd = {
|
|
106
|
-
"@context": "https://schema.org",
|
|
107
|
-
"@type": "BreadcrumbList",
|
|
108
|
-
itemListElement: [
|
|
109
|
-
{
|
|
110
|
-
"@type": "ListItem",
|
|
111
|
-
position: 1,
|
|
112
|
-
name: siteConfig.name,
|
|
113
|
-
item: new URL(localizedPath(locale), siteConfig.url).href,
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
"@type": "ListItem",
|
|
117
|
-
position: 2,
|
|
118
|
-
name: t("breadcrumbTools"),
|
|
119
|
-
item: new URL(localizedPath(locale, "tools"), siteConfig.url).href,
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
"@type": "ListItem",
|
|
123
|
-
position: 3,
|
|
124
|
-
name: copy.title,
|
|
125
|
-
item: toolUrl,
|
|
126
|
-
},
|
|
127
|
-
],
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
// FAQ 来源优先级:显式 faqItems(package 工具)→ toolsPages.{camelSlug}.guide.faqItems;都缺失时不输出 FAQPage
|
|
131
|
-
let resolvedFaqItems: Array<{ question: string; answer: string }> =
|
|
132
|
-
faqItems ?? [];
|
|
133
|
-
if (resolvedFaqItems.length === 0) {
|
|
134
|
-
const camelSlug = spec.slug.replace(/-([a-z0-9])/g, (_, char: string) =>
|
|
135
|
-
char.toUpperCase(),
|
|
136
|
-
);
|
|
137
|
-
try {
|
|
138
|
-
const guideT = createToolMessageTranslator(
|
|
139
|
-
locale,
|
|
140
|
-
messages,
|
|
141
|
-
`toolsPages.${camelSlug}.guide`,
|
|
142
|
-
);
|
|
143
|
-
const rawFaqItems = guideT.raw("faqItems") as unknown;
|
|
144
|
-
resolvedFaqItems = Array.isArray(rawFaqItems)
|
|
145
|
-
? rawFaqItems.filter(
|
|
146
|
-
(item): item is { question: string; answer: string } =>
|
|
147
|
-
typeof item?.question === "string" &&
|
|
148
|
-
typeof item?.answer === "string",
|
|
149
|
-
)
|
|
150
|
-
: [];
|
|
151
|
-
} catch {
|
|
152
|
-
resolvedFaqItems = [];
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
const faqJsonLd: Record<string, unknown> | null =
|
|
156
|
-
resolvedFaqItems.length > 0
|
|
157
|
-
? {
|
|
158
|
-
"@context": "https://schema.org",
|
|
159
|
-
"@type": "FAQPage",
|
|
160
|
-
mainEntity: resolvedFaqItems.map((item) => ({
|
|
161
|
-
"@type": "Question",
|
|
162
|
-
name: item.question,
|
|
163
|
-
acceptedAnswer: {
|
|
164
|
-
"@type": "Answer",
|
|
165
|
-
text: item.answer,
|
|
166
|
-
},
|
|
167
|
-
})),
|
|
168
|
-
}
|
|
169
|
-
: null;
|
|
170
66
|
|
|
171
67
|
return (
|
|
172
68
|
<div className="min-h-screen overflow-x-clip bg-white text-[#111111]">
|
|
173
|
-
<script
|
|
174
|
-
type="application/ld+json"
|
|
175
|
-
dangerouslySetInnerHTML={{ __html: serializeJsonLd(toolJsonLd) }}
|
|
176
|
-
/>
|
|
177
|
-
<script
|
|
178
|
-
type="application/ld+json"
|
|
179
|
-
dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
|
|
180
|
-
/>
|
|
181
|
-
{faqJsonLd ? (
|
|
182
|
-
<script
|
|
183
|
-
type="application/ld+json"
|
|
184
|
-
dangerouslySetInnerHTML={{ __html: serializeJsonLd(faqJsonLd) }}
|
|
185
|
-
/>
|
|
186
|
-
) : null}
|
|
187
|
-
<SiteNavigation
|
|
188
|
-
locale={locale}
|
|
189
|
-
copy={navigationCopy}
|
|
190
|
-
authMessages={authMessages}
|
|
191
|
-
surface="solid"
|
|
192
|
-
className="sticky top-0 z-50 border-t-0"
|
|
193
|
-
/>
|
|
194
|
-
{handoffSlot}
|
|
195
69
|
<main className="overflow-x-clip">
|
|
196
70
|
{heroVariant === "immersive" ? (
|
|
197
71
|
<>
|
|
@@ -230,7 +104,7 @@ export async function ToolPageShell({
|
|
|
230
104
|
) : null}
|
|
231
105
|
|
|
232
106
|
<div className="relative z-10 flex min-h-[min(72svh,680px)] flex-col">
|
|
233
|
-
<div className="mx-auto flex w-full max-w-
|
|
107
|
+
<div className="mx-auto flex w-full max-w-[76rem] flex-1 flex-col justify-center gap-10 px-4 pb-8 pt-8 sm:px-8 lg:pt-12">
|
|
234
108
|
<div className="grid flex-1 gap-10 lg:grid-cols-[0.9fr_1.1fr] lg:items-center">
|
|
235
109
|
<div className="max-w-4xl">
|
|
236
110
|
<p className="inline-flex items-center gap-2 rounded-full border border-[#2B211A]/14 bg-[#FFF8EA]/45 px-4 py-2 text-sm font-semibold text-[#4C3C31] shadow-[0_12px_36px_rgba(83,48,28,0.08)] backdrop-blur">
|
|
@@ -262,13 +136,13 @@ export async function ToolPageShell({
|
|
|
262
136
|
</div>
|
|
263
137
|
</section>
|
|
264
138
|
|
|
265
|
-
<div className="mx-auto flex max-w-
|
|
139
|
+
<div className="mx-auto flex max-w-[76rem] flex-col gap-16 px-4 py-16 sm:px-8 md:gap-20 md:py-24">
|
|
266
140
|
{children}
|
|
267
141
|
</div>
|
|
268
142
|
</>
|
|
269
143
|
) : (
|
|
270
144
|
<>
|
|
271
|
-
<div className="mx-auto max-w-
|
|
145
|
+
<div className="mx-auto max-w-[76rem] px-4 py-5 sm:px-8">
|
|
272
146
|
{heroVariant === "showcase" && heroVisual ? (
|
|
273
147
|
<section className="pb-14 pt-14 md:pb-16 md:pt-16">
|
|
274
148
|
<div className="mx-auto max-w-4xl text-center">
|
|
@@ -337,12 +211,11 @@ export async function ToolPageShell({
|
|
|
337
211
|
</>
|
|
338
212
|
)}
|
|
339
213
|
{relatedToolSlugs?.length ? (
|
|
340
|
-
<div className="mx-auto w-full max-w-
|
|
214
|
+
<div className="mx-auto w-full max-w-[76rem] px-4 pb-16 sm:px-8 md:pb-20">
|
|
341
215
|
<RelatedTools locale={locale} currentToolSlug={spec.slug} slugs={relatedToolSlugs} />
|
|
342
216
|
</div>
|
|
343
217
|
) : null}
|
|
344
218
|
</main>
|
|
345
|
-
<SiteFooter locale={locale} />
|
|
346
219
|
</div>
|
|
347
220
|
);
|
|
348
221
|
}
|