@tomako/tools-runtime 0.1.1 → 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.
Files changed (33) hide show
  1. package/package.json +13 -4
  2. package/src/components/tools/workspace/tool-workspace-layout.tsx +1 -1
  3. package/src/constants/related-tools.ts +4 -0
  4. package/src/features/tools/app-icon-resizer/app-icon-resizer.container.tsx +15 -2
  5. package/src/features/tools/app-icon-resizer/app-icon-resizer.spec.ts +1 -1
  6. package/src/features/tools/app-icon-resizer/widget/app-icon-resizer.tsx +54 -11
  7. package/src/features/tools/app-icon-resizer/widget/platform-icons.tsx +2 -5
  8. package/src/features/tools/mrr-calculator/mrr-calculator.container.tsx +23 -0
  9. package/src/features/tools/mrr-calculator/mrr-calculator.spec.ts +15 -0
  10. package/src/features/tools/mrr-calculator/widget/calculations.ts +74 -0
  11. package/src/features/tools/mrr-calculator/widget/index.ts +1 -0
  12. package/src/features/tools/mrr-calculator/widget/mrr-calculator.tsx +581 -0
  13. package/src/features/tools/registry.ts +6 -0
  14. package/src/features/tools/runtime-registry.ts +4 -0
  15. package/src/features/tools/shared/create-standard-landing-container.tsx +35 -5
  16. package/src/features/tools/shared/tool-page-shell.tsx +4 -4
  17. package/src/features/tools/shared/tool-standard-landing-sections.tsx +262 -52
  18. package/src/features/tools/spec-registry.ts +2 -0
  19. package/src/features/tools/widget-registry.ts +2 -0
  20. package/src/i18n/messages/en/tools/app-icon-resizer.ts +90 -62
  21. package/src/i18n/messages/en/tools/index.ts +2 -0
  22. package/src/i18n/messages/en/tools/mrr-calculator.ts +202 -0
  23. package/src/i18n/messages/en/tools-ui.ts +3 -2
  24. package/src/i18n/messages/zh/page/tools.ts +2 -3
  25. package/src/i18n/messages/zh/tools/app-icon-resizer.ts +90 -62
  26. package/src/i18n/messages/zh/tools/index.ts +2 -0
  27. package/src/i18n/messages/zh/tools/mrr-calculator.ts +112 -0
  28. package/src/i18n/messages/zh/tools-ui.ts +3 -2
  29. package/src/i18n/messages/zh-tw/tools/app-icon-resizer.ts +90 -62
  30. package/src/i18n/messages/zh-tw/tools/index.ts +2 -0
  31. package/src/i18n/messages/zh-tw/tools/mrr-calculator.ts +52 -0
  32. package/src/i18n/messages/zh-tw/tools-ui.ts +3 -2
  33. 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 string[];
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 string[];
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
- generation: {
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
- generation: <ToolStandardGenerationSection {...generation} />,
131
- faq: <ToolStandardFaqSection {...faq} />,
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
- }: ToolStandardNarrativeSection) {
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] px-5 py-20 md:px-10 md:py-24 lg:px-16 lg:py-28 ${
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="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"
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-[1284px]">
183
- <h2 className="text-balance text-center text-[2rem] font-semibold leading-[1.18] text-[#010205] md:text-[40px]">
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-8 grid gap-6 lg:grid-cols-3">
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-[#F6F7F8] p-6"
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
- <div className="flex min-w-0 items-center gap-4">
196
- <span className="flex size-12 shrink-0 items-center justify-center rounded-lg bg-[#E0D9FC] text-[#0D1216]">
197
- <Icon aria-hidden className="size-5" />
198
- </span>
199
- <h3 className="text-xl font-semibold leading-snug text-[#010205] md:text-2xl">
200
- {item.title}
201
- </h3>
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
- }: ToolStandardImageTextSection & { imagePriority?: boolean }) {
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="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"
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-[1356px] gap-8 xl:items-center xl:gap-12 ${
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={`min-w-0 ${imageFirst ? "xl:pl-4" : "xl:pr-4"}`}>
239
- <h2 className="max-w-[620px] whitespace-pre-line text-balance text-[2rem] font-semibold leading-[1.18] text-[#010205] md:text-[40px]">
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-pretty text-base leading-[26px] text-[#0D1216]">
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
- }: ToolLandingStandardSectionsProps["generation"]) {
465
+ showImage = true,
466
+ showCta = true,
467
+ compactMobile = false,
468
+ }: ToolLandingStandardSectionsProps["generation"] & { compactMobile?: boolean }) {
263
469
  return (
264
470
  <section
265
- 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"
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="mx-auto grid max-w-[1284px] gap-10 xl:grid-cols-2 xl:items-center xl:gap-14">
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] px-5 py-24 md:px-10 md:py-28 lg:px-16 lg:py-32"
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-[1284px]">
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
- }: ToolLandingStandardSectionsProps["faq"]) {
570
+ showCta = true,
571
+ compactMobile = false,
572
+ }: ToolLandingStandardSectionsProps["faq"] & { compactMobile?: boolean }) {
365
573
  return (
366
574
  <section
367
- className="relative left-1/2 w-screen -translate-x-1/2 overflow-hidden border-t border-[#E8EBF0] bg-white px-5 py-24 md:px-10 md:py-28 lg:px-16 lg:py-32"
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-[1280px] gap-12 lg:grid-cols-[0.42fr_0.58fr] lg:gap-[90px]">
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="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">
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,