@tomako/tools-runtime 0.1.0 → 0.1.1

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.0",
3
+ "version": "0.1.1",
4
4
  "description": "Shared interactive tools runtime for Tomako-FE workspace dialogs and Tomako-SEO tool pages",
5
5
  "type": "module",
6
6
  "exports": {
@@ -579,7 +579,6 @@
579
579
  ],
580
580
  "sideEffects": false,
581
581
  "peerDependencies": {
582
- "@tomako/public-chrome": ">=0.1.0",
583
582
  "@tomako/ui": ">=0.1.0",
584
583
  "next": ">=15",
585
584
  "next-intl": ">=4",
@@ -619,8 +618,7 @@
619
618
  "react-dom": "^19.0.0",
620
619
  "typescript": "^5.8.0",
621
620
  "zod": "^4.4.3",
622
- "@tomako/ui": "0.1.0",
623
- "@tomako/public-chrome": "0.1.0"
621
+ "@tomako/ui": "0.1.0"
624
622
  },
625
623
  "license": "UNLICENSED",
626
624
  "publishConfig": {
@@ -0,0 +1,71 @@
1
+ import type { CSSProperties } from "react";
2
+
3
+ export type TomatoLogoProps = {
4
+ href: string;
5
+ className?: string;
6
+ label?: string;
7
+ showUnionMark?: boolean;
8
+ inverted?: boolean;
9
+ target?: "_blank" | "_self" | "_parent" | "_top";
10
+ rel?: string;
11
+ /**
12
+ * When running behind an asset host / CDN.
13
+ * Keep it optional since SEO/public routes typically serve assets from `/`.
14
+ */
15
+ assetPrefix?: string;
16
+ };
17
+
18
+ export function TomatoLogo({
19
+ href,
20
+ className,
21
+ label = "Tomako",
22
+ showUnionMark,
23
+ inverted = false,
24
+ target,
25
+ rel,
26
+ assetPrefix = "",
27
+ }: TomatoLogoProps) {
28
+ const shouldShowUnionMark = showUnionMark ?? className === undefined;
29
+ const resolvedClassName =
30
+ className ??
31
+ "group flex w-[172px] shrink-0 cursor-pointer items-center justify-start gap-3 border-r border-[#DDD8CF] px-5 transition-colors hover:bg-white";
32
+
33
+ const backgroundImageBase: CSSProperties = {
34
+ backgroundRepeat: "no-repeat",
35
+ backgroundPosition: "center",
36
+ backgroundSize: "contain",
37
+ imageRendering: "pixelated" as CSSProperties["imageRendering"],
38
+ };
39
+
40
+ return (
41
+ <a
42
+ href={href}
43
+ className={resolvedClassName}
44
+ target={target}
45
+ rel={rel}
46
+ aria-label={label}
47
+ >
48
+ <span
49
+ aria-hidden
50
+ className="block h-[29px] w-[35px] bg-contain bg-center bg-no-repeat [image-rendering:pixelated]"
51
+ style={{
52
+ ...backgroundImageBase,
53
+ backgroundImage: `url('${assetPrefix}/landing/tomato-mark.svg')`,
54
+ }}
55
+ />
56
+ {shouldShowUnionMark ? (
57
+ <span
58
+ aria-hidden
59
+ className={`block h-[18px] w-[88px] bg-contain bg-left bg-no-repeat ${inverted ? "invert" : ""}`}
60
+ style={{
61
+ ...backgroundImageBase,
62
+ backgroundImage: `url('${assetPrefix}/landing/tomako-union-mark.svg')`,
63
+ }}
64
+ />
65
+ ) : null}
66
+ </a>
67
+ );
68
+ }
69
+
70
+ export default TomatoLogo;
71
+
@@ -3,18 +3,8 @@ import Image from "next/image";
3
3
  import type { ReactNode } from "react";
4
4
 
5
5
  import { relatedToolSlugsByTool } from "../../../constants/related-tools";
6
- import {
7
- RelatedTools,
8
- SiteFooter,
9
- SiteNavigation,
10
- getAuthClientMessages,
11
- siteConfig,
12
- type SiteNavigationCopy,
13
- } from "../../../host/server";
14
- import { bcp47ByLocale } from "../../../i18n/locale-fallback";
6
+ import { RelatedTools } from "../../../host/server";
15
7
  import type { AppLocale } from "../../../i18n/locale";
16
- import { localizedPath } from "../../../lib/routing/locale-paths";
17
- import { serializeJsonLd } from "../../../lib/seo-meta";
18
8
  import { resolveToolLocaleCopy } from "../../../lib/tools/resolve-tool-locale-copy";
19
9
 
20
10
  import type { ToolSpec } from "../types";
@@ -62,136 +52,20 @@ export async function ToolPageShell({
62
52
  handoffSlot?: ReactNode;
63
53
  children: React.ReactNode;
64
54
  }) {
55
+ // ToolMarketingShell 在 SEO route 层输出 JSON-LD / 导航 / 页脚;
56
+ // runtime 仅保留 hero / 内容 / related tools 的布局。
57
+ void faqItems;
58
+ void handoffSlot;
59
+
65
60
  const copy = resolveToolLocaleCopy(spec, locale);
66
61
  const relatedToolSlugs = relatedToolSlugsByTool[spec.slug];
67
62
  const displayTitle = copy.displayTitle ?? copy.title;
68
63
  // Full messages: public getRequestConfig omits toolsPages; client tools layout alone is not enough for RSC.
69
64
  const messages = await loadToolMessages(locale);
70
65
  const t = createToolMessageTranslator(locale, messages, "toolsPages.shared");
71
- const navT = createToolMessageTranslator(locale, messages, "page.landing.hero");
72
- const navigationCopy: SiteNavigationCopy = {
73
- languageLabel: navT("languageLabel"),
74
- productCapabilities: navT.raw("atomicCapabilities") as SiteNavigationCopy["productCapabilities"],
75
- nav: {
76
- tools: navT("nav.tools"),
77
- viewAllTools: navT("nav.viewAllTools"),
78
- pricing: navT("nav.pricing"),
79
- blog: navT("nav.blog"),
80
- skills: navT("nav.skills"),
81
- },
82
- };
83
- const authMessages = await getAuthClientMessages(locale);
84
- const toolUrl = new URL(
85
- localizedPath(locale, `tools/${spec.slug}`),
86
- siteConfig.url,
87
- ).href;
88
- const toolJsonLd = {
89
- "@context": "https://schema.org",
90
- "@type": "WebApplication",
91
- name: copy.title,
92
- description: copy.description,
93
- url: toolUrl,
94
- applicationCategory: "BusinessApplication",
95
- operatingSystem: "Web",
96
- inLanguage: bcp47ByLocale[locale],
97
- isAccessibleForFree: true,
98
- offers: {
99
- "@type": "Offer",
100
- price: "0",
101
- priceCurrency: "USD",
102
- },
103
- };
104
-
105
- const breadcrumbJsonLd = {
106
- "@context": "https://schema.org",
107
- "@type": "BreadcrumbList",
108
- itemListElement: [
109
- {
110
- "@type": "ListItem",
111
- position: 1,
112
- name: siteConfig.name,
113
- item: new URL(localizedPath(locale), siteConfig.url).href,
114
- },
115
- {
116
- "@type": "ListItem",
117
- position: 2,
118
- name: t("breadcrumbTools"),
119
- item: new URL(localizedPath(locale, "tools"), siteConfig.url).href,
120
- },
121
- {
122
- "@type": "ListItem",
123
- position: 3,
124
- name: copy.title,
125
- item: toolUrl,
126
- },
127
- ],
128
- };
129
-
130
- // FAQ 来源优先级:显式 faqItems(package 工具)→ toolsPages.{camelSlug}.guide.faqItems;都缺失时不输出 FAQPage
131
- let resolvedFaqItems: Array<{ question: string; answer: string }> =
132
- faqItems ?? [];
133
- if (resolvedFaqItems.length === 0) {
134
- const camelSlug = spec.slug.replace(/-([a-z0-9])/g, (_, char: string) =>
135
- char.toUpperCase(),
136
- );
137
- try {
138
- const guideT = createToolMessageTranslator(
139
- locale,
140
- messages,
141
- `toolsPages.${camelSlug}.guide`,
142
- );
143
- const rawFaqItems = guideT.raw("faqItems") as unknown;
144
- resolvedFaqItems = Array.isArray(rawFaqItems)
145
- ? rawFaqItems.filter(
146
- (item): item is { question: string; answer: string } =>
147
- typeof item?.question === "string" &&
148
- typeof item?.answer === "string",
149
- )
150
- : [];
151
- } catch {
152
- resolvedFaqItems = [];
153
- }
154
- }
155
- const faqJsonLd: Record<string, unknown> | null =
156
- resolvedFaqItems.length > 0
157
- ? {
158
- "@context": "https://schema.org",
159
- "@type": "FAQPage",
160
- mainEntity: resolvedFaqItems.map((item) => ({
161
- "@type": "Question",
162
- name: item.question,
163
- acceptedAnswer: {
164
- "@type": "Answer",
165
- text: item.answer,
166
- },
167
- })),
168
- }
169
- : null;
170
66
 
171
67
  return (
172
68
  <div className="min-h-screen overflow-x-clip bg-white text-[#111111]">
173
- <script
174
- type="application/ld+json"
175
- dangerouslySetInnerHTML={{ __html: serializeJsonLd(toolJsonLd) }}
176
- />
177
- <script
178
- type="application/ld+json"
179
- dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
180
- />
181
- {faqJsonLd ? (
182
- <script
183
- type="application/ld+json"
184
- dangerouslySetInnerHTML={{ __html: serializeJsonLd(faqJsonLd) }}
185
- />
186
- ) : null}
187
- <SiteNavigation
188
- locale={locale}
189
- copy={navigationCopy}
190
- authMessages={authMessages}
191
- surface="solid"
192
- className="sticky top-0 z-50 border-t-0"
193
- />
194
- {handoffSlot}
195
69
  <main className="overflow-x-clip">
196
70
  {heroVariant === "immersive" ? (
197
71
  <>
@@ -342,7 +216,6 @@ export async function ToolPageShell({
342
216
  </div>
343
217
  ) : null}
344
218
  </main>
345
- <SiteFooter locale={locale} />
346
219
  </div>
347
220
  );
348
221
  }
@@ -2,16 +2,12 @@ import {
2
2
  ArrowRight,
3
3
  CopyCheck,
4
4
  } from "lucide-react";
5
- import { TomatoLogo } from "@tomako/public-chrome";
5
+ import { TomatoLogo } from "../../../components/tomato-logo";
6
6
  import { createToolGuideTranslator } from "../shared/tool-message-translator";
7
7
  import type { CSSProperties } from "react";
8
- import { siteConfig } from "../../../host/server";
9
8
  import { isZhRouteLocale } from "../../../i18n/locale-fallback";
10
9
  import type { AppLocale } from "../../../i18n/locale";
11
10
  import { buildAppHomeUrl } from "../../../lib/app-origin";
12
- import { localizedPath } from "../../../lib/routing/locale-paths";
13
- import { serializeJsonLd } from "../../../lib/seo-meta";
14
- import { resolveToolLocaleCopy } from "../../../lib/tools/resolve-tool-locale-copy";
15
11
  import { TarotReadingWidget } from "./widget";
16
12
  import { ToolCta } from "../shared/tool-cta";
17
13
  import {
@@ -288,33 +284,16 @@ function TimelineRows({ items }: { items: readonly TextCard[] }) {
288
284
 
289
285
  export async function TarotReadingContainer({ locale, spec }: ToolPageProps) {
290
286
  const t = await createToolGuideTranslator(locale, "toolsPages.tarotReading.guide");
291
- const copy = resolveToolLocaleCopy(spec, locale);
292
287
  const valueItems = t.raw("valueItems") as readonly TextCard[];
293
288
  const flowItems = t.raw("flowItems") as readonly TextCard[];
294
289
  const marketingItems = t.raw("marketingItems") as readonly TextCard[];
295
290
  const boundaryItems = t.raw("boundaryItems") as readonly TextCard[];
296
291
  const faqItems = t.raw("faqItems") as readonly FaqItem[];
297
- const toolUrl = new URL(localizedPath(locale, `tools/${spec.slug}`), siteConfig.url).href;
298
- const toolJsonLd = {
299
- "@context": "https://schema.org",
300
- "@type": "WebApplication",
301
- name: copy.title,
302
- description: copy.description,
303
- url: toolUrl,
304
- applicationCategory: "LifestyleApplication",
305
- operatingSystem: "Web",
306
- inLanguage: locale,
307
- isAccessibleForFree: true,
308
- };
309
292
 
310
293
  return (
311
294
  <main className="min-h-screen bg-[#F7F6F2] text-[#111111]">
312
295
  {/* eslint-disable-next-line @next/next/no-css-tags -- keep tarot-only styles off every /tools/[slug] page. */}
313
296
  <link rel="stylesheet" href="/tools/tarot-reading.css" />
314
- <script
315
- type="application/ld+json"
316
- dangerouslySetInnerHTML={{ __html: serializeJsonLd(toolJsonLd) }}
317
- />
318
297
  <section className="relative min-h-svh overflow-hidden bg-black text-[#F7F0DE]">
319
298
  <TarotAtmosphere />
320
299
  <HeroChrome locale={locale} />