@tomako/tools-runtime 0.1.2 → 0.1.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomako/tools-runtime",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Shared interactive tools runtime for Tomako-FE workspace dialogs and Tomako-SEO tool pages",
5
5
  "type": "module",
6
6
  "exports": {
@@ -20,20 +20,23 @@ import {
20
20
  import { useLocale, useTranslations } from "../../../i18n/client";
21
21
  import { isZhRouteLocale } from "../../../i18n/locale-fallback";
22
22
  import type { AppLocale } from "../../../i18n/locale";
23
+ import { isRemoteStaticAsset, staticAsset } from "../../../lib/static-asset";
23
24
 
24
25
  function deckTitle(entry: PitchDeckLibraryEntry, roundLabel: string): string {
25
26
  return `${entry.company} ${roundLabel} Pitch Deck (${entry.year})`;
26
27
  }
27
28
 
28
29
  function DeckCover({ entry, alt }: { entry: PitchDeckLibraryEntry; alt: string }) {
30
+ const src = staticAsset(`${PITCH_DECK_LIBRARY_COVER_BASE}/${entry.slug}-cover.webp`);
29
31
  return (
30
32
  <div className="relative aspect-video w-full overflow-hidden bg-[#EEEAE0]">
31
33
  <Image
32
- src={`${PITCH_DECK_LIBRARY_COVER_BASE}/${entry.slug}-cover.webp`}
34
+ src={src}
33
35
  alt={alt}
34
36
  fill
35
37
  sizes="(min-width: 1280px) 25vw, (min-width: 768px) 33vw, 100vw"
36
38
  className="object-cover"
39
+ unoptimized={isRemoteStaticAsset(src)}
37
40
  />
38
41
  <span className="absolute left-3 top-3 rounded-md bg-[#111111] px-2 py-1 text-[11px] font-semibold tracking-wide text-white">
39
42
  BP
@@ -98,21 +101,25 @@ export function PitchDeckLibrary() {
98
101
  <p className="text-sm leading-6 text-[#555555]">{activeCopy.takeaway}</p>
99
102
  {active.pages.length ? (
100
103
  <div className="mt-4 grid gap-4">
101
- {active.pages.map((page, index) => (
104
+ {active.pages.map((page, index) => {
105
+ const src = staticAsset(page);
106
+ return (
102
107
  <div
103
108
  key={page}
104
109
  className="overflow-hidden rounded-[0.75rem] border border-[#E1DDD3] bg-[#EEEAE0]"
105
110
  >
106
111
  <Image
107
- src={page}
112
+ src={src}
108
113
  alt={t("pageAlt", { company: active.company, page: index + 1 })}
109
114
  width={1440}
110
115
  height={1080}
111
116
  sizes="(min-width: 1024px) 960px, 100vw"
112
117
  className="h-auto w-full"
118
+ unoptimized={isRemoteStaticAsset(src)}
113
119
  />
114
120
  </div>
115
- ))}
121
+ );
122
+ })}
116
123
  </div>
117
124
  ) : (
118
125
  <div className="mt-4 grid gap-4">
@@ -144,7 +151,7 @@ export function PitchDeckLibrary() {
144
151
  type="button"
145
152
  className="min-h-9 bg-[#111111] text-white hover:bg-[#2A2A2A]"
146
153
  >
147
- <a href={active.downloadUrl} download>
154
+ <a href={staticAsset(active.downloadUrl)} download>
148
155
  <Download className="size-4" aria-hidden />
149
156
  {t("download")}
150
157
  </a>
@@ -1,6 +1,8 @@
1
1
  import type { ReactNode } from "react";
2
2
  import Image from "next/image";
3
3
 
4
+ import { isRemoteStaticAsset, staticAsset } from "../../../lib/static-asset";
5
+
4
6
  type VisualTone = "lime" | "blue" | "coral" | "wine";
5
7
 
6
8
  const toneStyles: Record<
@@ -171,14 +173,16 @@ export function ToolImageVisual({
171
173
  priority?: boolean;
172
174
  wide?: boolean;
173
175
  }) {
176
+ const resolved = staticAsset(src);
174
177
  return (
175
178
  <figure className="overflow-hidden rounded-[1.5rem]">
176
179
  <Image
177
- src={src}
180
+ src={resolved}
178
181
  alt={alt}
179
182
  width={1672}
180
183
  height={941}
181
184
  priority={priority}
185
+ unoptimized={isRemoteStaticAsset(resolved)}
182
186
  sizes={
183
187
  wide
184
188
  ? "(min-width: 1024px) 1120px, 100vw"
@@ -5,6 +5,7 @@ import type { ReactNode } from "react";
5
5
  import { relatedToolSlugsByTool } from "../../../constants/related-tools";
6
6
  import { RelatedTools } from "../../../host/server";
7
7
  import type { AppLocale } from "../../../i18n/locale";
8
+ import { isRemoteStaticAsset, staticAsset } from "../../../lib/static-asset";
8
9
  import { resolveToolLocaleCopy } from "../../../lib/tools/resolve-tool-locale-copy";
9
10
 
10
11
  import type { ToolSpec } from "../types";
@@ -63,6 +64,9 @@ export async function ToolPageShell({
63
64
  // Full messages: public getRequestConfig omits toolsPages; client tools layout alone is not enough for RSC.
64
65
  const messages = await loadToolMessages(locale);
65
66
  const t = createToolMessageTranslator(locale, messages, "toolsPages.shared");
67
+ const resolvedHeroBackground = heroBackgroundImage
68
+ ? staticAsset(heroBackgroundImage)
69
+ : undefined;
66
70
 
67
71
  return (
68
72
  <div className="min-h-screen overflow-x-clip bg-white text-[#111111]">
@@ -73,28 +77,30 @@ export async function ToolPageShell({
73
77
  className="relative w-full overflow-hidden text-[#2B211A]"
74
78
  style={{
75
79
  minHeight: "min(72svh, 680px)",
76
- background: heroBackgroundImage ? heroBackgroundColor : "#F4E6D8",
80
+ background: resolvedHeroBackground ? heroBackgroundColor : "#F4E6D8",
77
81
  }}
78
82
  >
79
- {heroBackgroundImage ? (
83
+ {resolvedHeroBackground ? (
80
84
  <>
81
85
  {heroBackgroundImageFrameClassName ? (
82
86
  <div className={`absolute ${heroBackgroundImageFrameClassName}`}>
83
87
  <Image
84
- src={heroBackgroundImage}
88
+ src={resolvedHeroBackground}
85
89
  alt={heroBackgroundAlt}
86
90
  fill
87
91
  priority
92
+ unoptimized={isRemoteStaticAsset(resolvedHeroBackground)}
88
93
  sizes={heroBackgroundImageSizes}
89
94
  className={`pointer-events-none ${heroBackgroundImageClassName}`}
90
95
  />
91
96
  </div>
92
97
  ) : (
93
98
  <Image
94
- src={heroBackgroundImage}
99
+ src={resolvedHeroBackground}
95
100
  alt={heroBackgroundAlt}
96
101
  fill
97
102
  priority
103
+ unoptimized={isRemoteStaticAsset(resolvedHeroBackground)}
98
104
  sizes={heroBackgroundImageSizes}
99
105
  className={`pointer-events-none ${heroBackgroundImageClassName}`}
100
106
  />
@@ -5,6 +5,7 @@ import Image from "next/image";
5
5
 
6
6
  import { Button } from "@tomako/ui/button";
7
7
 
8
+ import { isRemoteStaticAsset, staticAsset } from "../../../lib/static-asset";
8
9
  import { ToolStandardGenerationSteps } from "./tool-standard-generation-steps";
9
10
 
10
11
  export type ToolStandardBenefitCard = {
@@ -641,6 +642,7 @@ function ToolStandardImageFrame({
641
642
  priority?: boolean;
642
643
  className?: string;
643
644
  }) {
645
+ const resolved = staticAsset(src);
644
646
  return (
645
647
  <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 ?? ""}`}>
646
648
  <Image
@@ -649,7 +651,8 @@ function ToolStandardImageFrame({
649
651
  height={840}
650
652
  priority={priority}
651
653
  sizes="(min-width: 1536px) 630px, (min-width: 1280px) 45vw, (min-width: 768px) 760px, calc(100vw - 40px)"
652
- src={src}
654
+ src={resolved}
655
+ unoptimized={isRemoteStaticAsset(resolved)}
653
656
  width={1260}
654
657
  />
655
658
  </figure>
@@ -152,7 +152,7 @@ const buildInPublicStoryGenerator = {
152
152
  stageLabel: "Current stage",
153
153
  productContextLabel: "Product context",
154
154
  productContextPlaceholder:
155
- "Tomako is an AI GTM OS for indie makers that turns product context into channel strategy, launch assets, and growth actions.",
155
+ "Tomako is an always-on AI CMO for software teams that turns product and market context into prioritized, reviewable growth work across content, creators, search, feedback, launches, and competitor intelligence.",
156
156
  thisWeekLabel: "What actually happened this week",
157
157
  thisWeekPlaceholder:
158
158
  "Example: We used Tomako to rank cold-start channels and learned that Reddit is better for asking a specific judgment question, while Product Hunt is not ready yet.",
@@ -153,7 +153,7 @@ const buildInPublicStoryGenerator = {
153
153
  stageLabel: "当前阶段",
154
154
  productContextLabel: "产品背景",
155
155
  productContextPlaceholder:
156
- "Tomako 是面向独立开发者的 AI GTM OS,帮助把产品上下文转成渠道策略、发布素材和增长动作。",
156
+ "Tomako 是软件团队 24 小时在线的 AI CMO,把产品与市场上下文转成有优先级、可审核的内容、KOL、SEO、用户反馈、发布和竞品情报工作。",
157
157
  thisWeekLabel: "这周真实进展",
158
158
  thisWeekPlaceholder:
159
159
  "例如:我们用 Tomako 生成了冷启动渠道排序,发现 Reddit 适合问具体判断,Product Hunt 暂时不适合直接冲发布。",
@@ -153,7 +153,7 @@ const buildInPublicStoryGenerator = {
153
153
  stageLabel: "當前階段",
154
154
  productContextLabel: "產品背景",
155
155
  productContextPlaceholder:
156
- "Tomako 是面向獨立開發者的 AI GTM OS,幫助把產品上下文轉成渠道策略、發佈素材和增長動作。",
156
+ "Tomako 是軟體團隊 24 小時在線的 AI CMO,把產品與市場上下文轉成有優先順序、可審核的內容、KOL、SEO、用戶回饋、發佈和競品情報工作。",
157
157
  thisWeekLabel: "這周真實進展",
158
158
  thisWeekPlaceholder:
159
159
  "例如:我們用 Tomako 生成了冷啟動渠道排序,發現 Reddit 適合問具體判斷,Product Hunt 暫時不適合直接衝發佈。",
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Resolve root-relative marketing asset paths to the static CDN.
3
+ * Mirrors `@tomako/public-chrome` staticAsset so tools-runtime stays free of
4
+ * a chrome peer dependency.
5
+ *
6
+ * Image styles (`!960` etc.) follow Tomako OSS image guidelines — append only
7
+ * when serving from STATIC_ORIGIN; do not rename object keys to `.webp`.
8
+ */
9
+
10
+ const STYLE_SUFFIX_PATTERN = /!(default|256|640|960|1600|2560)$/;
11
+ const RASTER_EXT = /\.(png|jpe?g|webp|gif|bmp|tiff?)$/i;
12
+
13
+ export const DEFAULT_STATIC_ORIGIN = "https://static.tomako.ai";
14
+
15
+ export type OssImageStyle =
16
+ | "default"
17
+ | "256"
18
+ | "640"
19
+ | "960"
20
+ | "1600"
21
+ | "2560";
22
+
23
+ function readStaticOrigin(): string {
24
+ const raw = process.env.NEXT_PUBLIC_STATIC_ORIGIN;
25
+ if (raw !== undefined) {
26
+ const value = raw.trim().replace(/\/$/, "");
27
+ if (value.length > 0) return value;
28
+ }
29
+ return DEFAULT_STATIC_ORIGIN;
30
+ }
31
+
32
+ export function staticAsset(path: string, style?: OssImageStyle): string {
33
+ if (typeof path !== "string" || path.length === 0) {
34
+ throw new Error("staticAsset(path): path is required");
35
+ }
36
+ if (/^https?:\/\//i.test(path) || path.startsWith("//")) {
37
+ return path;
38
+ }
39
+
40
+ const normalized = path.startsWith("/") ? path : `/${path}`;
41
+ const origin = readStaticOrigin();
42
+ const absolute = `${origin}/static${normalized}`;
43
+ if (!style || !RASTER_EXT.test(normalized.replace(STYLE_SUFFIX_PATTERN, ""))) {
44
+ return absolute;
45
+ }
46
+ return `${absolute.replace(STYLE_SUFFIX_PATTERN, "")}!${style}`;
47
+ }
48
+
49
+ /** True when src is an absolute CDN/http URL (skip Next image optimizer). */
50
+ export function isRemoteStaticAsset(src: string): boolean {
51
+ return /^https?:\/\//i.test(src) || src.startsWith("//");
52
+ }