@tomako/tools-runtime 0.1.3 → 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/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>
|
|
@@ -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
|
+
}
|