From 91cbe68583c4f1e048118ffa01fdfe704e80762a Mon Sep 17 00:00:00 2001 From: hiqiancheng <51521689+hiqiancheng@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:28:26 +0800 Subject: [PATCH] refactor(site): modularize homepage demo runtime --- apps/site/src/components/ComponentDemo.astro | 363 +----------------- .../src/components/homepage-demo/README.md | 10 + .../src/components/homepage-demo/host-css.ts | 350 +++++++++++++++++ .../homepage-demo/scenario-types.ts | 3 + apps/site/src/data/homepage-demo-scenarios.ts | 96 +++++ apps/site/src/pages/index.astro | 144 ++----- .../src/scripts/component-demo-runtime.ts | 245 +----------- apps/site/src/scripts/homepage-demo-locale.ts | 89 +++++ apps/site/src/utils/homepage-demo-src.ts | 20 + 9 files changed, 638 insertions(+), 682 deletions(-) create mode 100644 apps/site/src/components/homepage-demo/README.md create mode 100644 apps/site/src/components/homepage-demo/host-css.ts create mode 100644 apps/site/src/components/homepage-demo/scenario-types.ts create mode 100644 apps/site/src/data/homepage-demo-scenarios.ts create mode 100644 apps/site/src/scripts/homepage-demo-locale.ts create mode 100644 apps/site/src/utils/homepage-demo-src.ts diff --git a/apps/site/src/components/ComponentDemo.astro b/apps/site/src/components/ComponentDemo.astro index b7d71e01..987dc573 100644 --- a/apps/site/src/components/ComponentDemo.astro +++ b/apps/site/src/components/ComponentDemo.astro @@ -1,15 +1,22 @@ --- import fs from 'node:fs'; import path from 'node:path'; +import { + createHomepageDemoHostCss, + createHomepageDemoInstanceSelector, + scopeHomepageDemoCss, +} from './homepage-demo/host-css'; +import type { HomepageDemoScenarioId } from './homepage-demo/scenario-types'; interface Props { className: string; src: string; title: string; + scenarioId?: HomepageDemoScenarioId; loading?: 'lazy' | 'eager'; } -const { className, src, title, loading = 'eager' } = Astro.props; +const { className, src, title, scenarioId = '', loading = 'eager' } = Astro.props; const toPublicPath = (value: string, basePathname: string) => { if (!value || value.startsWith('data:') || value.startsWith('#')) return value; @@ -34,127 +41,6 @@ const extractBody = (html: string) => { }; }; -const normalizeSelector = (selector: string, instanceSelector: string) => { - const trimmed = selector.trim(); - if (!trimmed) return ''; - if (trimmed.startsWith('@')) return trimmed; - if (trimmed.startsWith('from') || trimmed.startsWith('to') || /^\d+%$/.test(trimmed)) - return trimmed; - - const scoped = trimmed - .replace(/:root/g, instanceSelector) - .replace(/\bhtml\s*,\s*body\b/g, instanceSelector) - .replace(/\bbody((?:\.[\w-]+)+)/g, (_match, classes) => `${instanceSelector}${classes}`) - .replace(/\bbody\b/g, instanceSelector) - .replace(/\bhtml\b/g, instanceSelector); - - if (scoped.startsWith(instanceSelector)) { - return scoped; - } - - return `${instanceSelector} ${scoped}`; -}; - -const scopeSelectorList = (selectorText: string, instanceSelector: string) => - selectorText - .split(',') - .map((selector) => normalizeSelector(selector, instanceSelector)) - .filter(Boolean) - .join(', '); - -const scopeCss = (css: string, instanceSelector: string) => { - let index = 0; - - const consumeBlock = (): string => { - let output = ''; - let selectorBuffer = ''; - - while (index < css.length) { - const char = css[index]; - - if (char === '"' || char === "'") { - const quote = char; - selectorBuffer += char; - index += 1; - while (index < css.length) { - const innerChar = css[index]; - selectorBuffer += innerChar; - index += 1; - if (innerChar === '\\') { - if (index < css.length) { - selectorBuffer += css[index]; - index += 1; - } - continue; - } - if (innerChar === quote) break; - } - continue; - } - - if (char === '/' && css[index + 1] === '*') { - const end = css.indexOf('*/', index + 2); - const comment = end === -1 ? css.slice(index) : css.slice(index, end + 2); - selectorBuffer += comment; - index = end === -1 ? css.length : end + 2; - continue; - } - - if (char === '{') { - const selector = selectorBuffer.trim(); - selectorBuffer = ''; - index += 1; - - if ( - selector.startsWith('@keyframes') || - selector.startsWith('@-webkit-keyframes') - ) { - const keyframeBodyStart = index; - let depth = 1; - while (index < css.length && depth > 0) { - if (css[index] === '{') depth += 1; - if (css[index] === '}') depth -= 1; - index += 1; - } - output += `${selector}{${css.slice(keyframeBodyStart, index - 1)}}`; - continue; - } - - if (selector.startsWith('@')) { - const inner = consumeBlock(); - output += `${selector}{${inner}}`; - continue; - } - - const bodyStart = index; - let depth = 1; - while (index < css.length && depth > 0) { - if (css[index] === '{') depth += 1; - if (css[index] === '}') depth -= 1; - index += 1; - } - const body = css.slice(bodyStart, index - 1); - output += `${scopeSelectorList(selector, instanceSelector)}{${body}}`; - continue; - } - - if (char === '}') { - index += 1; - output += selectorBuffer; - return output; - } - - selectorBuffer += char; - index += 1; - } - - output += selectorBuffer; - return output; - }; - - return consumeBlock(); -}; - const makeId = (value: string) => { let hash = 0; for (let index = 0; index < value.length; index += 1) { @@ -189,234 +75,10 @@ const resolvedBodyHtmlWithoutScripts = resolvedBodyHtml.replace( '' ); const instanceId = makeId(`${className}:${src}`); -const instanceSelector = `touchai-component-demo[data-demo-id="${instanceId}"]`; -const hostCss = ` -${instanceSelector} { - display: block; - width: 100%; - height: 100%; - overflow: hidden; - border-radius: inherit; - isolation: isolate; - background: var(--page-bg, transparent); - color-scheme: light; -} - -${instanceSelector}.component-frame { - width: min(60vw, 680px, 100%) !important; - max-width: min(680px, 100%); - justify-self: center; - overflow: visible !important; -} - -${instanceSelector}.component-frame .stage { - width: 100% !important; - max-width: 100% !important; -} - -${instanceSelector}.feature-component-frame, -${instanceSelector}.feature-work-frame, -${instanceSelector}.feature-reminder-frame { - display: flex; - align-items: stretch; - justify-content: center; - width: 760px !important; - height: 657px !important; - min-height: 657px !important; - max-height: 657px !important; - overflow: visible !important; - background: transparent !important; -} - -${instanceSelector}.feature-component-frame .stage, -${instanceSelector}.feature-work-frame .stage, -${instanceSelector}.feature-reminder-frame .stage { - width: 100% !important; - min-height: 100% !important; - height: 100% !important; - padding: 0 !important; - justify-content: flex-start !important; - align-items: center !important; -} - -${instanceSelector}.component-frame .chat-panel, -${instanceSelector}.feature-component-frame .chat-panel, -${instanceSelector}.feature-work-frame .chat-panel, -${instanceSelector}.feature-reminder-frame .chat-panel { - margin: 0 auto !important; - box-shadow: - 0 34px 90px rgba(107, 114, 128, 0.22), - 0 12px 34px rgba(107, 114, 128, 0.12), - 0 0 48px rgba(107, 114, 128, 0.14) !important; -} - -${instanceSelector}.is-scroll-driven .chat-panel { - will-change: auto !important; -} - -${instanceSelector}.component-frame .chat-panel { - width: 100% !important; - max-width: 100% !important; -} - -${instanceSelector}.component-frame.is-idle .chat-panel { - border-radius: 8px !important; - overflow: hidden !important; - background: var(--panel, #fff) !important; -} - -${instanceSelector}.component-frame .conversation-content { - flex: 1 1 auto !important; - min-height: 0 !important; - overflow-y: auto !important; - padding-bottom: 18px !important; - overscroll-behavior: contain !important; - scrollbar-width: none !important; - -webkit-overflow-scrolling: touch !important; -} - -${instanceSelector}.component-frame .conversation-content, -${instanceSelector}.feature-component-frame .conversation-content, -${instanceSelector}.feature-work-frame .conversation-content, -${instanceSelector}.feature-reminder-frame .conversation-content { - min-height: 0 !important; - overflow-y: auto !important; - overscroll-behavior: contain !important; - scrollbar-width: none !important; - -webkit-overflow-scrolling: touch !important; -} - -${instanceSelector}.component-frame .conversation-content::-webkit-scrollbar, -${instanceSelector}.feature-component-frame .conversation-content::-webkit-scrollbar, -${instanceSelector}.feature-work-frame .conversation-content::-webkit-scrollbar, -${instanceSelector}.feature-reminder-frame .conversation-content::-webkit-scrollbar { - display: none; -} - -${instanceSelector}.component-frame.is-answering .chat-panel, -${instanceSelector}.component-frame.is-complete .chat-panel, -${instanceSelector}.feature-component-frame.is-answering .chat-panel, -${instanceSelector}.feature-component-frame.is-complete .chat-panel, -${instanceSelector}.feature-work-frame.is-answering .chat-panel, -${instanceSelector}.feature-work-frame.is-complete .chat-panel, -${instanceSelector}.feature-reminder-frame.is-answering .chat-panel, -${instanceSelector}.feature-reminder-frame.is-complete .chat-panel { - display: flex !important; - flex-direction: column !important; - max-width: 100% !important; -} - -${instanceSelector}.feature-component-frame.is-answering .stage, -${instanceSelector}.feature-component-frame.is-complete .stage, -${instanceSelector}.feature-work-frame.is-answering .stage, -${instanceSelector}.feature-work-frame.is-complete .stage, -${instanceSelector}.feature-reminder-frame.is-answering .stage, -${instanceSelector}.feature-reminder-frame.is-complete .stage { - justify-content: flex-start !important; -} - -${instanceSelector}.feature-component-frame.is-complete .chat-panel, -${instanceSelector}.feature-work-frame.is-complete .chat-panel, -${instanceSelector}.feature-reminder-frame.is-complete .chat-panel, -${instanceSelector}.feature-component-frame.is-scroll-driven.is-complete .chat-panel, -${instanceSelector}.feature-work-frame.is-scroll-driven.is-complete .chat-panel, -${instanceSelector}.feature-reminder-frame.is-scroll-driven.is-complete .chat-panel { - min-height: 0 !important; - height: auto !important; -} - -${instanceSelector}.component-frame.is-answering .conversation-content, -${instanceSelector}.component-frame.is-complete .conversation-content, -${instanceSelector}.feature-component-frame.is-answering .conversation-content, -${instanceSelector}.feature-component-frame.is-complete .conversation-content, -${instanceSelector}.feature-work-frame.is-answering .conversation-content, -${instanceSelector}.feature-work-frame.is-complete .conversation-content, -${instanceSelector}.feature-reminder-frame.is-answering .conversation-content, -${instanceSelector}.feature-reminder-frame.is-complete .conversation-content { - flex: 1 1 auto !important; - min-height: 0 !important; - overflow-y: auto !important; - padding-bottom: 20px !important; - overscroll-behavior: contain !important; - -webkit-overflow-scrolling: touch !important; -} - -${instanceSelector}.component-frame.is-answering .composer, -${instanceSelector}.component-frame.is-complete .composer, -${instanceSelector}.feature-component-frame.is-answering .composer, -${instanceSelector}.feature-component-frame.is-complete .composer, -${instanceSelector}.feature-work-frame.is-answering .composer, -${instanceSelector}.feature-work-frame.is-complete .composer, -${instanceSelector}.feature-reminder-frame.is-answering .composer, -${instanceSelector}.feature-reminder-frame.is-complete .composer { - margin-top: auto !important; -} - -@media (max-width: 900px) { - ${instanceSelector}.component-frame { - width: min(60vw, 520px, 100%) !important; - height: auto !important; - min-height: 0 !important; - aspect-ratio: 760 / 657 !important; - max-height: min(62vh, 657px) !important; - } - - ${instanceSelector}.component-frame .stage { - height: 100% !important; - min-height: 0 !important; - } -} - -@media (max-height: 780px) { - ${instanceSelector}.component-frame { - width: min(52vw, 560px, 100%) !important; - max-width: min(560px, 100%) !important; - height: auto !important; - min-height: 0 !important; - aspect-ratio: 760 / 657 !important; - max-height: min(60vh, 520px) !important; - overflow: hidden !important; - } - - ${instanceSelector}.component-frame .stage { - min-height: 0 !important; - height: 100% !important; - padding: 0 !important; - align-items: center !important; - justify-content: center !important; - } - - ${instanceSelector}.component-frame.is-answering .chat-panel, - ${instanceSelector}.component-frame.is-complete .chat-panel, - ${instanceSelector}.component-frame.is-scroll-driven.is-answering .chat-panel, - ${instanceSelector}.component-frame.is-scroll-driven.is-complete .chat-panel { - display: flex !important; - flex-direction: column !important; - max-width: 100% !important; - } -} - -@media (max-width: 560px) { - ${instanceSelector}.component-frame { - width: min(calc(100vw - 56px), 360px, 100%) !important; - max-width: min(calc(100vw - 56px), 360px, 100%) !important; - height: auto !important; - min-height: 0 !important; - aspect-ratio: 760 / 657 !important; - max-height: min(58vh, 420px) !important; - overflow: visible !important; - } - - ${instanceSelector}.component-frame .stage { - min-height: 0 !important; - height: 100% !important; - padding: 0 !important; - align-items: center !important; - justify-content: center !important; - } -} -`; -const initialCss = `${scopeCss(inlineStyles, instanceSelector)}\n${hostCss}`; +const instanceSelector = createHomepageDemoInstanceSelector(instanceId); +const initialCss = `${scopeHomepageDemoCss(inlineStyles, instanceSelector)}\n${createHomepageDemoHostCss( + instanceSelector +)}`; const initialClassName = [className, body.className].filter(Boolean).join(' '); --- @@ -426,6 +88,7 @@ const initialClassName = [className, body.className].filter(Boolean).join(' '); class={initialClassName} data-component-demo data-demo-id={instanceId} + data-scenario-id={scenarioId || undefined} data-src={src} data-title={title} data-loading={loading} diff --git a/apps/site/src/components/homepage-demo/README.md b/apps/site/src/components/homepage-demo/README.md new file mode 100644 index 00000000..44d44b6b --- /dev/null +++ b/apps/site/src/components/homepage-demo/README.md @@ -0,0 +1,10 @@ +# Homepage Demo Architecture + +The homepage demo layer now has an explicit module boundary: + +- `src/data/homepage-demo-scenarios.ts` is the typed scenario registry for locale-specific demo sources and labels. +- `src/components/homepage-demo/host-css.ts` is the shared host/scope CSS source of truth used by both the Astro host and the browser runtime. +- `src/components/ComponentDemo.astro` is the compatibility host that still loads legacy standalone HTML demo documents. +- `src/scripts/component-demo-runtime.ts` mounts, reloads, and message-bridges those legacy demo documents in the browser. + +Those standalone HTML files under `public/*/touchai-components.html` are now treated as legacy migration sources. Future refactors should migrate one scenario at a time into reusable render/runtime modules while keeping this host boundary stable. diff --git a/apps/site/src/components/homepage-demo/host-css.ts b/apps/site/src/components/homepage-demo/host-css.ts new file mode 100644 index 00000000..539ecb26 --- /dev/null +++ b/apps/site/src/components/homepage-demo/host-css.ts @@ -0,0 +1,350 @@ +export const createHomepageDemoInstanceSelector = (id: string) => + `touchai-component-demo[data-demo-id="${id}"]`; + +const normalizeHomepageDemoSelector = (selector: string, instanceSelector: string) => { + const trimmed = selector.trim(); + if (!trimmed) return ''; + if (trimmed.startsWith('@')) return trimmed; + if (trimmed.startsWith('from') || trimmed.startsWith('to') || /^\d+%$/.test(trimmed)) + return trimmed; + + const scoped = trimmed + .replace(/:root/g, instanceSelector) + .replace(/\bhtml\s*,\s*body\b/g, instanceSelector) + .replace(/\bbody((?:\.[\w-]+)+)/g, (_match, classes) => `${instanceSelector}${classes}`) + .replace(/\bbody\b/g, instanceSelector) + .replace(/\bhtml\b/g, instanceSelector); + + if (scoped.startsWith(instanceSelector)) { + return scoped; + } + + return `${instanceSelector} ${scoped}`; +}; + +const scopeHomepageDemoSelectorList = (selectorText: string, instanceSelector: string) => + selectorText + .split(',') + .map((selector) => normalizeHomepageDemoSelector(selector, instanceSelector)) + .filter(Boolean) + .join(', '); + +export const scopeHomepageDemoCss = (css: string, instanceSelector: string) => { + let index = 0; + + const consumeBlock = (): string => { + let output = ''; + let selectorBuffer = ''; + + while (index < css.length) { + const char = css[index]; + + if (char === '"' || char === "'") { + const quote = char; + selectorBuffer += char; + index += 1; + while (index < css.length) { + const innerChar = css[index]; + selectorBuffer += innerChar; + index += 1; + if (innerChar === '\\') { + if (index < css.length) { + selectorBuffer += css[index]; + index += 1; + } + continue; + } + if (innerChar === quote) break; + } + continue; + } + + if (char === '/' && css[index + 1] === '*') { + const end = css.indexOf('*/', index + 2); + const comment = end === -1 ? css.slice(index) : css.slice(index, end + 2); + selectorBuffer += comment; + index = end === -1 ? css.length : end + 2; + continue; + } + + if (char === '{') { + const selector = selectorBuffer.trim(); + selectorBuffer = ''; + index += 1; + + if ( + selector.startsWith('@keyframes') || + selector.startsWith('@-webkit-keyframes') + ) { + const keyframeBodyStart = index; + let depth = 1; + while (index < css.length && depth > 0) { + if (css[index] === '{') depth += 1; + if (css[index] === '}') depth -= 1; + index += 1; + } + output += `${selector}{${css.slice(keyframeBodyStart, index - 1)}}`; + continue; + } + + if (selector.startsWith('@')) { + const inner = consumeBlock(); + output += `${selector}{${inner}}`; + continue; + } + + const bodyStart = index; + let depth = 1; + while (index < css.length && depth > 0) { + if (css[index] === '{') depth += 1; + if (css[index] === '}') depth -= 1; + index += 1; + } + const body = css.slice(bodyStart, index - 1); + output += `${scopeHomepageDemoSelectorList(selector, instanceSelector)}{${body}}`; + continue; + } + + if (char === '}') { + index += 1; + output += selectorBuffer; + return output; + } + + selectorBuffer += char; + index += 1; + } + + output += selectorBuffer; + return output; + }; + + return consumeBlock(); +}; + +export const createHomepageDemoHostCss = (instanceSelector: string) => ` +${instanceSelector} { + display: block; + width: 100%; + height: 100%; + overflow: hidden; + border-radius: inherit; + isolation: isolate; + background: var(--page-bg, transparent); + color-scheme: light; +} + +${instanceSelector}.component-frame { + width: min(60vw, 680px, 100%) !important; + max-width: min(680px, 100%); + justify-self: center; + overflow: visible !important; +} + +${instanceSelector}.component-frame .stage { + width: 100% !important; + max-width: 100% !important; +} + +${instanceSelector}.feature-component-frame, +${instanceSelector}.feature-work-frame, +${instanceSelector}.feature-reminder-frame { + display: flex; + align-items: stretch; + justify-content: center; + width: 760px !important; + height: 657px !important; + min-height: 657px !important; + max-height: 657px !important; + overflow: visible !important; + background: transparent !important; +} + +${instanceSelector}.feature-component-frame .stage, +${instanceSelector}.feature-work-frame .stage, +${instanceSelector}.feature-reminder-frame .stage { + width: 100% !important; + min-height: 100% !important; + height: 100% !important; + padding: 0 !important; + justify-content: flex-start !important; + align-items: center !important; +} + +${instanceSelector}.component-frame .chat-panel, +${instanceSelector}.feature-component-frame .chat-panel, +${instanceSelector}.feature-work-frame .chat-panel, +${instanceSelector}.feature-reminder-frame .chat-panel { + margin: 0 auto !important; + box-shadow: + 0 34px 90px rgba(107, 114, 128, 0.22), + 0 12px 34px rgba(107, 114, 128, 0.12), + 0 0 48px rgba(107, 114, 128, 0.14) !important; +} + +${instanceSelector}.is-scroll-driven .chat-panel { + will-change: auto !important; +} + +${instanceSelector}.component-frame .chat-panel { + width: 100% !important; + max-width: 100% !important; +} + +${instanceSelector}.component-frame.is-idle .chat-panel { + border-radius: 8px !important; + overflow: hidden !important; + background: var(--panel, #fff) !important; +} + +${instanceSelector}.component-frame .conversation-content { + flex: 1 1 auto !important; + min-height: 0 !important; + overflow-y: auto !important; + padding-bottom: 18px !important; + overscroll-behavior: contain !important; + scrollbar-width: none !important; + -webkit-overflow-scrolling: touch !important; +} + +${instanceSelector}.component-frame .conversation-content, +${instanceSelector}.feature-component-frame .conversation-content, +${instanceSelector}.feature-work-frame .conversation-content, +${instanceSelector}.feature-reminder-frame .conversation-content { + min-height: 0 !important; + overflow-y: auto !important; + overscroll-behavior: contain !important; + scrollbar-width: none !important; + -webkit-overflow-scrolling: touch !important; +} + +${instanceSelector}.component-frame .conversation-content::-webkit-scrollbar, +${instanceSelector}.feature-component-frame .conversation-content::-webkit-scrollbar, +${instanceSelector}.feature-work-frame .conversation-content::-webkit-scrollbar, +${instanceSelector}.feature-reminder-frame .conversation-content::-webkit-scrollbar { + display: none; +} + +${instanceSelector}.component-frame.is-answering .chat-panel, +${instanceSelector}.component-frame.is-complete .chat-panel, +${instanceSelector}.feature-component-frame.is-answering .chat-panel, +${instanceSelector}.feature-component-frame.is-complete .chat-panel, +${instanceSelector}.feature-work-frame.is-answering .chat-panel, +${instanceSelector}.feature-work-frame.is-complete .chat-panel, +${instanceSelector}.feature-reminder-frame.is-answering .chat-panel, +${instanceSelector}.feature-reminder-frame.is-complete .chat-panel { + display: flex !important; + flex-direction: column !important; + max-width: 100% !important; +} + +${instanceSelector}.feature-component-frame.is-answering .stage, +${instanceSelector}.feature-component-frame.is-complete .stage, +${instanceSelector}.feature-work-frame.is-answering .stage, +${instanceSelector}.feature-work-frame.is-complete .stage, +${instanceSelector}.feature-reminder-frame.is-answering .stage, +${instanceSelector}.feature-reminder-frame.is-complete .stage { + justify-content: flex-start !important; +} + +${instanceSelector}.feature-component-frame.is-complete .chat-panel, +${instanceSelector}.feature-work-frame.is-complete .chat-panel, +${instanceSelector}.feature-reminder-frame.is-complete .chat-panel, +${instanceSelector}.feature-component-frame.is-scroll-driven.is-complete .chat-panel, +${instanceSelector}.feature-work-frame.is-scroll-driven.is-complete .chat-panel, +${instanceSelector}.feature-reminder-frame.is-scroll-driven.is-complete .chat-panel { + min-height: 0 !important; + height: auto !important; +} + +${instanceSelector}.component-frame.is-answering .conversation-content, +${instanceSelector}.component-frame.is-complete .conversation-content, +${instanceSelector}.feature-component-frame.is-answering .conversation-content, +${instanceSelector}.feature-component-frame.is-complete .conversation-content, +${instanceSelector}.feature-work-frame.is-answering .conversation-content, +${instanceSelector}.feature-work-frame.is-complete .conversation-content, +${instanceSelector}.feature-reminder-frame.is-answering .conversation-content, +${instanceSelector}.feature-reminder-frame.is-complete .conversation-content { + flex: 1 1 auto !important; + min-height: 0 !important; + overflow-y: auto !important; + padding-bottom: 20px !important; + overscroll-behavior: contain !important; + -webkit-overflow-scrolling: touch !important; +} + +${instanceSelector}.component-frame.is-answering .composer, +${instanceSelector}.component-frame.is-complete .composer, +${instanceSelector}.feature-component-frame.is-answering .composer, +${instanceSelector}.feature-component-frame.is-complete .composer, +${instanceSelector}.feature-work-frame.is-answering .composer, +${instanceSelector}.feature-work-frame.is-complete .composer, +${instanceSelector}.feature-reminder-frame.is-answering .composer, +${instanceSelector}.feature-reminder-frame.is-complete .composer { + margin-top: auto !important; +} + +@media (max-width: 900px) { + ${instanceSelector}.component-frame { + width: min(60vw, 520px, 100%) !important; + height: auto !important; + min-height: 0 !important; + aspect-ratio: 760 / 657 !important; + max-height: min(62vh, 657px) !important; + } + + ${instanceSelector}.component-frame .stage { + height: 100% !important; + min-height: 0 !important; + } +} + +@media (max-height: 780px) { + ${instanceSelector}.component-frame { + width: min(52vw, 560px, 100%) !important; + max-width: min(560px, 100%) !important; + height: auto !important; + min-height: 0 !important; + aspect-ratio: 760 / 657 !important; + max-height: min(60vh, 520px) !important; + overflow: hidden !important; + } + + ${instanceSelector}.component-frame .stage { + min-height: 0 !important; + height: 100% !important; + padding: 0 !important; + align-items: center !important; + justify-content: center !important; + } + + ${instanceSelector}.component-frame.is-answering .chat-panel, + ${instanceSelector}.component-frame.is-complete .chat-panel, + ${instanceSelector}.component-frame.is-scroll-driven.is-answering .chat-panel, + ${instanceSelector}.component-frame.is-scroll-driven.is-complete .chat-panel { + display: flex !important; + flex-direction: column !important; + max-width: 100% !important; + } +} + +@media (max-width: 560px) { + ${instanceSelector}.component-frame { + width: min(calc(100vw - 56px), 360px, 100%) !important; + max-width: min(calc(100vw - 56px), 360px, 100%) !important; + height: auto !important; + min-height: 0 !important; + aspect-ratio: 760 / 657 !important; + max-height: min(58vh, 420px) !important; + overflow: visible !important; + } + + ${instanceSelector}.component-frame .stage { + min-height: 0 !important; + height: 100% !important; + padding: 0 !important; + align-items: center !important; + justify-content: center !important; + } +} +`; diff --git a/apps/site/src/components/homepage-demo/scenario-types.ts b/apps/site/src/components/homepage-demo/scenario-types.ts new file mode 100644 index 00000000..eb68ae0c --- /dev/null +++ b/apps/site/src/components/homepage-demo/scenario-types.ts @@ -0,0 +1,3 @@ +export type HomepageDemoScenarioId = 'intro' | 'solver' | 'work-organizer' | 'reminder'; + +export type HomepageDemoReplayScenarioId = Exclude; diff --git a/apps/site/src/data/homepage-demo-scenarios.ts b/apps/site/src/data/homepage-demo-scenarios.ts new file mode 100644 index 00000000..355ce21e --- /dev/null +++ b/apps/site/src/data/homepage-demo-scenarios.ts @@ -0,0 +1,96 @@ +import type { HomepageDemoScenarioId } from '../components/homepage-demo/scenario-types'; + +export type HomepageDemoLocale = 'zh' | 'en'; + +export interface HomepageDemoVariant { + src: string; + title: string; +} + +export interface HomepageDemoScenario { + id: HomepageDemoScenarioId; + frameClassName: string; + replayCardSelector?: string; + replayLabels?: Record; + restoreProgressOnReload?: boolean; + variants: Record; +} + +export const homepageDemoScenarios: Record = { + intro: { + id: 'intro', + frameClassName: 'component-frame', + variants: { + zh: { + src: '/touchai-intro/touchai-components.html?v=8', + title: 'TouchAI 介绍组件演示', + }, + en: { + src: '/touchai-intro-en/touchai-components.html?v=1', + title: 'TouchAI intro component demo', + }, + }, + }, + solver: { + id: 'solver', + frameClassName: 'feature-component-frame', + replayCardSelector: '.feature-component-card', + replayLabels: { + zh: '解题对话框', + en: 'Math solver conversation', + }, + restoreProgressOnReload: true, + variants: { + zh: { + src: '/feature-solver/touchai-components.html?v=17', + title: 'TouchAI 解题组件演示', + }, + en: { + src: '/feature-solver-en/touchai-components.html?v=1', + title: 'TouchAI math solver component demo', + }, + }, + }, + 'work-organizer': { + id: 'work-organizer', + frameClassName: 'feature-work-frame', + replayCardSelector: '.feature-work-card', + replayLabels: { + zh: '工作整理对话框', + en: 'Work organizer conversation', + }, + restoreProgressOnReload: true, + variants: { + zh: { + src: '/feature-work-organizer/touchai-components.html?v=13', + title: 'TouchAI 工作整理组件演示', + }, + en: { + src: '/feature-work-organizer-en/touchai-components.html?v=1', + title: 'TouchAI work organizer component demo', + }, + }, + }, + reminder: { + id: 'reminder', + frameClassName: 'feature-reminder-frame', + replayCardSelector: '.feature-reminder-card', + replayLabels: { + zh: '提醒对话框', + en: 'Reminder conversation', + }, + restoreProgressOnReload: true, + variants: { + zh: { + src: '/feature-reminder/touchai-components.html?v=5', + title: 'TouchAI MCP 工具调用组件演示', + }, + en: { + src: '/feature-reminder-en/touchai-components.html?v=1', + title: 'TouchAI meeting reminder component demo', + }, + }, + }, +}; + +export const homepageDemoScenarioList = Object.values(homepageDemoScenarios); diff --git a/apps/site/src/pages/index.astro b/apps/site/src/pages/index.astro index 8de68066..b2e9a14c 100644 --- a/apps/site/src/pages/index.astro +++ b/apps/site/src/pages/index.astro @@ -1,5 +1,7 @@ --- import ComponentDemo from '../components/ComponentDemo.astro'; +import type { HomepageDemoLocale } from '../data/homepage-demo-scenarios'; +import { getHomepageDemoSrc, getHomepageDemoTitle } from '../utils/homepage-demo-src'; const CLARITY_ID = import.meta.env.PUBLIC_CLARITY_ID ?? ''; const ANALYTICS_ENDPOINT = import.meta.env.PUBLIC_ANALYTICS_ENDPOINT ?? ''; @@ -12,6 +14,7 @@ const titleZh = 'TouchAI:一触即达的桌面效率 Agent'; const titleEn = 'TouchAI: A Desktop Efficiency Agent at Your Fingertips'; const descriptionZh = '一触即达的桌面效率 Agent——随时待命,做您触手可及的 AI 入口'; const descriptionEn = 'Your desktop AI agent, always ready — your touch-accessible AI gateway'; +const initialHomepageDemoLocale: HomepageDemoLocale = 'zh'; const keywords = 'TouchAI, desktop AI agent, AI desktop assistant, productivity agent, AI 助手, 桌面 AI, 桌面效率工具, 触控 AI, 自动化工作流, 本地桌面助手'; const socialImageUrl = `${siteUrl}seo-card.png`; @@ -1941,8 +1944,9 @@ const structuredData = JSON.stringify({
@@ -1964,8 +1968,15 @@ const structuredData = JSON.stringify({
@@ -1978,8 +1989,15 @@ const structuredData = JSON.stringify({
@@ -2005,8 +2023,15 @@ const structuredData = JSON.stringify({
@@ -2285,12 +2310,12 @@ const structuredData = JSON.stringify({
© 2026 TouchAI. GPL-3.0-or-later.
-