@tomako/tools-runtime 0.1.1 → 0.1.3
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 -4
- 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 +4 -4
- 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/widget-registry.ts +2 -0
- package/src/i18n/messages/en/tools/app-icon-resizer.ts +90 -62
- package/src/i18n/messages/en/tools/build-in-public-story-generator.ts +1 -1
- 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/build-in-public-story-generator.ts +1 -1
- 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/build-in-public-story-generator.ts +1 -1
- 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
|
@@ -16,12 +16,14 @@ export type ToolStandardBenefitCard = {
|
|
|
16
16
|
export type ToolStandardImageTextSection = {
|
|
17
17
|
id?: string;
|
|
18
18
|
title: string;
|
|
19
|
-
body: readonly
|
|
19
|
+
body: readonly ToolStandardBodyParagraph[];
|
|
20
20
|
image: {
|
|
21
21
|
src: string;
|
|
22
22
|
alt: string;
|
|
23
23
|
};
|
|
24
24
|
imageFirst?: boolean;
|
|
25
|
+
/** Keep a useful desktop illustration without forcing it into a narrow reading flow. */
|
|
26
|
+
hideImageOnMobile?: boolean;
|
|
25
27
|
background?: "white" | "muted";
|
|
26
28
|
};
|
|
27
29
|
|
|
@@ -32,10 +34,28 @@ export type ToolStandardImageTextSection = {
|
|
|
32
34
|
export type ToolStandardNarrativeSection = {
|
|
33
35
|
id?: string;
|
|
34
36
|
title: string;
|
|
35
|
-
body: readonly
|
|
37
|
+
body: readonly ToolStandardBodyParagraph[];
|
|
36
38
|
background?: "white" | "muted";
|
|
37
39
|
};
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* A shared, visual reading pattern for rules, limits, and next actions that
|
|
43
|
+
* need hierarchy but do not need a decorative illustration.
|
|
44
|
+
*/
|
|
45
|
+
export type ToolStandardEditorialSection = {
|
|
46
|
+
id?: string;
|
|
47
|
+
title: string;
|
|
48
|
+
body: readonly ToolStandardBodyParagraph[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Highlight only the short phrase that carries a rule, formula, or decision. */
|
|
52
|
+
export type ToolStandardBodyParagraph =
|
|
53
|
+
| string
|
|
54
|
+
| {
|
|
55
|
+
text: string;
|
|
56
|
+
highlights?: readonly string[];
|
|
57
|
+
};
|
|
58
|
+
|
|
39
59
|
export type ToolStandardGenerationStep = {
|
|
40
60
|
title: string;
|
|
41
61
|
body: string;
|
|
@@ -54,12 +74,26 @@ export type ToolStandardReportPreview = {
|
|
|
54
74
|
}[];
|
|
55
75
|
};
|
|
56
76
|
|
|
77
|
+
export type ToolStandardReferenceTable = {
|
|
78
|
+
title: string;
|
|
79
|
+
intro: string;
|
|
80
|
+
mobileHint?: string;
|
|
81
|
+
columns: readonly [string, string, string, string];
|
|
82
|
+
rows: readonly {
|
|
83
|
+
label: string;
|
|
84
|
+
cells: readonly [string, string, string];
|
|
85
|
+
}[];
|
|
86
|
+
note?: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
57
89
|
export type ToolStandardSectionId =
|
|
58
90
|
| "benefits"
|
|
59
91
|
| "reportPreview"
|
|
60
92
|
| "imageTextSections"
|
|
61
93
|
| "narrative"
|
|
94
|
+
| "editorial"
|
|
62
95
|
| "generation"
|
|
96
|
+
| "referenceTable"
|
|
63
97
|
| "faq";
|
|
64
98
|
|
|
65
99
|
export type ToolStandardFaqItem = {
|
|
@@ -74,7 +108,8 @@ export type ToolLandingStandardSectionsProps = {
|
|
|
74
108
|
};
|
|
75
109
|
imageTextSections: readonly ToolStandardImageTextSection[];
|
|
76
110
|
narrativeSections?: readonly ToolStandardNarrativeSection[];
|
|
77
|
-
|
|
111
|
+
editorialSections?: readonly ToolStandardEditorialSection[];
|
|
112
|
+
generation?: {
|
|
78
113
|
title: string;
|
|
79
114
|
image: {
|
|
80
115
|
src: string;
|
|
@@ -83,17 +118,23 @@ export type ToolLandingStandardSectionsProps = {
|
|
|
83
118
|
steps: readonly ToolStandardGenerationStep[];
|
|
84
119
|
ctaLabel: string;
|
|
85
120
|
ctaHref?: string;
|
|
121
|
+
showImage?: boolean;
|
|
122
|
+
showCta?: boolean;
|
|
86
123
|
};
|
|
87
124
|
reportPreview?: ToolStandardReportPreview;
|
|
125
|
+
referenceTable?: ToolStandardReferenceTable;
|
|
88
126
|
faq: {
|
|
89
127
|
title: string;
|
|
90
128
|
intro: string;
|
|
91
129
|
ctaLabel: string;
|
|
92
130
|
ctaHref?: string;
|
|
131
|
+
showCta?: boolean;
|
|
93
132
|
items: readonly ToolStandardFaqItem[];
|
|
94
133
|
};
|
|
95
134
|
/** Keep the shared primitives, while allowing a report-style tool to lead with its output. */
|
|
96
135
|
sectionOrder?: readonly ToolStandardSectionId[];
|
|
136
|
+
/** Use a denser vertical rhythm for tools whose first task is already above the fold. */
|
|
137
|
+
mobileDensity?: "default" | "compact";
|
|
97
138
|
className?: string;
|
|
98
139
|
};
|
|
99
140
|
|
|
@@ -101,19 +142,24 @@ export function ToolLandingStandardSections({
|
|
|
101
142
|
benefits,
|
|
102
143
|
imageTextSections,
|
|
103
144
|
narrativeSections = [],
|
|
145
|
+
editorialSections = [],
|
|
104
146
|
generation,
|
|
105
147
|
reportPreview,
|
|
148
|
+
mobileDensity = "default",
|
|
149
|
+
referenceTable,
|
|
106
150
|
faq,
|
|
107
151
|
sectionOrder = [
|
|
108
152
|
"benefits",
|
|
109
153
|
"imageTextSections",
|
|
154
|
+
"editorial",
|
|
110
155
|
"generation",
|
|
111
156
|
"faq",
|
|
112
157
|
],
|
|
113
158
|
className,
|
|
114
159
|
}: ToolLandingStandardSectionsProps) {
|
|
160
|
+
const compactMobile = mobileDensity === "compact";
|
|
115
161
|
const sections: Record<ToolStandardSectionId, ReactNode> = {
|
|
116
|
-
benefits: <ToolStandardBenefitsSection {...benefits} />,
|
|
162
|
+
benefits: <ToolStandardBenefitsSection {...benefits} compactMobile={compactMobile} />,
|
|
117
163
|
reportPreview: reportPreview ? (
|
|
118
164
|
<ToolStandardReportPreviewSection {...reportPreview} />
|
|
119
165
|
) : null,
|
|
@@ -121,14 +167,21 @@ export function ToolLandingStandardSections({
|
|
|
121
167
|
<ToolStandardImageTextBlock
|
|
122
168
|
key={section.id ?? section.title}
|
|
123
169
|
{...section}
|
|
170
|
+
compactMobile={compactMobile}
|
|
124
171
|
imagePriority={index === 0}
|
|
125
172
|
/>
|
|
126
173
|
)),
|
|
127
174
|
narrative: narrativeSections.map((section) => (
|
|
128
|
-
<ToolStandardNarrativeBlock key={section.id ?? section.title} {...section} />
|
|
175
|
+
<ToolStandardNarrativeBlock key={section.id ?? section.title} {...section} compactMobile={compactMobile} />
|
|
129
176
|
)),
|
|
130
|
-
|
|
131
|
-
|
|
177
|
+
editorial: editorialSections.length > 0 ? (
|
|
178
|
+
<ToolStandardEditorialSection items={editorialSections} compactMobile={compactMobile} />
|
|
179
|
+
) : null,
|
|
180
|
+
generation: generation ? <ToolStandardGenerationSection {...generation} compactMobile={compactMobile} /> : null,
|
|
181
|
+
referenceTable: referenceTable ? (
|
|
182
|
+
<ToolStandardReferenceTableSection {...referenceTable} />
|
|
183
|
+
) : null,
|
|
184
|
+
faq: <ToolStandardFaqSection {...faq} compactMobile={compactMobile} />,
|
|
132
185
|
};
|
|
133
186
|
|
|
134
187
|
return (
|
|
@@ -140,26 +193,134 @@ export function ToolLandingStandardSections({
|
|
|
140
193
|
);
|
|
141
194
|
}
|
|
142
195
|
|
|
196
|
+
function ToolStandardEditorialSection({
|
|
197
|
+
items,
|
|
198
|
+
compactMobile = false,
|
|
199
|
+
}: {
|
|
200
|
+
items: readonly ToolStandardEditorialSection[];
|
|
201
|
+
compactMobile?: boolean;
|
|
202
|
+
}) {
|
|
203
|
+
return (
|
|
204
|
+
<div
|
|
205
|
+
className={`relative left-1/2 w-screen -translate-x-1/2 border-t border-[#E8EBF0] bg-white ${compactMobile ? "py-10 md:py-14" : "py-12 md:py-16"}`}
|
|
206
|
+
data-tool-standard-section="editorial"
|
|
207
|
+
>
|
|
208
|
+
<div className="mx-auto w-full max-w-[76rem] overflow-hidden rounded-[28px] bg-[#F6F8FA] px-4 sm:px-8">
|
|
209
|
+
{items.map((item, index) => (
|
|
210
|
+
<section
|
|
211
|
+
key={item.id ?? item.title}
|
|
212
|
+
className={`grid gap-7 px-5 py-8 sm:px-8 md:px-10 md:py-10 xl:grid-cols-[minmax(0,0.4fr)_minmax(0,0.6fr)] xl:gap-12 ${index > 0 ? "border-t border-[#DFE5EB]" : ""} ${compactMobile ? "lg:py-11" : ""}`}
|
|
213
|
+
data-tool-standard-section={item.id ?? "editorial-item"}
|
|
214
|
+
>
|
|
215
|
+
<div className="flex min-w-0 xl:gap-5">
|
|
216
|
+
<span
|
|
217
|
+
aria-hidden
|
|
218
|
+
className="hidden pt-1 text-xs font-semibold tabular-nums tracking-[0.12em] text-[#687785] xl:block"
|
|
219
|
+
>
|
|
220
|
+
{String(index + 1).padStart(2, "0")}
|
|
221
|
+
</span>
|
|
222
|
+
<h2 className="max-w-[450px] text-[25px] font-semibold leading-[33px] text-[#010205] sm:text-balance sm:text-[28px] sm:leading-[35px]">
|
|
223
|
+
{item.title}
|
|
224
|
+
</h2>
|
|
225
|
+
</div>
|
|
226
|
+
<div className="max-w-[680px] space-y-4 text-[16px] leading-[27px] text-[#26323D] sm:text-[16px] sm:leading-[28px] md:border-l md:border-[#DFE5EB] md:pl-10 xl:pl-12">
|
|
227
|
+
{item.body.map((paragraph, index) => (
|
|
228
|
+
<p key={index}>{renderToolStandardParagraph(paragraph)}</p>
|
|
229
|
+
))}
|
|
230
|
+
</div>
|
|
231
|
+
</section>
|
|
232
|
+
))}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function ToolStandardReferenceTableSection({
|
|
239
|
+
title,
|
|
240
|
+
intro,
|
|
241
|
+
mobileHint,
|
|
242
|
+
columns,
|
|
243
|
+
rows,
|
|
244
|
+
note,
|
|
245
|
+
}: ToolStandardReferenceTable) {
|
|
246
|
+
return (
|
|
247
|
+
<section
|
|
248
|
+
className="relative left-1/2 w-screen -translate-x-1/2 border-t border-[#E8EBF0] bg-white px-5 py-24 md:px-10 md:py-28 lg:px-16 lg:py-32"
|
|
249
|
+
data-tool-standard-section="reference-table"
|
|
250
|
+
>
|
|
251
|
+
<div className="mx-auto max-w-[1284px]">
|
|
252
|
+
<h2 className="max-w-[760px] text-balance text-[2rem] font-semibold leading-[1.18] text-[#010205] md:text-[40px]">
|
|
253
|
+
{title}
|
|
254
|
+
</h2>
|
|
255
|
+
<p className="mt-5 max-w-[760px] text-pretty text-base leading-[26px] text-[#0D1216]">
|
|
256
|
+
{intro}
|
|
257
|
+
</p>
|
|
258
|
+
{mobileHint ? (
|
|
259
|
+
<p className="mt-6 text-sm font-medium text-[#56616B] md:hidden">{mobileHint}</p>
|
|
260
|
+
) : null}
|
|
261
|
+
<div className="mt-4 overflow-x-auto rounded-2xl border border-[#DDE3EA] bg-white md:mt-8">
|
|
262
|
+
<table className="w-full min-w-[760px] border-collapse text-left">
|
|
263
|
+
<thead className="bg-[#F6F7F8]">
|
|
264
|
+
<tr>
|
|
265
|
+
{columns.map((column, index) => (
|
|
266
|
+
<th
|
|
267
|
+
key={column}
|
|
268
|
+
className={`border-b border-[#DDE3EA] px-5 py-4 text-sm font-semibold text-[#010205] ${
|
|
269
|
+
index === 0 ? "sticky left-0 z-10 bg-[#F6F7F8] md:static" : ""
|
|
270
|
+
}`}
|
|
271
|
+
scope="col"
|
|
272
|
+
>
|
|
273
|
+
{column}
|
|
274
|
+
</th>
|
|
275
|
+
))}
|
|
276
|
+
</tr>
|
|
277
|
+
</thead>
|
|
278
|
+
<tbody>
|
|
279
|
+
{rows.map((row) => (
|
|
280
|
+
<tr key={row.label} className="border-b border-[#E8EBF0] last:border-b-0">
|
|
281
|
+
<th
|
|
282
|
+
className="sticky left-0 z-10 bg-white px-5 py-4 text-sm font-semibold text-[#010205] md:static"
|
|
283
|
+
scope="row"
|
|
284
|
+
>
|
|
285
|
+
{row.label}
|
|
286
|
+
</th>
|
|
287
|
+
{row.cells.map((cell) => (
|
|
288
|
+
<td key={cell} className="px-5 py-4 text-sm leading-6 text-[#4D5964]">
|
|
289
|
+
{cell}
|
|
290
|
+
</td>
|
|
291
|
+
))}
|
|
292
|
+
</tr>
|
|
293
|
+
))}
|
|
294
|
+
</tbody>
|
|
295
|
+
</table>
|
|
296
|
+
</div>
|
|
297
|
+
{note ? <p className="mt-4 text-sm leading-6 text-[#56616B]">{note}</p> : null}
|
|
298
|
+
</div>
|
|
299
|
+
</section>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
143
303
|
function ToolStandardNarrativeBlock({
|
|
144
304
|
id,
|
|
145
305
|
title,
|
|
146
306
|
body,
|
|
147
307
|
background = "white",
|
|
148
|
-
|
|
308
|
+
compactMobile = false,
|
|
309
|
+
}: ToolStandardNarrativeSection & { compactMobile?: boolean }) {
|
|
149
310
|
return (
|
|
150
311
|
<section
|
|
151
|
-
className={`relative left-1/2 w-screen -translate-x-1/2 border-t border-[#E8EBF0]
|
|
312
|
+
className={`relative left-1/2 w-screen -translate-x-1/2 border-t border-[#E8EBF0] ${compactMobile ? "py-16 md:py-24 lg:py-28" : "py-20 md:py-24 lg:py-28"} ${
|
|
152
313
|
background === "muted" ? "bg-[#F6F7F8]" : "bg-white"
|
|
153
314
|
}`}
|
|
154
315
|
data-tool-standard-section={id ?? "narrative"}
|
|
155
316
|
>
|
|
156
|
-
<div className="mx-auto max-w-[880px]">
|
|
317
|
+
<div className="mx-auto w-full max-w-[880px] px-4 sm:px-8">
|
|
157
318
|
<h2 className="text-balance text-[2rem] font-semibold leading-[1.18] text-[#010205] md:text-[40px]">
|
|
158
319
|
{title}
|
|
159
320
|
</h2>
|
|
160
321
|
<div className="mt-5 space-y-4 text-pretty text-base leading-[28px] text-[#0D1216] md:text-lg">
|
|
161
322
|
{body.map((paragraph, index) => (
|
|
162
|
-
<p key={index}>{paragraph}</p>
|
|
323
|
+
<p key={index}>{renderToolStandardParagraph(paragraph)}</p>
|
|
163
324
|
))}
|
|
164
325
|
</div>
|
|
165
326
|
</div>
|
|
@@ -170,37 +331,37 @@ function ToolStandardNarrativeBlock({
|
|
|
170
331
|
function ToolStandardBenefitsSection({
|
|
171
332
|
title,
|
|
172
333
|
cards,
|
|
334
|
+
compactMobile = false,
|
|
173
335
|
}: {
|
|
174
336
|
title: string;
|
|
175
337
|
cards: readonly ToolStandardBenefitCard[];
|
|
338
|
+
compactMobile?: boolean;
|
|
176
339
|
}) {
|
|
177
340
|
return (
|
|
178
341
|
<section
|
|
179
|
-
className=
|
|
342
|
+
className={`relative left-1/2 w-screen -translate-x-1/2 border-t border-[#E8EBF0] bg-white ${compactMobile ? "py-12 sm:py-16 md:py-20 lg:py-24" : "py-16 sm:py-20 md:py-24 lg:py-28"}`}
|
|
180
343
|
data-tool-standard-section="benefits"
|
|
181
344
|
>
|
|
182
|
-
<div className="mx-auto max-w-[
|
|
183
|
-
<h2 className="text-
|
|
345
|
+
<div className="mx-auto w-full max-w-[76rem] px-4 sm:px-8">
|
|
346
|
+
<h2 className="text-[25px] font-semibold leading-[33px] text-[#010205] sm:text-balance sm:text-center sm:text-[28px] sm:leading-[35px]">
|
|
184
347
|
{title}
|
|
185
348
|
</h2>
|
|
186
|
-
<div className="mt-
|
|
349
|
+
<div className="mt-6 grid gap-3 sm:mt-8 sm:gap-5 lg:grid-cols-3">
|
|
187
350
|
{cards.map((item) => {
|
|
188
351
|
const Icon = item.icon;
|
|
189
352
|
|
|
190
353
|
return (
|
|
191
354
|
<article
|
|
192
355
|
key={item.title}
|
|
193
|
-
className="min-w-0 rounded-2xl bg-[#
|
|
356
|
+
className="grid min-w-0 grid-cols-[40px_minmax(0,1fr)] gap-x-3 rounded-2xl border border-[#E4E9F1] bg-[#F8F9FA] p-5 sm:gap-x-4 sm:p-6"
|
|
194
357
|
>
|
|
195
|
-
<
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
</div>
|
|
203
|
-
<p className="mt-4 text-pretty text-base leading-[26px] text-[#11171D]/60">
|
|
358
|
+
<span className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-[#E0D9FC] text-[#0D1216]">
|
|
359
|
+
<Icon aria-hidden className="size-[18px]" />
|
|
360
|
+
</span>
|
|
361
|
+
<h3 className="self-center text-[18px] font-semibold leading-[28px] text-[#010205]">
|
|
362
|
+
{item.title}
|
|
363
|
+
</h3>
|
|
364
|
+
<p className="col-start-2 mt-2 text-[16px] leading-[27px] text-[#4D5964]">
|
|
204
365
|
{item.body}
|
|
205
366
|
</p>
|
|
206
367
|
</article>
|
|
@@ -218,75 +379,120 @@ function ToolStandardImageTextBlock({
|
|
|
218
379
|
body,
|
|
219
380
|
image,
|
|
220
381
|
imageFirst = false,
|
|
382
|
+
hideImageOnMobile = false,
|
|
221
383
|
imagePriority = false,
|
|
222
|
-
|
|
384
|
+
compactMobile = false,
|
|
385
|
+
}: ToolStandardImageTextSection & { imagePriority?: boolean; compactMobile?: boolean }) {
|
|
386
|
+
const imageClassName = `${imageFirst ? "order-2 xl:order-1" : "order-2"} ${hideImageOnMobile ? "hidden md:block" : ""}`;
|
|
387
|
+
const contentClassName = `order-1 min-w-0 ${imageFirst ? "xl:order-2 xl:pl-4" : "xl:pr-4"}`;
|
|
388
|
+
|
|
223
389
|
return (
|
|
224
390
|
<section
|
|
225
|
-
className=
|
|
391
|
+
className={`relative left-1/2 w-screen -translate-x-1/2 border-t border-[#E8EBF0] bg-white ${compactMobile ? "py-16 md:py-24 lg:py-32" : "py-24 md:py-28 lg:py-32"}`}
|
|
226
392
|
data-tool-standard-section={id ?? "image-text"}
|
|
227
393
|
>
|
|
228
394
|
<div
|
|
229
|
-
className={`mx-auto grid max-w-[
|
|
395
|
+
className={`mx-auto grid w-full max-w-[76rem] gap-8 px-4 sm:px-8 xl:items-center xl:gap-12 ${
|
|
230
396
|
imageFirst
|
|
231
397
|
? "xl:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]"
|
|
232
398
|
: "xl:grid-cols-[minmax(0,1.05fr)_minmax(0,0.95fr)]"
|
|
233
399
|
}`}
|
|
234
400
|
>
|
|
235
401
|
{imageFirst ? (
|
|
236
|
-
<ToolStandardImageFrame {...image} priority={imagePriority} />
|
|
402
|
+
<ToolStandardImageFrame {...image} className={imageClassName} priority={imagePriority} />
|
|
237
403
|
) : null}
|
|
238
|
-
<div className={
|
|
239
|
-
<h2 className="max-w-[620px]
|
|
240
|
-
{title}
|
|
404
|
+
<div className={contentClassName}>
|
|
405
|
+
<h2 className="max-w-[620px] text-[25px] font-semibold leading-[33px] text-[#010205] sm:text-balance sm:text-[28px] sm:leading-[35px]">
|
|
406
|
+
{renderToolStandardTitle(title)}
|
|
241
407
|
</h2>
|
|
242
|
-
<div className="mt-5 max-w-2xl space-y-4 text-
|
|
408
|
+
<div className="mt-5 max-w-2xl space-y-4 text-[16px] leading-[27px] text-[#0D1216] sm:leading-[28px]">
|
|
243
409
|
{body.map((paragraph, index) => (
|
|
244
|
-
<p key={index}>{paragraph}</p>
|
|
410
|
+
<p key={index}>{renderToolStandardParagraph(paragraph)}</p>
|
|
245
411
|
))}
|
|
246
412
|
</div>
|
|
247
413
|
</div>
|
|
248
414
|
{imageFirst ? null : (
|
|
249
|
-
<ToolStandardImageFrame {...image} priority={imagePriority} />
|
|
415
|
+
<ToolStandardImageFrame {...image} className={imageClassName} priority={imagePriority} />
|
|
250
416
|
)}
|
|
251
417
|
</div>
|
|
252
418
|
</section>
|
|
253
419
|
);
|
|
254
420
|
}
|
|
255
421
|
|
|
422
|
+
function renderToolStandardTitle(title: string) {
|
|
423
|
+
const lines = title.split("\n");
|
|
424
|
+
|
|
425
|
+
return lines.map((line, index) => (
|
|
426
|
+
<span key={`${line}-${index}`} className={index > 0 ? "xl:block" : undefined}>
|
|
427
|
+
{index > 0 ? <span className="xl:hidden"> </span> : null}
|
|
428
|
+
{line}
|
|
429
|
+
</span>
|
|
430
|
+
));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function renderToolStandardParagraph(paragraph: ToolStandardBodyParagraph) {
|
|
434
|
+
if (typeof paragraph === "string" || !paragraph.highlights?.length) {
|
|
435
|
+
return typeof paragraph === "string" ? paragraph : paragraph.text;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const highlights = [...new Set(paragraph.highlights.filter(Boolean))].sort((a, b) => b.length - a.length);
|
|
439
|
+
const expression = new RegExp(`(${highlights.map(escapeRegExp).join("|")})`, "g");
|
|
440
|
+
|
|
441
|
+
return paragraph.text.split(expression).filter(Boolean).map((part, index) =>
|
|
442
|
+
highlights.includes(part) ? (
|
|
443
|
+
<mark
|
|
444
|
+
key={index}
|
|
445
|
+
className="rounded-[3px] bg-[#FFF0A8] px-1 font-semibold text-[#171714] [box-decoration-break:clone] [-webkit-box-decoration-break:clone]"
|
|
446
|
+
>
|
|
447
|
+
{part}
|
|
448
|
+
</mark>
|
|
449
|
+
) : (
|
|
450
|
+
part
|
|
451
|
+
),
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function escapeRegExp(value: string) {
|
|
456
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
457
|
+
}
|
|
458
|
+
|
|
256
459
|
function ToolStandardGenerationSection({
|
|
257
460
|
title,
|
|
258
461
|
image,
|
|
259
462
|
steps,
|
|
260
463
|
ctaLabel,
|
|
261
464
|
ctaHref,
|
|
262
|
-
|
|
465
|
+
showImage = true,
|
|
466
|
+
showCta = true,
|
|
467
|
+
compactMobile = false,
|
|
468
|
+
}: ToolLandingStandardSectionsProps["generation"] & { compactMobile?: boolean }) {
|
|
263
469
|
return (
|
|
264
470
|
<section
|
|
265
|
-
className=
|
|
471
|
+
className={`relative left-1/2 w-screen -translate-x-1/2 border-t border-[#E8EBF0] bg-white ${compactMobile ? "py-16 md:py-24 lg:py-32" : "py-24 md:py-28 lg:py-32"}`}
|
|
266
472
|
data-tool-standard-section="generation-steps"
|
|
267
473
|
>
|
|
268
|
-
<div className=
|
|
269
|
-
<ToolStandardImageFrame {...image} />
|
|
270
|
-
<div className="min-w-0 xl:pl-4">
|
|
474
|
+
<div className={`mx-auto w-full px-4 sm:px-8 ${showImage ? "grid max-w-[76rem] gap-10 xl:grid-cols-2 xl:items-center xl:gap-14" : "max-w-[760px]"}`}>
|
|
475
|
+
{showImage ? <ToolStandardImageFrame {...image} /> : null}
|
|
476
|
+
<div className={showImage ? "min-w-0 xl:pl-4" : "min-w-0"}>
|
|
271
477
|
<h2 className="max-w-[620px] whitespace-pre-line text-balance text-[2rem] font-semibold leading-[1.18] text-[#010205] md:text-[40px]">
|
|
272
478
|
{title}
|
|
273
479
|
</h2>
|
|
274
480
|
<ToolStandardGenerationSteps steps={steps} />
|
|
275
|
-
{ctaHref ? (
|
|
481
|
+
{showCta && ctaHref ? (
|
|
276
482
|
<Button
|
|
277
483
|
asChild
|
|
278
484
|
className="mt-8 h-12 min-w-40 rounded-lg bg-black px-4 text-base font-semibold text-white shadow-none hover:bg-[#1F1F1F]"
|
|
279
485
|
>
|
|
280
486
|
<a href={ctaHref}>{ctaLabel}</a>
|
|
281
487
|
</Button>
|
|
282
|
-
) : (
|
|
488
|
+
) : showCta ? (
|
|
283
489
|
<Button
|
|
284
490
|
type="button"
|
|
285
491
|
className="mt-8 h-12 min-w-40 rounded-lg bg-black px-4 text-base font-semibold text-white shadow-none hover:bg-[#1F1F1F]"
|
|
286
492
|
>
|
|
287
493
|
{ctaLabel}
|
|
288
494
|
</Button>
|
|
289
|
-
)}
|
|
495
|
+
) : null}
|
|
290
496
|
</div>
|
|
291
497
|
</div>
|
|
292
498
|
</section>
|
|
@@ -304,10 +510,10 @@ function ToolStandardReportPreviewSection({
|
|
|
304
510
|
}: ToolStandardReportPreview) {
|
|
305
511
|
return (
|
|
306
512
|
<section
|
|
307
|
-
className="relative left-1/2 w-screen -translate-x-1/2 border-t border-[#E8EBF0] bg-[#F6F7F8]
|
|
513
|
+
className="relative left-1/2 w-screen -translate-x-1/2 border-t border-[#E8EBF0] bg-[#F6F7F8] py-24 md:py-28 lg:py-32"
|
|
308
514
|
data-tool-standard-section="report-preview"
|
|
309
515
|
>
|
|
310
|
-
<div className="mx-auto max-w-[
|
|
516
|
+
<div className="mx-auto w-full max-w-[76rem] px-4 sm:px-8">
|
|
311
517
|
<p className="text-sm font-semibold uppercase tracking-[0.08em] text-[#4A5968]">
|
|
312
518
|
{eyebrow}
|
|
313
519
|
</p>
|
|
@@ -361,13 +567,15 @@ function ToolStandardFaqSection({
|
|
|
361
567
|
ctaLabel,
|
|
362
568
|
ctaHref,
|
|
363
569
|
items,
|
|
364
|
-
|
|
570
|
+
showCta = true,
|
|
571
|
+
compactMobile = false,
|
|
572
|
+
}: ToolLandingStandardSectionsProps["faq"] & { compactMobile?: boolean }) {
|
|
365
573
|
return (
|
|
366
574
|
<section
|
|
367
|
-
className=
|
|
575
|
+
className={`relative left-1/2 w-screen -translate-x-1/2 overflow-hidden border-t border-[#E8EBF0] bg-white ${compactMobile ? "py-16 md:py-24 lg:py-32" : "py-24 md:py-28 lg:py-32"}`}
|
|
368
576
|
data-tool-standard-section="faq"
|
|
369
577
|
>
|
|
370
|
-
<div className="relative mx-auto grid max-w-[
|
|
578
|
+
<div className="relative mx-auto grid w-full max-w-[76rem] gap-12 px-4 sm:px-8 lg:grid-cols-[0.42fr_0.58fr] lg:gap-[90px]">
|
|
371
579
|
<div>
|
|
372
580
|
<h2 className="max-w-[620px] whitespace-pre-line text-balance text-[2rem] font-semibold leading-[1.18] text-[#010205] md:text-[40px]">
|
|
373
581
|
{title}
|
|
@@ -375,7 +583,7 @@ function ToolStandardFaqSection({
|
|
|
375
583
|
<p className="mt-8 max-w-[531px] text-pretty text-base leading-[26px] text-[#0D1216]">
|
|
376
584
|
{intro}
|
|
377
585
|
</p>
|
|
378
|
-
{ctaHref ? (
|
|
586
|
+
{showCta && ctaHref ? (
|
|
379
587
|
<Button
|
|
380
588
|
asChild
|
|
381
589
|
variant="outline"
|
|
@@ -383,7 +591,7 @@ function ToolStandardFaqSection({
|
|
|
383
591
|
>
|
|
384
592
|
<a href={ctaHref}>{ctaLabel}</a>
|
|
385
593
|
</Button>
|
|
386
|
-
) : (
|
|
594
|
+
) : showCta ? (
|
|
387
595
|
<Button
|
|
388
596
|
type="button"
|
|
389
597
|
variant="outline"
|
|
@@ -391,7 +599,7 @@ function ToolStandardFaqSection({
|
|
|
391
599
|
>
|
|
392
600
|
{ctaLabel}
|
|
393
601
|
</Button>
|
|
394
|
-
)}
|
|
602
|
+
) : null}
|
|
395
603
|
</div>
|
|
396
604
|
<div className="min-w-0">
|
|
397
605
|
{items.map((item, index) => (
|
|
@@ -426,13 +634,15 @@ function ToolStandardImageFrame({
|
|
|
426
634
|
alt,
|
|
427
635
|
src,
|
|
428
636
|
priority = false,
|
|
637
|
+
className,
|
|
429
638
|
}: {
|
|
430
639
|
alt: string;
|
|
431
640
|
src: string;
|
|
432
641
|
priority?: boolean;
|
|
642
|
+
className?: string;
|
|
433
643
|
}) {
|
|
434
644
|
return (
|
|
435
|
-
<figure className=
|
|
645
|
+
<figure className={`mx-auto w-full min-w-0 max-w-[760px] overflow-hidden rounded-2xl border border-[#40576D]/[0.07] bg-white xl:max-w-none ${className ?? ""}`}>
|
|
436
646
|
<Image
|
|
437
647
|
alt={alt}
|
|
438
648
|
className="h-auto w-full object-cover"
|
|
@@ -17,6 +17,7 @@ import { kokoCodexAssistantSpec } from "./koko-codex-assistant/koko-codex-assist
|
|
|
17
17
|
import { logoGeneratorSpec } from "./logo-generator/logo-generator.spec";
|
|
18
18
|
import { marketForecastReportSpec } from "./market-forecast-report/market-forecast-report.spec";
|
|
19
19
|
import { millionUserGrowthTestSpec } from "./million-user-growth-test/million-user-growth-test.spec";
|
|
20
|
+
import { mrrCalculatorSpec } from "./mrr-calculator/mrr-calculator.spec";
|
|
20
21
|
import { ogImageGeneratorSpec } from "./og-image-generator/og-image-generator.spec";
|
|
21
22
|
import { createPackageToolSpec } from "./package/to-tool-spec";
|
|
22
23
|
import { pitchDeckGeneratorSpec } from "./pitch-deck-generator/pitch-deck-generator.spec";
|
|
@@ -46,6 +47,7 @@ const toolSpecs: ToolSpec[] = [
|
|
|
46
47
|
productNameGeneratorSpec,
|
|
47
48
|
productPosterGeneratorSpec,
|
|
48
49
|
millionUserGrowthTestSpec,
|
|
50
|
+
mrrCalculatorSpec,
|
|
49
51
|
marketForecastReportSpec,
|
|
50
52
|
productUiGeneratorSpec,
|
|
51
53
|
appStoreScreenshotGeneratorSpec,
|
|
@@ -15,6 +15,7 @@ import { GtmReadinessChecklistWidget } from "./gtm-readiness-checklist/widget";
|
|
|
15
15
|
import { KokoCodexAssistantDownload } from "./koko-codex-assistant/widget";
|
|
16
16
|
import { LogoGenerator } from "./logo-generator/widget";
|
|
17
17
|
import { MillionUserGrowthTest } from "./million-user-growth-test/widget";
|
|
18
|
+
import { MrrCalculator } from "./mrr-calculator/widget";
|
|
18
19
|
import { OgImageGenerator } from "./og-image-generator/widget";
|
|
19
20
|
import { PitchDeckGenerator } from "./pitch-deck-generator/widget";
|
|
20
21
|
import { ProductAgreementGenerator } from "./product-agreement-generator/widget";
|
|
@@ -50,6 +51,7 @@ export const colocatedToolWidgets: Record<string, ComponentType> = {
|
|
|
50
51
|
"koko-codex-assistant": KokoCodexAssistantDownload,
|
|
51
52
|
"logo-generator": LogoGenerator,
|
|
52
53
|
"million-user-growth-test": MillionUserGrowthTest,
|
|
54
|
+
"mrr-calculator": MrrCalculator,
|
|
53
55
|
"og-image-generator": OgImageGenerator,
|
|
54
56
|
"pitch-deck-generator": PitchDeckGenerator,
|
|
55
57
|
"product-agreement-generator": ProductAgreementGenerator,
|