@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.
Files changed (36) 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/build-in-public-story-generator.ts +1 -1
  22. package/src/i18n/messages/en/tools/index.ts +2 -0
  23. package/src/i18n/messages/en/tools/mrr-calculator.ts +202 -0
  24. package/src/i18n/messages/en/tools-ui.ts +3 -2
  25. package/src/i18n/messages/zh/page/tools.ts +2 -3
  26. package/src/i18n/messages/zh/tools/app-icon-resizer.ts +90 -62
  27. package/src/i18n/messages/zh/tools/build-in-public-story-generator.ts +1 -1
  28. package/src/i18n/messages/zh/tools/index.ts +2 -0
  29. package/src/i18n/messages/zh/tools/mrr-calculator.ts +112 -0
  30. package/src/i18n/messages/zh/tools-ui.ts +3 -2
  31. package/src/i18n/messages/zh-tw/tools/app-icon-resizer.ts +90 -62
  32. package/src/i18n/messages/zh-tw/tools/build-in-public-story-generator.ts +1 -1
  33. package/src/i18n/messages/zh-tw/tools/index.ts +2 -0
  34. package/src/i18n/messages/zh-tw/tools/mrr-calculator.ts +52 -0
  35. package/src/i18n/messages/zh-tw/tools-ui.ts +3 -2
  36. package/src/lib/tools/resolve-tool-locale-copy.ts +7 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomako/tools-runtime",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Shared interactive tools runtime for Tomako-FE workspace dialogs and Tomako-SEO tool pages",
5
5
  "type": "module",
6
6
  "exports": {
@@ -176,6 +176,12 @@
176
176
  "./features/tools/million-user-growth-test/widget/index": "./src/features/tools/million-user-growth-test/widget/index.ts",
177
177
  "./features/tools/million-user-growth-test/widget": "./src/features/tools/million-user-growth-test/widget/index.ts",
178
178
  "./features/tools/million-user-growth-test/widget/million-user-growth-test": "./src/features/tools/million-user-growth-test/widget/million-user-growth-test.tsx",
179
+ "./features/tools/mrr-calculator/mrr-calculator.container": "./src/features/tools/mrr-calculator/mrr-calculator.container.tsx",
180
+ "./features/tools/mrr-calculator/mrr-calculator.spec": "./src/features/tools/mrr-calculator/mrr-calculator.spec.ts",
181
+ "./features/tools/mrr-calculator/widget/calculations": "./src/features/tools/mrr-calculator/widget/calculations.ts",
182
+ "./features/tools/mrr-calculator/widget/index": "./src/features/tools/mrr-calculator/widget/index.ts",
183
+ "./features/tools/mrr-calculator/widget": "./src/features/tools/mrr-calculator/widget/index.ts",
184
+ "./features/tools/mrr-calculator/widget/mrr-calculator": "./src/features/tools/mrr-calculator/widget/mrr-calculator.tsx",
179
185
  "./features/tools/module-loader": "./src/features/tools/module-loader.ts",
180
186
  "./features/tools/og-image-generator/og-image-generator.container": "./src/features/tools/og-image-generator/og-image-generator.container.tsx",
181
187
  "./features/tools/og-image-generator/og-image-generator.spec": "./src/features/tools/og-image-generator/og-image-generator.spec.ts",
@@ -410,6 +416,7 @@
410
416
  "./i18n/messages/en/tools/logo-generator": "./src/i18n/messages/en/tools/logo-generator.ts",
411
417
  "./i18n/messages/en/tools/market-forecast-report": "./src/i18n/messages/en/tools/market-forecast-report.ts",
412
418
  "./i18n/messages/en/tools/million-user-growth-test": "./src/i18n/messages/en/tools/million-user-growth-test.ts",
419
+ "./i18n/messages/en/tools/mrr-calculator": "./src/i18n/messages/en/tools/mrr-calculator.ts",
413
420
  "./i18n/messages/en/tools/og-image-generator": "./src/i18n/messages/en/tools/og-image-generator.ts",
414
421
  "./i18n/messages/en/tools/pitch-deck-generator": "./src/i18n/messages/en/tools/pitch-deck-generator.ts",
415
422
  "./i18n/messages/en/tools/product-agreement-generator": "./src/i18n/messages/en/tools/product-agreement-generator.ts",
@@ -448,6 +455,7 @@
448
455
  "./i18n/messages/zh/tools/logo-generator": "./src/i18n/messages/zh/tools/logo-generator.ts",
449
456
  "./i18n/messages/zh/tools/market-forecast-report": "./src/i18n/messages/zh/tools/market-forecast-report.ts",
450
457
  "./i18n/messages/zh/tools/million-user-growth-test": "./src/i18n/messages/zh/tools/million-user-growth-test.ts",
458
+ "./i18n/messages/zh/tools/mrr-calculator": "./src/i18n/messages/zh/tools/mrr-calculator.ts",
451
459
  "./i18n/messages/zh/tools/og-image-generator": "./src/i18n/messages/zh/tools/og-image-generator.ts",
452
460
  "./i18n/messages/zh/tools/pitch-deck-generator": "./src/i18n/messages/zh/tools/pitch-deck-generator.ts",
453
461
  "./i18n/messages/zh/tools/product-agreement-generator": "./src/i18n/messages/zh/tools/product-agreement-generator.ts",
@@ -486,6 +494,7 @@
486
494
  "./i18n/messages/zh-tw/tools/logo-generator": "./src/i18n/messages/zh-tw/tools/logo-generator.ts",
487
495
  "./i18n/messages/zh-tw/tools/market-forecast-report": "./src/i18n/messages/zh-tw/tools/market-forecast-report.ts",
488
496
  "./i18n/messages/zh-tw/tools/million-user-growth-test": "./src/i18n/messages/zh-tw/tools/million-user-growth-test.ts",
497
+ "./i18n/messages/zh-tw/tools/mrr-calculator": "./src/i18n/messages/zh-tw/tools/mrr-calculator.ts",
489
498
  "./i18n/messages/zh-tw/tools/og-image-generator": "./src/i18n/messages/zh-tw/tools/og-image-generator.ts",
490
499
  "./i18n/messages/zh-tw/tools/pitch-deck-generator": "./src/i18n/messages/zh-tw/tools/pitch-deck-generator.ts",
491
500
  "./i18n/messages/zh-tw/tools/product-agreement-generator": "./src/i18n/messages/zh-tw/tools/product-agreement-generator.ts",
@@ -606,6 +615,7 @@
606
615
  }
607
616
  },
608
617
  "devDependencies": {
618
+ "@tomako/ui": "workspace:*",
609
619
  "@types/react": "^19.0.0",
610
620
  "@ffmpeg/ffmpeg": "^0.12.15",
611
621
  "@ffmpeg/util": "^0.12.2",
@@ -617,11 +627,10 @@
617
627
  "react": "^19.0.0",
618
628
  "react-dom": "^19.0.0",
619
629
  "typescript": "^5.8.0",
620
- "zod": "^4.4.3",
621
- "@tomako/ui": "0.1.0"
630
+ "zod": "^4.4.3"
622
631
  },
623
632
  "license": "UNLICENSED",
624
633
  "publishConfig": {
625
634
  "access": "public"
626
635
  }
627
- }
636
+ }
@@ -173,7 +173,7 @@ export function ToolWorkspaceLayout({
173
173
  >
174
174
  <style>{`
175
175
  .tool-workspace-grid {
176
- grid-template-columns: 1fr;
176
+ grid-template-columns: minmax(0, 1fr);
177
177
  }
178
178
  @media (min-width: 1024px) {
179
179
  .tool-workspace-grid {
@@ -3,6 +3,10 @@
3
3
  * these slugs against the current public tool catalogue before rendering.
4
4
  */
5
5
  export const relatedToolSlugsByTool: Readonly<Record<string, readonly string[]>> = {
6
+ "app-icon-resizer": [
7
+ "app-store-screenshot-generator",
8
+ "social-platform-asset-adapter",
9
+ ],
6
10
  "cold-start-channel-selector": [
7
11
  "gtm-readiness-checklist",
8
12
  "community-post-risk-checker",
@@ -1,4 +1,4 @@
1
- import { Download, FileArchive, Upload } from "lucide-react";
1
+ import { Eye, FolderTree, Layers3 } from "lucide-react";
2
2
 
3
3
  import { createStandardLandingContainer } from "../../../features/tools/shared/create-standard-landing-container";
4
4
  import { AppIconResizer } from "./widget";
@@ -6,6 +6,19 @@ import { AppIconResizer } from "./widget";
6
6
  export const AppIconResizerContainer = createStandardLandingContainer({
7
7
  guideNamespace: "toolsPages.appIconResizer.guide",
8
8
  Widget: AppIconResizer,
9
- benefitIcons: [Upload, FileArchive, Download],
9
+ benefitIcons: [Layers3, FolderTree, Eye],
10
10
  generationImageSrc: "/tools/app-icon-resizer-section-steps-8bit-v3.webp",
11
+ copyKeys: {
12
+ reportPreview: "standard.outputPreview",
13
+ referenceTable: "standard.referenceTable",
14
+ },
15
+ contentCtaHref: "#tool-workspace",
16
+ sectionOrder: [
17
+ "reportPreview",
18
+ "benefits",
19
+ "imageTextSections",
20
+ "generation",
21
+ "referenceTable",
22
+ "faq",
23
+ ],
11
24
  });
@@ -6,7 +6,7 @@ export const appIconResizerSpec: ToolSpec = {
6
6
  riskLevel: "low",
7
7
  seo: {
8
8
  "publishedAt": "2026-06-30",
9
- "updatedAt": "2026-07-13",
9
+ "updatedAt": "2026-08-19",
10
10
  "readingTime": "5 min",
11
11
  "featured": false,
12
12
  "ogImage": "/tools/app-icon-resizer-section-primary-8bit-v3.webp"
@@ -14,7 +14,7 @@ import {
14
14
  getAppIconAssetSpecs,
15
15
  type AppIconPlatform,
16
16
  } from "../../../../lib/tools/app-icon-resizer-schema";
17
- import { FileArchive, RotateCcw } from "lucide-react";
17
+ import { FileArchive, FileImage, FolderTree, RotateCcw } from "lucide-react";
18
18
  import type { ChangeEvent, DragEvent, FormEvent } from "react";
19
19
  import { useEffect, useMemo, useRef, useState } from "react";
20
20
 
@@ -395,16 +395,7 @@ export function AppIconResizer() {
395
395
  t={t}
396
396
  />
397
397
  ) : (
398
- <ToolEmptyPanel
399
- body={t("outputEmptyHint")}
400
- className="h-full min-h-0 flex-1 rounded-lg"
401
- icon={
402
- <span className="mx-auto flex size-12 items-center justify-center rounded-lg bg-white text-[#111111] shadow-[0_10px_26px_rgba(17,17,17,0.08)]">
403
- <FileArchive aria-hidden className="size-5" />
404
- </span>
405
- }
406
- title={t("outputEmptyTitle")}
407
- />
398
+ <AppIconPackageEmptyState platform={platform} t={t} />
408
399
  )}
409
400
  </ToolPreviewPanel>
410
401
  </div>
@@ -442,3 +433,55 @@ export function AppIconResizer() {
442
433
  </>
443
434
  );
444
435
  }
436
+
437
+ function AppIconPackageEmptyState({
438
+ platform,
439
+ t,
440
+ }: {
441
+ platform: AppIconPlatform;
442
+ t: (key: string, vars?: Record<string, string | number>) => string;
443
+ }) {
444
+ const paths = [
445
+ ...(platform !== "google"
446
+ ? ["Apple/AppIcon.appiconset/", "Apple/AppStoreIcon-1024.png"]
447
+ : []),
448
+ ...(platform !== "apple"
449
+ ? ["GooglePlay/app-icon-512.png", "Android/res/mipmap-*/"]
450
+ : []),
451
+ "README.txt",
452
+ ];
453
+
454
+ return (
455
+ <ToolEmptyPanel
456
+ body={
457
+ <span className="block w-full">
458
+ <span className="block">{t("outputEmptyHint")}</span>
459
+ <span className="mx-auto mt-5 block max-w-md rounded-xl border border-[#E1E5EA] bg-white p-3 text-left shadow-[0_12px_30px_rgba(17,17,17,0.05)]">
460
+ <span className="flex items-center gap-2 border-b border-[#EEF0F2] px-1 pb-2 text-xs font-semibold text-[#111111]">
461
+ <FolderTree aria-hidden className="size-4" />
462
+ {t("outputExampleLabel")}
463
+ </span>
464
+ <span className="mt-2 grid gap-1.5">
465
+ {paths.map((path) => (
466
+ <span
467
+ key={path}
468
+ className="flex min-w-0 items-center gap-2 rounded-md px-2 py-1.5 font-mono text-[11px] leading-4 text-[#4D5964]"
469
+ >
470
+ <FileImage aria-hidden className="size-3.5 shrink-0 text-[#7B8792]" />
471
+ <span className="min-w-0 break-all">{path}</span>
472
+ </span>
473
+ ))}
474
+ </span>
475
+ </span>
476
+ </span>
477
+ }
478
+ className="h-full min-h-0 flex-1 rounded-lg"
479
+ icon={
480
+ <span className="mx-auto flex size-12 items-center justify-center rounded-lg bg-white text-[#111111] shadow-[0_10px_26px_rgba(17,17,17,0.08)]">
481
+ <FileArchive aria-hidden className="size-5" />
482
+ </span>
483
+ }
484
+ title={t("outputEmptyTitle")}
485
+ />
486
+ );
487
+ }
@@ -26,19 +26,15 @@ function PlatformBrandIcon({
26
26
  kind: "app-store" | "google-play";
27
27
  variant: "pair" | "single";
28
28
  }) {
29
- let sizeClass = "size-[34px]";
30
29
  let px = 34;
31
30
 
32
31
  if (variant === "pair") {
33
32
  if (kind === "google-play") {
34
- sizeClass = "size-[22px]";
35
33
  px = 22;
36
34
  } else {
37
- sizeClass = "size-[30px]";
38
35
  px = 30;
39
36
  }
40
37
  } else if (kind === "google-play") {
41
- sizeClass = "size-[26px]";
42
38
  px = 26;
43
39
  }
44
40
 
@@ -46,13 +42,14 @@ function PlatformBrandIcon({
46
42
  <Image
47
43
  alt=""
48
44
  aria-hidden
49
- className={`${sizeClass} shrink-0 object-contain`}
45
+ className="shrink-0 object-contain"
50
46
  height={px}
51
47
  src={
52
48
  kind === "app-store"
53
49
  ? "/tools/platform-app-store-icon.webp"
54
50
  : "/tools/platform-google-play-icon.webp"
55
51
  }
52
+ style={{ height: px, width: px }}
56
53
  width={px}
57
54
  />
58
55
  );
@@ -0,0 +1,23 @@
1
+ import { Calculator, Eye, ReceiptText } from "lucide-react";
2
+
3
+ import { createStandardLandingContainer } from "../../../features/tools/shared/create-standard-landing-container";
4
+ import { MrrCalculator } from "./widget";
5
+
6
+ export const MrrCalculatorContainer = createStandardLandingContainer({
7
+ guideNamespace: "toolsPages.mrrCalculator.guide",
8
+ Widget: MrrCalculator,
9
+ benefitIcons: [Calculator, ReceiptText, Eye],
10
+ generationImageSrc: "/tools/mrr-calculator-section-support-8bit.webp",
11
+ copyKeys: {
12
+ editorialSections: "standard.editorialSections",
13
+ },
14
+ compactMobileSections: true,
15
+ showContentCtas: false,
16
+ showGeneration: false,
17
+ sectionOrder: [
18
+ "benefits",
19
+ "imageTextSections",
20
+ "editorial",
21
+ "faq",
22
+ ],
23
+ });
@@ -0,0 +1,15 @@
1
+ import type { ToolSpec } from "../types";
2
+
3
+ export const mrrCalculatorSpec: ToolSpec = {
4
+ slug: "mrr-calculator",
5
+ toolType: "calculator",
6
+ riskLevel: "low",
7
+ seo: {
8
+ publishedAt: "2026-08-23",
9
+ updatedAt: "2026-08-23",
10
+ readingTime: "6 min",
11
+ featured: false,
12
+ ogImage: "/tools/mrr-calculator-section-primary-8bit.webp",
13
+ },
14
+ widget: { id: "mrr-calculator", mode: "custom" },
15
+ };
@@ -0,0 +1,74 @@
1
+ export type BillingPeriod = "month" | "quarter" | "year";
2
+
3
+ export type ExactMrrInput = {
4
+ activeCustomers: number;
5
+ recurringAmount: number;
6
+ billingPeriod: BillingPeriod;
7
+ previousMrr?: number;
8
+ };
9
+
10
+ export type ExactMrrResult = {
11
+ monthlyArpa: number;
12
+ mrr: number;
13
+ arr: number;
14
+ growthRate: number | null;
15
+ };
16
+
17
+ export type EstimatedMrrInput = {
18
+ monthlyVisits: number;
19
+ planPrice: number;
20
+ billingPeriod: BillingPeriod;
21
+ eligibleVisitShare: number;
22
+ visitorToPaidRate: number;
23
+ averagePaidLifetimeMonths: number;
24
+ };
25
+
26
+ export type EstimatedMrrResult = {
27
+ monthlyPlanPrice: number;
28
+ newPaidCustomers: number;
29
+ activePaidCustomers: number;
30
+ mrr: number;
31
+ arr: number;
32
+ };
33
+
34
+ const PERIOD_MONTHS: Record<BillingPeriod, number> = {
35
+ month: 1,
36
+ quarter: 3,
37
+ year: 12,
38
+ };
39
+
40
+ export function normalizeMonthlyAmount(amount: number, period: BillingPeriod): number {
41
+ return amount / PERIOD_MONTHS[period];
42
+ }
43
+
44
+ export function calculateExactMrr(input: ExactMrrInput): ExactMrrResult {
45
+ const monthlyArpa = normalizeMonthlyAmount(input.recurringAmount, input.billingPeriod);
46
+ const mrr = input.activeCustomers * monthlyArpa;
47
+ const growthRate =
48
+ input.previousMrr !== undefined && input.previousMrr > 0
49
+ ? (mrr - input.previousMrr) / input.previousMrr
50
+ : null;
51
+
52
+ return {
53
+ monthlyArpa,
54
+ mrr,
55
+ arr: mrr * 12,
56
+ growthRate,
57
+ };
58
+ }
59
+
60
+ export function estimateMrr(input: EstimatedMrrInput): EstimatedMrrResult {
61
+ const monthlyPlanPrice = normalizeMonthlyAmount(input.planPrice, input.billingPeriod);
62
+ const newPaidCustomers =
63
+ input.monthlyVisits * (input.eligibleVisitShare / 100) * (input.visitorToPaidRate / 100);
64
+ const activePaidCustomers = newPaidCustomers * input.averagePaidLifetimeMonths;
65
+ const mrr = activePaidCustomers * monthlyPlanPrice;
66
+
67
+ return {
68
+ monthlyPlanPrice,
69
+ newPaidCustomers,
70
+ activePaidCustomers,
71
+ mrr,
72
+ arr: mrr * 12,
73
+ };
74
+ }
@@ -0,0 +1 @@
1
+ export { MrrCalculator } from "./mrr-calculator";