@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 +1 -1
- package/src/features/tools/pitch-deck-generator/pitch-deck-library.tsx +12 -5
- package/src/features/tools/shared/tool-guide-section.tsx +5 -1
- package/src/features/tools/shared/tool-page-shell.tsx +10 -4
- package/src/features/tools/shared/tool-standard-landing-sections.tsx +4 -1
- package/src/i18n/messages/en/tools/build-in-public-story-generator.ts +1 -1
- package/src/i18n/messages/zh/tools/build-in-public-story-generator.ts +1 -1
- package/src/i18n/messages/zh-tw/tools/build-in-public-story-generator.ts +1 -1
- package/src/lib/static-asset.ts +52 -0
package/package.json
CHANGED
|
@@ -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={
|
|
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={
|
|
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={
|
|
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:
|
|
80
|
+
background: resolvedHeroBackground ? heroBackgroundColor : "#F4E6D8",
|
|
77
81
|
}}
|
|
78
82
|
>
|
|
79
|
-
{
|
|
83
|
+
{resolvedHeroBackground ? (
|
|
80
84
|
<>
|
|
81
85
|
{heroBackgroundImageFrameClassName ? (
|
|
82
86
|
<div className={`absolute ${heroBackgroundImageFrameClassName}`}>
|
|
83
87
|
<Image
|
|
84
|
-
src={
|
|
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={
|
|
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={
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|