diff --git a/.gitignore b/.gitignore index 8c630a677c..f6df3d1eb8 100644 --- a/.gitignore +++ b/.gitignore @@ -18,5 +18,10 @@ # Go workspace file go.work +# Agent workflow artifacts +docs/superpowers/ +*/docs/superpowers/ +.playwright-mcp/ + .idea/ *.wasm diff --git a/ui/README.md b/ui/README.md index 1ab2453528..0187aa6460 100644 --- a/ui/README.md +++ b/ui/README.md @@ -48,7 +48,7 @@ The same requirement should use one issue to connect the Design PR and Code PR. `ui/prototype` is a runnable product prototype, not an isolated demo. -It is used during Design PR to express product behavior, page structure, visual design, and basic interactions. See [Prototype Maintenance](skills/prototype-maintenance/SKILL.md) for detailed maintenance rules. +It is used during Design PR to express product behavior, page structure, visual design, and basic interactions. See [`prototype/AGENTS.md`](prototype/AGENTS.md) for detailed maintenance rules. ## Quick Start @@ -79,12 +79,6 @@ It is used during Design PR to express product behavior, page structure, visual | [Design to Code Mapping (Legacy)](docs/design-to-code-mapping-legacy.md) | Legacy `.pen` to `spx-gui` mapping rules | | [Component Docs Naming](docs/component-docs-naming.md) | Component documentation naming conventions | -## Skills - -| Skill | Description | -| ----- | ----------- | -| [Prototype Maintenance](skills/prototype-maintenance/SKILL.md) | Use when maintaining `ui/prototype` and keeping it aligned with the real frontend structure, routes, and interactions | - ## Tests and Validation `ui/tests/pen/` contains design asset validation tests for protecting the component library and page-level Pencil files. See [Design Asset Validation](docs/design-asset-validation.md) for details. diff --git a/ui/README.zh.md b/ui/README.zh.md index ccda809bfa..b950544cb4 100644 --- a/ui/README.zh.md +++ b/ui/README.zh.md @@ -48,7 +48,7 @@ PR(Code 发布) `ui/prototype` 是可运行的功能原型,不是孤立 demo。 -它用于在 Design PR 阶段表达产品功能、页面结构、设计样式和基础交互。具体维护规则见 [Prototype Maintenance](skills/prototype-maintenance/SKILL.md)。 +它用于在 Design PR 阶段表达产品功能、页面结构、设计样式和基础交互。具体维护规则见 [`prototype/AGENTS.md`](prototype/AGENTS.md)。 ## 快速开始 @@ -79,12 +79,6 @@ PR(Code 发布) | [设计到代码映射(旧版)](docs/design-to-code-mapping-legacy.md) | 旧版 `.pen` 到 `spx-gui` 的映射规则,历史参考 | | [组件文档命名](docs/component-docs-naming.md) | 组件文档命名约定 | -## Skills - -| Skill | 说明 | -| ----- | ---- | -| [Prototype Maintenance](skills/prototype-maintenance/SKILL.md) | 维护 `ui/prototype` 时使用,确保 prototype 与真实前端结构、路由和交互保持一致 | - ## 测试与校验 `ui/tests/pen/` 存放设计资产校验测试,用于保护组件库和页面级 Pencil 文件。详细说明见 [设计资产校验](docs/design-asset-validation.md)。 diff --git a/ui/docs/design-to-code-mapping-legacy.md b/ui/docs/design-to-code-mapping-legacy.md index 0eba928a4b..34ce9297aa 100644 --- a/ui/docs/design-to-code-mapping-legacy.md +++ b/ui/docs/design-to-code-mapping-legacy.md @@ -1,7 +1,7 @@ # Design to Code Mapping Guide (Legacy) > 本文是旧版 `.pen` 到 `spx-gui` 的映射与规范化文档,主要服务旧版设计同步流程。 -> 当前 prototype 维护规则请优先查看 [`ui/skills/prototype-maintenance/SKILL.md`](../skills/prototype-maintenance/SKILL.md)。 +> 当前 prototype 维护规则请优先查看 [`ui/prototype/AGENTS.md`](../prototype/AGENTS.md)。 本文记录整个仓库里 `.pen` 设计资产与 `spx-gui` 前端实现之间的对应关系,供后续开发者同步设计稿到代码时使用。 diff --git a/ui/docs/team-workflow.md b/ui/docs/team-workflow.md index 9f12623edc..8069807af7 100644 --- a/ui/docs/team-workflow.md +++ b/ui/docs/team-workflow.md @@ -108,11 +108,11 @@ Code PR 不应只从静态设计稿重新推导实现方案,而应把 Design P ## Prototype 维护 -Prototype 的维护规则已经单独整理为 skill: +Prototype 的维护规则已经合并到原型目录: -- [`ui/skills/prototype-maintenance/SKILL.md`](../skills/prototype-maintenance/SKILL.md) +- [`ui/prototype/AGENTS.md`](../prototype/AGENTS.md) -当任务涉及 `ui/prototype`、Pencil 页面改动同步、prototype 预览环境或 prototype 与真实前端代码对齐时,应优先使用该 skill。 +当任务涉及 `ui/prototype`、Pencil 页面改动同步、prototype 预览环境或 prototype 与真实前端代码对齐时,应优先遵循该文件。 ## 责任边界 @@ -142,5 +142,5 @@ Code PR 不应绕开 issue 和 Design PR 单独解释需求。 - Issue 编写规则:[Issue 模板](./issue-template.md) - Design PR 编写规则:[PR 模板](./pr-template.md) -- Prototype 维护规则:[`ui/skills/prototype-maintenance/SKILL.md`](../skills/prototype-maintenance/SKILL.md) +- Prototype 维护规则:[`ui/prototype/AGENTS.md`](../prototype/AGENTS.md) - 旧版协作流程:[团队工作流程(旧版)](./team-workflow-legacy.md) diff --git a/ui/images/tutorials-banner.jpg b/ui/images/tutorials-banner.jpg new file mode 100644 index 0000000000..3753884a15 Binary files /dev/null and b/ui/images/tutorials-banner.jpg differ diff --git a/ui/prototype/AGENTS.md b/ui/prototype/AGENTS.md index a77670f646..6f17bd2d5d 100644 --- a/ui/prototype/AGENTS.md +++ b/ui/prototype/AGENTS.md @@ -4,31 +4,60 @@ This file guides AI agents working under `ui/prototype/`. ## Purpose -`ui/prototype/` is a standalone, design-driven UI preview workspace, usually generated from `.pen` files in `ui/pages/` and assets in `ui/images/`. +`ui/prototype/` is a standalone, runnable XBuilder frontend prototype. It exists for Design PR work: product behavior, page structure, visual design, and basic interactions should be experienced here before production logic is implemented in `spx-gui/`. -It should stay close to the real frontend in stack and presentation style, but it is not the real frontend. +The prototype must look and behave like the current Builder frontend, but it must be disconnected from the server. Data shown in the prototype is local fake data. ## Core Contract -- `spx-gui/` is the real product frontend. -- `ui/prototype/` is a local preview sandbox. -- The prototype should resemble the real frontend in structure, theme shape, typography, and implementation style. -- The prototype must not depend on the real frontend at runtime, build time, or type-check time. +- `spx-gui/` is the real product frontend and the reference implementation. +- `ui/prototype/` is an independent frontend app with its own install, dev, build, and preview flow. +- Prototype code must follow the real frontend's stack and organization where practical: Vite, Vue 3, Vue Router, Tailwind CSS v4, page/component/API-style boundaries, local theme tokens, and local app styles. +- Runtime, build, and typecheck must not depend on `spx-gui/`. +- The prototype must not call real backend services. Stable rule: -- You may copy or adapt presentation-layer patterns from the real frontend. -- You must not import code, config, styles, fonts, routes, tokens, utilities, or components directly from `spx-gui/`. +- Inspect the current real frontend implementation before changing the corresponding prototype surface. +- Recreate the needed structure locally under `ui/prototype/`. +- Use local mock APIs and fake data instead of real network calls. +- Do not import code, config, styles, fonts, routes, tokens, utilities, or components directly from `spx-gui/`. -If something is needed from the real frontend, copy the minimum local version into `ui/prototype/` and remove production-only concerns. +If something is needed from the real frontend, copy or adapt the minimum local presentation-layer subset into `ui/prototype/` and remove production-only concerns. ## Source Priority When changing the prototype, use this order: -1. Design files and design assets decide what should be rendered. -2. Existing prototype files decide local conventions. -3. The real frontend is only a reference for structure, theme, naming, and visual implementation patterns. +1. The changed Pencil design file or design asset decides the intended UI. +2. The current real frontend decides the page structure, route shape, component boundaries, styling approach, and interaction model. +3. Existing prototype files decide local conventions. + +Do not invent an unrelated demo architecture when a matching real frontend surface exists. + +## Required Workflow + +1. Start from the real frontend. + - Inspect the corresponding implementation in the current Builder frontend before editing prototype code. + - Reuse the same route model, page/component split, style approach, and interaction logic where practical. + +2. Ensure the target prototype surface exists. + - If `ui/prototype` does not have the changed page or UI, initialize it from the current real frontend structure. + - If it exists but has drifted from the real frontend organization, align the structure first, then apply the design change. + +3. Apply only the current design change. + - Sync the latest relevant Pencil page change into `ui/prototype`. + - If only one page changed, only override that page or component surface. + - Other pages, routes, and features must remain accessible and usable through local mock behavior. + +4. Keep the prototype offline. + - Replace server APIs with local `src/apis/*` mock modules. + - Replace auth, persistence, permission, and backend state with deterministic local state. + - Keep interactions local and preview-oriented. + +5. Keep edits scoped. + - Write prototype-related changes under `ui/` unless the user explicitly authorizes broader scope. + - Do not modify `spx-gui/` by default. ## Boundaries @@ -38,8 +67,7 @@ Agents must preserve all of the following: - Runtime and build dependencies stay inside `ui/prototype/`, except shared static assets under `ui/images/`. - Do not import from `../../spx-gui` or any other real frontend directory. - Do not extend, merge, or proxy real frontend config. -- Do not add business logic, network requests, auth flow, persistence, or real app state. -- Keep interactions local, minimal, and preview-oriented. +- Do not add real network requests, real auth flow, real persistence, or real app state. - Keep mock data local. Forbidden shortcuts include: @@ -47,13 +75,16 @@ Forbidden shortcuts include: - importing the real frontend's Vite config, router, token files, or components - reading fonts from the real frontend instead of copying them locally - using aliases that resolve into the real frontend project +- adding `fetch`, `axios`, or backend client calls for prototype data +- replacing the app with a single isolated static demo page ## Alignment And Simplification Keep these areas aligned with the real frontend when practical: - Vite + Vue 3 + Vue Router + Tailwind CSS v4 -- page/component/data/style directory split +- route names and URL shape for community, search, tutorials, project, and editor surfaces +- page/component/API-style directory split - semantic token naming and UI-facing class style - typography, spacing, radius, shadow, and color conventions - `src/styles/app.css`, especially the `@theme inline` bridge and base presentation rules @@ -61,8 +92,10 @@ Keep these areas aligned with the real frontend when practical: At the same time, keep the prototype simpler than the real frontend: - replace business state with static data or minimal local state -- replace real flows with a small local router -- replace product actions with no-op handlers or local feedback +- replace real APIs with local functions returning mock data +- replace product actions with local feedback +- replace game/project runtime surfaces with static placeholder previews; do not copy SPX engine assets or parse XBP files +- do not recreate `/docs/*` pages or `src/widgets/*` entry surfaces because they are not end-user prototype UI - omit infrastructure and edge-case handling unless it changes the visual result ## Editing Rules @@ -70,51 +103,38 @@ At the same time, keep the prototype simpler than the real frontend: When iterating on the prototype: 1. Start from the design artifact that changed. -2. Reuse existing prototype components before adding abstractions. -3. Prefer pure presentational Vue components. -4. Prefer Tailwind utility classes. -5. Keep `src/styles/app.css` structurally aligned with the real frontend's `app.css`. -6. If new token families are needed, add local `--ui-*` variables first, then expose them through local `@theme inline` mapping. -7. If copying from the real frontend, copy the minimum presentational subset and strip production logic. - -## Long-Term Stability - -These rules are more important than reducing duplication: - -- Never reintroduce a direct dependency on the real frontend. -- Prefer local copies over shared imports for theme, style, asset, and config surfaces. -- Keep the prototype's public shape similar to the real frontend so later migration stays easy. -- Keep the prototype's implementation simpler so preview work stays cheap. -- If the prototype deliberately diverges from the real frontend, document that near the affected file or in `README.md`. - -Sync direction: - -1. Copy semantic token names and theme structure. -2. Copy base presentation rules. -3. Copy static assets locally when needed. -4. Recreate markup and styling locally. -5. Drop production-only logic. - -Do not treat prototype code as the source of truth for production code. +2. Inspect the matching real frontend files. +3. Reuse existing prototype components before adding abstractions. +4. Prefer pure presentational Vue components. +5. Prefer Tailwind utility classes. +6. Keep `src/styles/app.css` structurally aligned with the real frontend's `app.css`. +7. If new token families are needed, add local `--ui-*` variables first, then expose them through local `@theme inline` mapping. +8. If copying from the real frontend, copy the minimum presentational subset and strip production logic. ## Validation -After substantive changes: +After substantive changes, run from `ui/prototype/`: -- run `npm run build` inside `ui/prototype/` +```bash +npm run test:prototype +npm run build +``` -When UI structure or styling changes, also do the following when possible: +When UI structure or styling changes, also: - run the dev server from `ui/prototype/` - open the preview in a browser - verify the main route renders -- verify any local preview interaction still works +- verify changed pages are accessible +- verify important unchanged pages still load +- verify navigation, search, course-card navigation, project pages, and editor preview flows when relevant Before finishing, confirm: - no import points into `spx-gui/` - no config references the real frontend -- the prototype still behaves as a standalone app +- no real backend calls exist +- the prototype still behaves as a standalone offline app ## Keep This File Updated @@ -125,6 +145,7 @@ Update this file when any of the following changes: - the allowed relationship with the real frontend - the validation flow - the local prototype architecture +- the mock API/data strategy - the sync strategy for theme, asset, or config surfaces -If a change creates pressure to depend on the real frontend directly, document the local alternative here instead. \ No newline at end of file +If a change creates pressure to depend on the real frontend directly, document the local alternative here instead. diff --git a/ui/prototype/README.md b/ui/prototype/README.md index 9a5039cfc6..306e578c6a 100644 --- a/ui/prototype/README.md +++ b/ui/prototype/README.md @@ -1,8 +1,8 @@ # XBuilder Prototype Preview -这是一个独立的 UI 原型工程,用来预览 `ui/pages/spx/tutorial.pen` 对应的设计实现。 +这是一个独立的 XBuilder 前端原型工程,用来在 Design PR 阶段预览页面结构、视觉样式和基础交互。 它保持与真实前端相近的组织方式和技术栈:基于 Vite、Vue 3、Vue Router、Tailwind CSS v4,按页面、 -组件、数据和样式拆分;主题 token、基础排版和字体资源尽量与真实前端保持一致,但不包含业务逻辑,也不直接依赖真实前端项目。 +组件、mock API、数据和样式拆分;主题 token、基础排版和字体资源尽量与真实前端保持一致,但不直接依赖真实前端项目,也不调用服务端。 ## Run @@ -19,14 +19,24 @@ http://127.0.0.1:5174/ ## Scope -- `/` 重定向到 `/tutorials` -- `/tutorials` 使用 prototype 页面,并直接引用 `ui/images` 中的设计资源 +- `/`、`/explore`、`/search`、`/user/:nameInput` 以及 `/user/:nameInput/projects|likes|followers|following` 复刻社区和用户主页结构 +- `/project/:ownerInput/:nameInput` 复刻项目详情、运行预览、owner、说明、remix、release history 和相关项目区域 +- `/tutorials`、`/course-series/:courseSeriesIdInput`、`/course/:courseSeriesIdInput/:courseIdInput/start` 复刻教程主流程 +- `/editor/:ownerNameInput/:projectNameInput/:inEditorPath*` 提供离线编辑器预览,并包含 sprite editor 局部 prototype surface +- `/sign-in/callback`、`/sign-in/token` 提供本地模拟登录页面,不触发真实鉴权 - 样式通过 Tailwind v4 utility class 实现,并在 `src/styles/app.css` 中维护与真实前端接近的 `@theme inline` token 映射 -- 本地 mock 教程数据与卡片点击反馈 -- 导航、banner、列表和页脚都保留为纯展示层实现 +- 数据由 `src/apis/*` 和 `src/data/mock.ts` 提供,全部为本地假数据;导航、搜索、课程卡片跳转、项目页、编辑器局部交互均为本地状态 ## Constraints - 这个目录应始终可单独安装、单独启动、单独构建 - 可以复用 `ui/images` 这类设计资源,但不要直接 import 真实前端项目中的代码或配置 -- 如需模拟交互,只保留用于预览 UI 的最小本地状态 +- 不要加入真实 `fetch`、`axios`、鉴权、持久化、OpenAPI 服务加载或服务端状态 +- 不复刻 `/docs/*` 页面、`src/widgets/*` entry surface、SPX engine runtime 或 XBP 解析逻辑;项目运行区域使用静态占位预览 + +## Validate + +```bash +npm run test:prototype +npm run build +``` diff --git a/ui/prototype/package.json b/ui/prototype/package.json index 03f212c479..d456e72160 100644 --- a/ui/prototype/package.json +++ b/ui/prototype/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", - "preview": "vite preview" + "preview": "vite preview", + "test:prototype": "node scripts/check-prototype.mjs" }, "dependencies": { "@tailwindcss/vite": "4.2.2", diff --git a/ui/prototype/scripts/check-prototype.mjs b/ui/prototype/scripts/check-prototype.mjs new file mode 100644 index 0000000000..e760398c6d --- /dev/null +++ b/ui/prototype/scripts/check-prototype.mjs @@ -0,0 +1,875 @@ +import { readFileSync, readdirSync, statSync } from 'node:fs' +import { join, relative } from 'node:path' + +// Prototype contract check: +// keep this app offline and aligned with the real frontend surfaces we intentionally mirror, +// while rejecting production-only runtime/docs/widget artifacts that should not be copied here. +const root = new URL('..', import.meta.url).pathname +const srcRoot = join(root, 'src') + +function read(path) { + return readFileSync(join(root, path), 'utf8') +} + +function walk(dir) { + return readdirSync(dir).flatMap((entry) => { + const path = join(dir, entry) + if (entry === 'node_modules' || entry === 'dist') return [] + if (statSync(path).isDirectory()) return walk(path) + return [path] + }) +} + +const failures = [] +const sourceFiles = walk(srcRoot).filter((path) => /\.(ts|vue|css)$/.test(path)) +const router = read('src/router.ts') +const communityHome = read('src/pages/community/home.vue') +const communityExplore = read('src/pages/community/explore.vue') +const communityProject = read('src/pages/community/project.vue') +const guestBanner = read('src/components/community/home/GuestBanner.vue') +const navbar = read('src/components/community/CommunityNavbar.vue') +const projectsSection = read('src/components/community/ProjectsSection.vue') +const projectCard = read('src/components/project/ProjectCard.vue') +const app = read('src/App.vue') +const styles = read('src/styles/app.css') +const mockData = read('src/data/mock.ts') +const editorPage = read('src/pages/editor/index.vue') +const quickConfigBackIcon = read('src/assets/editor/quick-config/back.svg') +const codeZoomInIcon = read('src/assets/editor/code-editor/zoom-in.svg') +const codeZoomOutIcon = read('src/assets/editor/code-editor/zoom-out.svg') +const codeZoomResetIcon = read('src/assets/editor/code-editor/zoom-reset.svg') +const codeCloseCircleIcon = read('src/assets/editor/code-editor/close-circle.svg') +const tutorialIcon = read('src/assets/editor/navbar-icons/tutorial.svg') +const editorTimerIcon = read('src/assets/editor/ui-icons/timer.svg') +const editorStatusIcon = read('src/assets/editor/ui-icons/status.svg') +const editorSoundIcon = read('src/assets/editor/ui-icons/sound.svg') +const editorArrowDownIcon = read('src/assets/editor/ui-icons/arrow-down.svg') +const editorProjectFileIcon = read('src/assets/editor/navbar-icons/file.svg') +const navbarArrowMiniIcon = read('src/assets/navbar-icons/arrow-mini.svg') +const editorEyeOffIcon = read('src/assets/editor/ui-icons/eye-off.svg') +const editorPlusIcon = read('src/assets/editor/ui-icons/plus.svg') +const editorPublishIcon = read('src/assets/editor/ui-icons/publish.svg') +const editorLoadingIcon = read('src/assets/editor/ui-icons/loading.svg') +const editorMonitorIcon = read('src/assets/editor/widget/monitor.svg') +const projectRunner = read('src/components/project/ProjectRunner.vue') +const prototypeButton = read('src/components/ui/UIButton.vue') +const copilot = read('src/components/copilot/Copilot.vue') +const communityApi = read('src/apis/community.ts') +const centeredWrapper = read('src/components/community/CenteredWrapper.vue') +const prototypeTag = read('src/components/ui/UITag.vue') +const prototypeTab = read('src/components/ui/UITab.vue') +const prototypeSpriteItem = read('src/components/editor/SpriteItem.vue') +const prototypeCardHeader = read('src/components/ui/UICardHeader.vue') +const publishProjectModal = read('src/components/editor/PublishProjectModal.vue') +const spriteGeneratorModal = read('src/components/editor/SpriteGeneratorModal.vue') + +for (const route of [ + '/', + '/explore', + '/search', + '/user/:nameInput', + 'projects', + 'likes', + 'followers', + 'following', + '/project/:ownerInput/:nameInput', + '/editor', + '/editor/:ownerNameInput/:projectNameInput/:inEditorPath*', + '/tutorials', + '/course-series/:courseSeriesIdInput', + '/course/:courseSeriesIdInput/:courseIdInput/start', + '/sign-in/callback', + '/sign-in/token', + '/share/:owner/:name', + '/:pathMatch(.*)*' +]) { + if (!router.includes(route)) failures.push(`missing route: ${route}`) +} + +for (const requiredFile of [ + 'src/apis/community.ts', + 'src/apis/project.ts', + 'src/apis/tutorials.ts', + 'src/assets/projects/weathergggg/thumbnail.jpg', + 'src/assets/projects/niu-run/thumbnail.jpeg', + 'src/assets/editor/navbar-icons/tutorial.svg', + 'src/assets/editor/ui-icons/timer.svg', + 'src/assets/editor/ui-icons/status.svg', + 'src/assets/editor/ui-icons/sound.svg', + 'src/assets/editor/ui-icons/arrow-down.svg', + 'src/assets/editor/navbar-icons/file.svg', + 'src/assets/editor/ui-icons/plus.svg', + 'src/assets/editor/ui-icons/publish.svg', + 'src/assets/editor/ui-icons/loading.svg', + 'src/assets/editor/widget/monitor.svg', + 'src/components/project/ProjectRunner.vue', + 'src/components/editor/SpriteItem.vue', + 'src/components/editor/PublishProjectModal.vue', + 'src/components/editor/SpriteGeneratorModal.vue', + 'src/components/ui/UICardHeader.vue', + 'src/components/community/home/GuestBanner.vue', + 'src/pages/community/index.vue', + 'src/pages/community/project.vue', + 'src/pages/community/user/overview.vue', + 'src/pages/community/user/projects.vue', + 'src/pages/community/user/likes.vue', + 'src/pages/community/user/followers.vue', + 'src/pages/community/user/following.vue', + 'src/pages/editor/index.vue', + 'src/pages/sign-in/callback.vue', + 'src/pages/sign-in/token.vue', + 'src/pages/tutorials/course-series.vue', + 'src/pages/tutorials/course-start.vue' +]) { + try { + statSync(join(root, requiredFile)) + } catch { + failures.push(`missing file: ${requiredFile}`) + } +} + +for (const forbiddenPath of [ + 'public/spx_2.0.0', + 'src/assets/projects/weathergggg/Weathergggg.xbp', + 'src/assets/projects/niu-run/niu-run.xbp', + 'src/pages/docs', + 'src/widgets' +]) { + try { + statSync(join(root, forbiddenPath)) + failures.push(`production-only prototype artifact must not exist: ${forbiddenPath}`) + } catch { + // Expected: these surfaces are intentionally not copied into the prototype. + } +} + +if (router.includes('/docs') || router.includes('docs-api') || router.includes('docs-ui-design')) { + failures.push('prototype router must not include docs routes because docs are not end-user UI') +} + +if (mockData.includes('.xbp') || mockData.includes('Local XBP')) { + failures.push('mock project data must not depend on bundled XBP files') +} + +for (const renamedComponent of [ + 'PrototypeCopilot.vue', + 'PrototypeProjectRunner.vue', + 'PrototypeSpriteItem.vue', + 'PrototypeButton.vue', + 'PrototypeCard.vue', + 'PrototypeCardHeader.vue', + 'PrototypeTab.vue', + 'PrototypeTabs.vue', + 'PrototypeTag.vue' +]) { + for (const sourceFile of sourceFiles) { + const rel = relative(root, sourceFile) + const text = readFileSync(sourceFile, 'utf8') + if (text.includes(renamedComponent)) failures.push(`component filename must not keep Prototype prefix: ${rel}`) + } +} + +if (communityHome.includes('Build, play, and remix games') || communityHome.includes('ai-boy.png')) { + failures.push('community home must mirror real community home, not render a standalone marketing hero') +} + +for (const bannerToken of ['Join XBuilder', 'Build and share your projects', 'Join now', 'guest-banner-bg.png']) { + if (!guestBanner.includes(bannerToken)) failures.push(`guest home banner must mirror real token: ${bannerToken}`) +} + +for (const signedInOnlyToken of ['Your projects', '/explore?o=following']) { + if (communityHome.includes(signedInOnlyToken)) failures.push(`default community home must render guest state, not signed-in token: ${signedInOnlyToken}`) +} + +if (navbar.includes('to="/explore"')) { + failures.push('community navbar must mirror dev branch and avoid a standalone explore icon entry') +} + +if (communityExplore.includes('Browse fake local projects')) { + failures.push('community explore header must not render prototype-only helper copy') +} + +if ( + !communityExplore.includes('border-b border-grey-400 bg-grey-100') || + !communityExplore.includes('rounded-md border px-4') || + !communityExplore.includes("'border-primary-main bg-primary-main text-grey-100'") +) { + failures.push('community explore header filters must mirror the real CommunityHeader chip radio styling') +} + +if (projectCard.includes('remixes')) { + failures.push('project card must mirror dev branch metadata and show updated time instead of remix count') +} + +if (communityProject.includes('Remixed from {{ project.remixedFrom.title }}')) { + failures.push('project page remixed-from copy must mirror real text/link structure') +} + +if (!communityProject.includes('type="primary"') || !communityProject.includes('type="secondary"') || communityProject.includes('!rounded-md')) { + failures.push('project page action buttons must use prototype button variants instead of ad-hoc overrides') +} + +if (!communityProject.includes('release-timeline') || !communityProject.includes('group/timeline-item')) { + failures.push('project page release history must render timeline-style release notes') +} + +if (!projectCard.includes('updatedAt')) { + failures.push('project card must render updatedAt metadata') +} + +for (const route of ['/explore?o=likes', '/explore?o=remix']) { + if (!communityHome.includes(route)) failures.push(`community home must use real explore route: ${route}`) +} + +if (!navbar.includes('Sign in') || !navbar.includes('openSignInModal') || !navbar.includes('role="dialog"')) { + failures.push('community navbar must expose the real guest sign-in entry') +} + +if (!projectsSection.includes('link-primary flex items-center text-lg')) { + failures.push('projects section more link must mirror real RouterUILink/link-primary styling') +} + +if (projectsSection.includes('stroke="currentColor"')) { + failures.push('projects section more link must use real arrowRightSmall icon, not chevron stroke icon') +} + +if (!app.includes('Copilot')) { + failures.push('prototype must mount the offline Copilot surface globally') +} + +if (copilot.includes('const isOpen = ref(true)')) { + failures.push('offline Copilot must default to collapsed so it does not cover editor panels') +} + +if ( + projectRunner.includes('('initial')") || + !projectRunner.includes('const loading = computed(() => state.value === \'loading\')') || + !projectRunner.includes('window.setTimeout') || + !projectRunner.includes(':loading="loading"') || + !prototypeButton.includes("import loadingIcon from '@/assets/editor/ui-icons/loading.svg?raw'") || + !prototypeButton.includes('loading?: boolean') || + !prototypeButton.includes('v-if="loading"') || + !prototypeButton.includes('animate-spin') || + !editorLoadingIcon.includes('M6.99975 1.74999') +) { + failures.push('project runner Run button must expose a local UIButton loading animation before running') +} + +if (copilot.includes('const panelHeight =')) { + failures.push('copilot drag clamp must not compute unused panelHeight on every move') +} + +if (!copilot.includes('let resizeTimer') || !copilot.includes('setTimeout(persistPanelPosition, 100)')) { + failures.push('copilot resize persistence must be debounced') +} + +if ( + !copilot.includes("right: isOpen.value ? `${panelPosition.value.right}px` : '-340px'") || + !copilot.includes('class="copilot-trigger right visible group') || + !copilot.includes("-translate-x-full") || + !copilot.includes("bg-[linear-gradient(90deg,#c390ff_0%,#72bbff_100%)]") || + copilot.includes(' + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/prototype/src/assets/projects/niu-run/thumbnail.jpeg b/ui/prototype/src/assets/projects/niu-run/thumbnail.jpeg new file mode 100644 index 0000000000..16d8f84987 Binary files /dev/null and b/ui/prototype/src/assets/projects/niu-run/thumbnail.jpeg differ diff --git a/ui/prototype/src/assets/projects/weathergggg/editor/jaime.png b/ui/prototype/src/assets/projects/weathergggg/editor/jaime.png new file mode 100644 index 0000000000..821a10cc17 Binary files /dev/null and b/ui/prototype/src/assets/projects/weathergggg/editor/jaime.png differ diff --git a/ui/prototype/src/assets/projects/weathergggg/editor/kai.png b/ui/prototype/src/assets/projects/weathergggg/editor/kai.png new file mode 100644 index 0000000000..ad0d8b9d98 Binary files /dev/null and b/ui/prototype/src/assets/projects/weathergggg/editor/kai.png differ diff --git a/ui/prototype/src/assets/projects/weathergggg/editor/urban1.png b/ui/prototype/src/assets/projects/weathergggg/editor/urban1.png new file mode 100644 index 0000000000..3308cf1e5a Binary files /dev/null and b/ui/prototype/src/assets/projects/weathergggg/editor/urban1.png differ diff --git a/ui/prototype/src/assets/projects/weathergggg/thumbnail.jpg b/ui/prototype/src/assets/projects/weathergggg/thumbnail.jpg new file mode 100644 index 0000000000..7bca7cd49f Binary files /dev/null and b/ui/prototype/src/assets/projects/weathergggg/thumbnail.jpg differ diff --git a/ui/prototype/src/assets/stage-bg.svg b/ui/prototype/src/assets/stage-bg.svg new file mode 100644 index 0000000000..4ed2511929 --- /dev/null +++ b/ui/prototype/src/assets/stage-bg.svg @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/prototype/src/components/common/ListResultWrapper.vue b/ui/prototype/src/components/common/ListResultWrapper.vue new file mode 100644 index 0000000000..95b7a32b09 --- /dev/null +++ b/ui/prototype/src/components/common/ListResultWrapper.vue @@ -0,0 +1,16 @@ + + + diff --git a/ui/prototype/src/components/common/TextView.vue b/ui/prototype/src/components/common/TextView.vue new file mode 100644 index 0000000000..1cf3067afa --- /dev/null +++ b/ui/prototype/src/components/common/TextView.vue @@ -0,0 +1,12 @@ + + + diff --git a/ui/prototype/src/components/community/CenteredWrapper.vue b/ui/prototype/src/components/community/CenteredWrapper.vue index 538e473e67..eb3d04f36e 100644 --- a/ui/prototype/src/components/community/CenteredWrapper.vue +++ b/ui/prototype/src/components/community/CenteredWrapper.vue @@ -1,10 +1,7 @@ diff --git a/ui/prototype/src/components/community/CommunityHeader.vue b/ui/prototype/src/components/community/CommunityHeader.vue new file mode 100644 index 0000000000..e65bfcad8b --- /dev/null +++ b/ui/prototype/src/components/community/CommunityHeader.vue @@ -0,0 +1,16 @@ + + + diff --git a/ui/prototype/src/components/community/CommunityNavbar.vue b/ui/prototype/src/components/community/CommunityNavbar.vue index 25eb656dfb..277dd36536 100644 --- a/ui/prototype/src/components/community/CommunityNavbar.vue +++ b/ui/prototype/src/components/community/CommunityNavbar.vue @@ -1,47 +1,167 @@ diff --git a/ui/prototype/src/components/community/ProjectsSection.vue b/ui/prototype/src/components/community/ProjectsSection.vue new file mode 100644 index 0000000000..4e7b1e5de8 --- /dev/null +++ b/ui/prototype/src/components/community/ProjectsSection.vue @@ -0,0 +1,47 @@ + + + diff --git a/ui/prototype/src/components/community/home/GuestBanner.vue b/ui/prototype/src/components/community/home/GuestBanner.vue new file mode 100644 index 0000000000..cd8a030e3f --- /dev/null +++ b/ui/prototype/src/components/community/home/GuestBanner.vue @@ -0,0 +1,25 @@ + + + diff --git a/ui/prototype/src/components/community/user/UserContent.vue b/ui/prototype/src/components/community/user/UserContent.vue new file mode 100644 index 0000000000..2f42cc268b --- /dev/null +++ b/ui/prototype/src/components/community/user/UserContent.vue @@ -0,0 +1,13 @@ + diff --git a/ui/prototype/src/components/community/user/UserHeader.vue b/ui/prototype/src/components/community/user/UserHeader.vue new file mode 100644 index 0000000000..f7980ccbb9 --- /dev/null +++ b/ui/prototype/src/components/community/user/UserHeader.vue @@ -0,0 +1,23 @@ + + + diff --git a/ui/prototype/src/components/community/user/UserList.vue b/ui/prototype/src/components/community/user/UserList.vue new file mode 100644 index 0000000000..ae936a1cd0 --- /dev/null +++ b/ui/prototype/src/components/community/user/UserList.vue @@ -0,0 +1,23 @@ + + + diff --git a/ui/prototype/src/components/community/user/UserSidebar.vue b/ui/prototype/src/components/community/user/UserSidebar.vue new file mode 100644 index 0000000000..b454259876 --- /dev/null +++ b/ui/prototype/src/components/community/user/UserSidebar.vue @@ -0,0 +1,51 @@ + + + diff --git a/ui/prototype/src/components/copilot/Copilot.vue b/ui/prototype/src/components/copilot/Copilot.vue new file mode 100644 index 0000000000..fafdd86e93 --- /dev/null +++ b/ui/prototype/src/components/copilot/Copilot.vue @@ -0,0 +1,279 @@ + + + diff --git a/ui/prototype/src/components/editor/PublishProjectModal.vue b/ui/prototype/src/components/editor/PublishProjectModal.vue new file mode 100644 index 0000000000..917f1f433b --- /dev/null +++ b/ui/prototype/src/components/editor/PublishProjectModal.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/ui/prototype/src/components/editor/SpriteGeneratorModal.vue b/ui/prototype/src/components/editor/SpriteGeneratorModal.vue new file mode 100644 index 0000000000..424e57ccfa --- /dev/null +++ b/ui/prototype/src/components/editor/SpriteGeneratorModal.vue @@ -0,0 +1,790 @@ + + + + + diff --git a/ui/prototype/src/components/editor/SpriteItem.vue b/ui/prototype/src/components/editor/SpriteItem.vue new file mode 100644 index 0000000000..e21fb6c035 --- /dev/null +++ b/ui/prototype/src/components/editor/SpriteItem.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/ui/prototype/src/components/editor/UIBlockItem.vue b/ui/prototype/src/components/editor/UIBlockItem.vue new file mode 100644 index 0000000000..3a6dfabfaa --- /dev/null +++ b/ui/prototype/src/components/editor/UIBlockItem.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/ui/prototype/src/components/editor/UIEditorSpriteItem.vue b/ui/prototype/src/components/editor/UIEditorSpriteItem.vue new file mode 100644 index 0000000000..e9f9c6e056 --- /dev/null +++ b/ui/prototype/src/components/editor/UIEditorSpriteItem.vue @@ -0,0 +1,40 @@ + + + diff --git a/ui/prototype/src/components/project/ProjectCard.vue b/ui/prototype/src/components/project/ProjectCard.vue new file mode 100644 index 0000000000..c4288d0bb5 --- /dev/null +++ b/ui/prototype/src/components/project/ProjectCard.vue @@ -0,0 +1,77 @@ + + + diff --git a/ui/prototype/src/components/project/ProjectRunner.vue b/ui/prototype/src/components/project/ProjectRunner.vue new file mode 100644 index 0000000000..84a62123cc --- /dev/null +++ b/ui/prototype/src/components/project/ProjectRunner.vue @@ -0,0 +1,71 @@ + + + diff --git a/ui/prototype/src/components/tutorials/CourseItem.vue b/ui/prototype/src/components/tutorials/CourseItem.vue new file mode 100644 index 0000000000..c1f9759783 --- /dev/null +++ b/ui/prototype/src/components/tutorials/CourseItem.vue @@ -0,0 +1,20 @@ + + + diff --git a/ui/prototype/src/components/tutorials/CourseSeriesCard.vue b/ui/prototype/src/components/tutorials/CourseSeriesCard.vue index 5bb449637b..72c495e18e 100644 --- a/ui/prototype/src/components/tutorials/CourseSeriesCard.vue +++ b/ui/prototype/src/components/tutorials/CourseSeriesCard.vue @@ -1,32 +1,27 @@ diff --git a/ui/prototype/src/components/tutorials/TutorialHome.vue b/ui/prototype/src/components/tutorials/TutorialHome.vue index 8d71472bca..078b411625 100644 --- a/ui/prototype/src/components/tutorials/TutorialHome.vue +++ b/ui/prototype/src/components/tutorials/TutorialHome.vue @@ -1,33 +1,32 @@ diff --git a/ui/prototype/src/components/tutorials/TutorialsBanner.vue b/ui/prototype/src/components/tutorials/TutorialsBanner.vue index 8798650fac..500a9d0226 100644 --- a/ui/prototype/src/components/tutorials/TutorialsBanner.vue +++ b/ui/prototype/src/components/tutorials/TutorialsBanner.vue @@ -1,9 +1,15 @@ + + diff --git a/ui/prototype/src/components/ui/UIButton.vue b/ui/prototype/src/components/ui/UIButton.vue new file mode 100644 index 0000000000..7ba67a4480 --- /dev/null +++ b/ui/prototype/src/components/ui/UIButton.vue @@ -0,0 +1,47 @@ + + + diff --git a/ui/prototype/src/components/ui/UICard.vue b/ui/prototype/src/components/ui/UICard.vue new file mode 100644 index 0000000000..3b44104fa4 --- /dev/null +++ b/ui/prototype/src/components/ui/UICard.vue @@ -0,0 +1,5 @@ + diff --git a/ui/prototype/src/components/ui/UICardHeader.vue b/ui/prototype/src/components/ui/UICardHeader.vue new file mode 100644 index 0000000000..22179d2ff6 --- /dev/null +++ b/ui/prototype/src/components/ui/UICardHeader.vue @@ -0,0 +1,5 @@ + diff --git a/ui/prototype/src/components/ui/UITab.vue b/ui/prototype/src/components/ui/UITab.vue new file mode 100644 index 0000000000..988f4e6baf --- /dev/null +++ b/ui/prototype/src/components/ui/UITab.vue @@ -0,0 +1,31 @@ + + + diff --git a/ui/prototype/src/components/ui/UITabs.vue b/ui/prototype/src/components/ui/UITabs.vue new file mode 100644 index 0000000000..ec1d1a54c6 --- /dev/null +++ b/ui/prototype/src/components/ui/UITabs.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/ui/prototype/src/components/ui/UITag.vue b/ui/prototype/src/components/ui/UITag.vue new file mode 100644 index 0000000000..1a4ac4e1e6 --- /dev/null +++ b/ui/prototype/src/components/ui/UITag.vue @@ -0,0 +1,99 @@ + + + + + + + diff --git a/ui/prototype/src/composables/prototypeSignIn.ts b/ui/prototype/src/composables/prototypeSignIn.ts new file mode 100644 index 0000000000..7e82410546 --- /dev/null +++ b/ui/prototype/src/composables/prototypeSignIn.ts @@ -0,0 +1,19 @@ +import { ref } from 'vue' + +const signInModalOpen = ref(false) + +export function usePrototypeSignIn() { + function openSignInModal() { + signInModalOpen.value = true + } + + function closeSignInModal() { + signInModalOpen.value = false + } + + return { + signInModalOpen, + openSignInModal, + closeSignInModal + } +} diff --git a/ui/prototype/src/data/mock.ts b/ui/prototype/src/data/mock.ts new file mode 100644 index 0000000000..0c15df226e --- /dev/null +++ b/ui/prototype/src/data/mock.ts @@ -0,0 +1,413 @@ +import aiPixelImage from '@ui-images/ai-pixel.png' +import aiSideScrollingImage from '@ui-images/ai-side-scrolling.png' +import aiTopDownImage from '@ui-images/ai-top-down-view.png' +import avatarImage from '@ui-images/avatar.png' +import backdropImage from '@ui-images/backdrop.png' +import builderUsageImage from '@ui-images/builder-usage.png' +import mapImage from '@ui-images/map-bg.png' +import monitorImage from '@ui-images/monitor-colorful.png' +import personalInfoBackgroundImage from '@ui-images/personal-information-background.png' +import projectRunImage from '@ui-images/project-run.png' +import spriteReviewImage from '@ui-images/sprite-review.png' +import userBackgroundImage from '@ui-images/user-bg.png' +import niuRunThumbnail from '@/assets/projects/niu-run/thumbnail.jpeg' +import weatherggggThumbnail from '@/assets/projects/weathergggg/thumbnail.jpg' + +export type UserProfile = { + username: string + displayName: string + avatar: string + cover: string + bio: string + location: string + joinedAt: string + followers: number + following: number +} + +export type Project = { + id: string + name: string + title: string + owner: UserProfile + visibility?: 'public' | 'private' + description: string + instructions?: string + thumbnail: string + tags: string[] + likes: number + remixes: number + views: number + updatedAt: string + createdAt?: string + remixedFrom?: { + owner: string + name: string + title: string + } + releaseHistory?: Release[] +} + +export type Course = { + id: string + title: string + summary: string + duration: string + completed: boolean +} + +export type CourseSeries = { + id: string + title: string + description: string + total: string + updatedAt: string + cover: string + courses: Course[] +} + +export type Sprite = { + id: string + name: string + color: string + selected: boolean + visible: boolean +} + +export type Release = { + id: string + version: string + createdAt: string + notes: string +} + +export type ActivityUser = UserProfile & { + relation: 'follower' | 'following' +} + +export type WidgetSample = { + id: string + title: string + description: string +} + +export const signedInUsername = 'qingqing' + +export const releases: Release[] = [ + { + id: 'niu-run-v3', + version: 'v3', + createdAt: 'Today', + notes: 'Adjusted movement timing and refreshed the local thumbnail.' + }, + { + id: 'niu-run-v2', + version: 'v2', + createdAt: '1 week ago', + notes: 'Added touch controls and clearer start instructions.' + }, + { + id: 'niu-run-v1', + version: 'v1', + createdAt: '2 weeks ago', + notes: 'Published the first offline prototype build.' + } +] + +export const users: UserProfile[] = [ + { + username: 'code-kiko', + displayName: 'Code Kiko', + avatar: avatarImage, + cover: userBackgroundImage, + bio: 'Building playful coding projects and sharing remixable examples.', + location: 'XBuilder Studio', + joinedAt: 'Joined 2026', + followers: 1280, + following: 86 + }, + { + username: 'maya', + displayName: 'Maya', + avatar: avatarImage, + cover: personalInfoBackgroundImage, + bio: 'Loves side-scrolling games, sprite animation, and bright stage design.', + location: 'Creative Lab', + joinedAt: 'Joined 2025', + followers: 824, + following: 64 + }, + { + username: 'leo', + displayName: 'Leo', + avatar: avatarImage, + cover: userBackgroundImage, + bio: 'Prototype collector and remix challenge host.', + location: 'Game Club', + joinedAt: 'Joined 2025', + followers: 614, + following: 102 + }, + { + username: 'qingqing', + displayName: 'Qingqing', + avatar: avatarImage, + cover: personalInfoBackgroundImage, + bio: 'Designing and testing local XBuilder prototype projects.', + location: 'Builder Lab', + joinedAt: 'Joined 2026', + followers: 96, + following: 18 + } +] + +export const projects: Project[] = [ + { + id: 'local-weathergggg', + name: 'weathergggg', + title: 'Weathergggg', + owner: users[3], + visibility: 'public', + description: 'A local offline XBuilder project shown with static prototype data.', + instructions: 'Press Run to preview the project placeholder.', + thumbnail: weatherggggThumbnail, + tags: ['Game', 'Offline'], + likes: 412, + remixes: 63, + views: 5380, + updatedAt: 'Today', + createdAt: '2 weeks ago', + releaseHistory: releases.slice(1) + }, + { + id: 'local-niu-run', + name: 'niu-run', + title: 'niu-run', + owner: users[3], + visibility: 'private', + description: '被牛小花抓到你就输啦', + instructions: '点击草地控制小牛行走', + thumbnail: niuRunThumbnail, + tags: ['Game', 'Runner'], + likes: 389, + remixes: 57, + views: 5010, + updatedAt: 'Today', + createdAt: '2 weeks ago', + remixedFrom: { + owner: 'code-kiko', + name: 'forest-runner', + title: 'Forest Runner' + }, + releaseHistory: releases + }, + { + id: '1', + name: 'forest-runner', + title: 'Forest Runner', + owner: users[0], + visibility: 'public', + description: 'A side-scrolling runner with collectible stars, moving platforms, and a timed finish.', + thumbnail: projectRunImage, + tags: ['Game', 'Runner', 'Remixable'], + likes: 368, + remixes: 54, + views: 4920, + updatedAt: '1 day ago', + createdAt: '1 month ago' + }, + { + id: '2', + name: 'space-catcher', + title: 'Space Catcher', + owner: users[1], + visibility: 'public', + description: 'Catch falling crystals and avoid asteroids while the backdrop changes speed.', + thumbnail: aiSideScrollingImage, + tags: ['Arcade', 'Sprite'], + likes: 274, + remixes: 39, + views: 3560, + updatedAt: '3 days ago', + createdAt: '1 month ago' + }, + { + id: '3', + name: 'pixel-garden', + title: 'Pixel Garden', + owner: users[2], + visibility: 'public', + description: 'Grow a garden by sequencing actions and remixing sprite costumes.', + thumbnail: aiPixelImage, + tags: ['Pixel', 'Tutorial'], + likes: 221, + remixes: 42, + views: 2980, + updatedAt: '5 days ago', + createdAt: '1 month ago' + }, + { + id: '4', + name: 'robot-maze', + title: 'Robot Maze', + owner: users[0], + description: 'Program a robot to solve mazes with conditions and repeated commands.', + thumbnail: monitorImage, + tags: ['Logic', 'Maze'], + likes: 184, + remixes: 31, + views: 2640, + updatedAt: '1 week ago', + createdAt: '2 months ago' + }, + { + id: '5', + name: 'ocean-rescue', + title: 'Ocean Rescue', + owner: users[1], + description: 'A top-down rescue game with animated water, score feedback, and hazards.', + thumbnail: aiTopDownImage, + tags: ['Top Down', 'Animation'], + likes: 162, + remixes: 25, + views: 2110, + updatedAt: '2 weeks ago', + createdAt: '2 months ago' + }, + { + id: '6', + name: 'weather-stage', + title: 'Weather Stage', + owner: users[2], + description: 'Switch backdrops and sound effects to tell an interactive weather story.', + thumbnail: backdropImage, + tags: ['Story', 'Backdrop'], + likes: 148, + remixes: 18, + views: 1890, + updatedAt: '2 weeks ago', + createdAt: '2 months ago' + } +] + +export const courseSeries: CourseSeries[] = [ + { + id: 'code-kiko-usage', + title: 'Code Kiko: XBuilder Usage', + description: 'Learn the editor basics, sprites, backdrops, events, and project sharing.', + total: '10 Total', + updatedAt: '1 week ago', + cover: mapImage, + courses: [ + { + id: 'start', + title: 'Start with a Sprite', + summary: 'Create a project, rename sprites, and run the stage.', + duration: '8 min', + completed: true + }, + { + id: 'move', + title: 'Make It Move', + summary: 'Use events and motion blocks to build the first interaction.', + duration: '12 min', + completed: false + }, + { + id: 'share', + title: 'Publish and Remix', + summary: 'Preview, publish, and remix a community project.', + duration: '10 min', + completed: false + } + ] + }, + { + id: 'sprite-animation', + title: 'Sprite Animation Lab', + description: 'Practice costume editing, visibility states, and motion feedback.', + total: '8 Total', + updatedAt: '2 weeks ago', + cover: spriteReviewImage, + courses: [ + { + id: 'costumes', + title: 'Costumes and Frames', + summary: 'Build smooth sprite changes with costume frames.', + duration: '11 min', + completed: false + }, + { + id: 'visibility', + title: 'Hide and Show', + summary: 'Control sprite visibility and stage feedback.', + duration: '9 min', + completed: false + } + ] + }, + { + id: 'project-playground', + title: 'Project Playground', + description: 'Explore community projects and learn how remix flows work.', + total: '6 Total', + updatedAt: '3 weeks ago', + cover: builderUsageImage, + courses: [ + { + id: 'explore', + title: 'Find a Project', + summary: 'Search, filter, and open a project from the community.', + duration: '7 min', + completed: false + }, + { + id: 'remix', + title: 'Create a Remix', + summary: 'Make a local copy and change the main sprite.', + duration: '13 min', + completed: false + } + ] + } +] + +export const editorProject = { + project: projects[0], + sprites: [ + { + id: 'kiko', + name: 'Kiko Running Character', + color: '#36c2cf', + selected: true, + visible: true + }, + { + id: 'star', + name: 'Collectible Golden Star', + color: '#f3c614', + selected: false, + visible: false + }, + { + id: 'platform', + name: 'Long Moving Platform', + color: '#9b63f6', + selected: false, + visible: true + } + ] satisfies Sprite[] +} + +export const widgetSamples: WidgetSample[] = [ + { + id: 'runner', + title: 'Project preview', + description: 'Shows the project preview placeholder with local state.' + }, + { + id: 'code-editor', + title: 'Code editor preview', + description: 'Shows code, diagnostics, and snippets with local mock data.' + } +] diff --git a/ui/prototype/src/data/tutorials.ts b/ui/prototype/src/data/tutorials.ts deleted file mode 100644 index 9d67c43286..0000000000 --- a/ui/prototype/src/data/tutorials.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type TutorialCard = { - id: string - title: string - total: string - updatedAt: string -} - -export const tutorials: TutorialCard[] = Array.from({ length: 12 }, (_, index) => ({ - id: `code-kiko-${index + 1}`, - title: 'Code Kiko: XBuilder Usage', - total: '10 Total', - updatedAt: '1 weeks ago' -})) diff --git a/ui/prototype/src/pages/404/index.vue b/ui/prototype/src/pages/404/index.vue new file mode 100644 index 0000000000..cd9c275663 --- /dev/null +++ b/ui/prototype/src/pages/404/index.vue @@ -0,0 +1,29 @@ + + + diff --git a/ui/prototype/src/pages/community/explore.vue b/ui/prototype/src/pages/community/explore.vue new file mode 100644 index 0000000000..9790cf5f8e --- /dev/null +++ b/ui/prototype/src/pages/community/explore.vue @@ -0,0 +1,46 @@ + + + diff --git a/ui/prototype/src/pages/community/home.vue b/ui/prototype/src/pages/community/home.vue new file mode 100644 index 0000000000..58e2625077 --- /dev/null +++ b/ui/prototype/src/pages/community/home.vue @@ -0,0 +1,24 @@ + + + diff --git a/ui/prototype/src/pages/community/index.vue b/ui/prototype/src/pages/community/index.vue new file mode 100644 index 0000000000..fd0a06c28c --- /dev/null +++ b/ui/prototype/src/pages/community/index.vue @@ -0,0 +1,14 @@ + + + diff --git a/ui/prototype/src/pages/community/project.vue b/ui/prototype/src/pages/community/project.vue new file mode 100644 index 0000000000..3447a8f6a9 --- /dev/null +++ b/ui/prototype/src/pages/community/project.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/ui/prototype/src/pages/community/search.vue b/ui/prototype/src/pages/community/search.vue new file mode 100644 index 0000000000..00c01a15bc --- /dev/null +++ b/ui/prototype/src/pages/community/search.vue @@ -0,0 +1,60 @@ + + + diff --git a/ui/prototype/src/pages/community/user/followers.vue b/ui/prototype/src/pages/community/user/followers.vue new file mode 100644 index 0000000000..94cc80a1da --- /dev/null +++ b/ui/prototype/src/pages/community/user/followers.vue @@ -0,0 +1,20 @@ + + + diff --git a/ui/prototype/src/pages/community/user/following.vue b/ui/prototype/src/pages/community/user/following.vue new file mode 100644 index 0000000000..34446c3412 --- /dev/null +++ b/ui/prototype/src/pages/community/user/following.vue @@ -0,0 +1,20 @@ + + + diff --git a/ui/prototype/src/pages/community/user/index.vue b/ui/prototype/src/pages/community/user/index.vue new file mode 100644 index 0000000000..7a1be849e9 --- /dev/null +++ b/ui/prototype/src/pages/community/user/index.vue @@ -0,0 +1,30 @@ + + + diff --git a/ui/prototype/src/pages/community/user/likes.vue b/ui/prototype/src/pages/community/user/likes.vue new file mode 100644 index 0000000000..a0b5f7f1d0 --- /dev/null +++ b/ui/prototype/src/pages/community/user/likes.vue @@ -0,0 +1,24 @@ + + + diff --git a/ui/prototype/src/pages/community/user/overview.vue b/ui/prototype/src/pages/community/user/overview.vue new file mode 100644 index 0000000000..6d4dfef4ee --- /dev/null +++ b/ui/prototype/src/pages/community/user/overview.vue @@ -0,0 +1,48 @@ + + + diff --git a/ui/prototype/src/pages/community/user/projects.vue b/ui/prototype/src/pages/community/user/projects.vue new file mode 100644 index 0000000000..3eaea51d1b --- /dev/null +++ b/ui/prototype/src/pages/community/user/projects.vue @@ -0,0 +1,40 @@ + + + diff --git a/ui/prototype/src/pages/editor/index.vue b/ui/prototype/src/pages/editor/index.vue new file mode 100644 index 0000000000..e4176e0df0 --- /dev/null +++ b/ui/prototype/src/pages/editor/index.vue @@ -0,0 +1,5997 @@ + + + + + diff --git a/ui/prototype/src/pages/sign-in/callback.vue b/ui/prototype/src/pages/sign-in/callback.vue new file mode 100644 index 0000000000..c1752fb218 --- /dev/null +++ b/ui/prototype/src/pages/sign-in/callback.vue @@ -0,0 +1,20 @@ + + + diff --git a/ui/prototype/src/pages/sign-in/token.vue b/ui/prototype/src/pages/sign-in/token.vue new file mode 100644 index 0000000000..023f6893b7 --- /dev/null +++ b/ui/prototype/src/pages/sign-in/token.vue @@ -0,0 +1,39 @@ + + + diff --git a/ui/prototype/src/pages/tutorials/course-series.vue b/ui/prototype/src/pages/tutorials/course-series.vue new file mode 100644 index 0000000000..44791c9b03 --- /dev/null +++ b/ui/prototype/src/pages/tutorials/course-series.vue @@ -0,0 +1,51 @@ + + + diff --git a/ui/prototype/src/pages/tutorials/course-start.vue b/ui/prototype/src/pages/tutorials/course-start.vue new file mode 100644 index 0000000000..96650fc2cb --- /dev/null +++ b/ui/prototype/src/pages/tutorials/course-start.vue @@ -0,0 +1,72 @@ + + + diff --git a/ui/prototype/src/pages/tutorials/index.vue b/ui/prototype/src/pages/tutorials/index.vue index c4faf30e9c..59c0db0fb9 100644 --- a/ui/prototype/src/pages/tutorials/index.vue +++ b/ui/prototype/src/pages/tutorials/index.vue @@ -1,39 +1,16 @@ diff --git a/ui/prototype/src/router.ts b/ui/prototype/src/router.ts index 246daf27c6..d6dc9fd151 100644 --- a/ui/prototype/src/router.ts +++ b/ui/prototype/src/router.ts @@ -1,22 +1,137 @@ import { createRouter, createWebHistory } from 'vue-router' +export type UserTab = 'overview' | 'projects' | 'likes' | 'followers' | 'following' + +export function getProjectPageRoute(owner: string, name: string) { + return `/project/${encodeURIComponent(owner)}/${encodeURIComponent(name)}` +} + +export function getProjectEditorRoute(owner: string, name: string) { + return `/editor/${encodeURIComponent(owner)}/${encodeURIComponent(name)}` +} + +export function getUserPageRoute(name: string, tab: UserTab = 'overview') { + const base = `/user/${encodeURIComponent(name)}` + return tab === 'overview' ? base : `${base}/${tab}` +} + +export function getSearchRoute(keyword = '') { + return keyword === '' ? '/search' : `/search?q=${encodeURIComponent(keyword)}` +} + const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', - redirect: '/tutorials' + component: () => import('@/pages/community/index.vue'), + children: [ + { + path: '/', + name: 'home', + component: () => import('@/pages/community/home.vue') + }, + { + path: '/explore', + name: 'explore', + component: () => import('@/pages/community/explore.vue') + }, + { + path: '/search', + name: 'search', + component: () => import('@/pages/community/search.vue') + }, + { + path: '/user/:nameInput', + component: () => import('@/pages/community/user/index.vue'), + props: true, + children: [ + { + path: '', + name: 'user-overview', + component: () => import('@/pages/community/user/overview.vue'), + props: true + }, + { + path: 'projects', + name: 'user-projects', + component: () => import('@/pages/community/user/projects.vue'), + props: true + }, + { + path: 'likes', + name: 'user-likes', + component: () => import('@/pages/community/user/likes.vue'), + props: true + }, + { + path: 'followers', + name: 'user-followers', + component: () => import('@/pages/community/user/followers.vue'), + props: true + }, + { + path: 'following', + name: 'user-following', + component: () => import('@/pages/community/user/following.vue'), + props: true + } + ] + }, + { + path: '/project/:ownerInput/:nameInput', + name: 'project', + component: () => import('@/pages/community/project.vue'), + props: true + } + ] + }, + { + path: '/editor', + redirect: '/' + }, + { + path: '/editor/:ownerNameInput/:projectNameInput/:inEditorPath*', + name: 'editor', + component: () => import('@/pages/editor/index.vue'), + props: true }, { path: '/tutorials', name: 'tutorials', component: () => import('@/pages/tutorials/index.vue') }, + { + path: '/course/:courseSeriesIdInput/:courseIdInput/start', + name: 'course-start', + component: () => import('@/pages/tutorials/course-start.vue'), + props: true + }, + { + path: '/course-series/:courseSeriesIdInput', + name: 'course-series', + component: () => import('@/pages/tutorials/course-series.vue'), + props: true + }, + { + path: '/sign-in/callback', + name: 'sign-in-callback', + component: () => import('@/pages/sign-in/callback.vue') + }, + { + path: '/sign-in/token', + name: 'sign-in-token', + component: () => import('@/pages/sign-in/token.vue') + }, + { + path: '/share/:owner/:name', + redirect: (to) => getProjectPageRoute(to.params.owner as string, to.params.name as string) + }, { path: '/:pathMatch(.*)*', - redirect: '/tutorials' + component: () => import('@/pages/404/index.vue') } ] }) -export default router \ No newline at end of file +export default router diff --git a/ui/prototype/src/styles/app.css b/ui/prototype/src/styles/app.css index 0d453029c0..caf86bc683 100644 --- a/ui/prototype/src/styles/app.css +++ b/ui/prototype/src/styles/app.css @@ -1,62 +1,91 @@ @import 'tailwindcss/theme.css' layer(theme); @import 'tailwindcss/utilities.css' layer(utilities); +@layer components { + .link-primary { + color: var(--ui-color-primary-main); + text-decoration: none; + transition: color 0.1s; + + &:hover { + color: var(--ui-color-primary-400); + } + + &:active { + color: var(--ui-color-primary-600); + } + } + + .link-boring { + color: inherit; + transition: color 0.1s; + + &:hover { + color: var(--ui-color-primary-main); + } + + &:active { + color: var(--ui-color-primary-600); + } + } +} + @layer base { :root { - --ui-color-turquoise-100: #d2f4f5; - --ui-color-turquoise-200: #9ce6ea; - --ui-color-turquoise-300: #74dbe1; - --ui-color-turquoise-400: #4bcdd6; + --ui-color-turquoise-100: #f3fbfc; + --ui-color-turquoise-200: #eaf9fa; + --ui-color-turquoise-300: #afe7ec; + --ui-color-turquoise-400: #3fcdd9; --ui-color-turquoise-500: #36c2cf; - --ui-color-turquoise-600: #2fa6b1; + --ui-color-turquoise-600: #2b9ba5; --ui-color-turquoise-700: #20747c; --ui-color-turquoise-main: var(--ui-color-turquoise-500); - --ui-color-yellow-100: #fff8db; - --ui-color-yellow-200: #ffefad; - --ui-color-yellow-300: #ffe47a; - --ui-color-yellow-400: #ffd84d; - --ui-color-yellow-500: #f3c614; - --ui-color-yellow-600: #d2a500; - --ui-color-yellow-700: #9a7700; + --ui-color-yellow-100: #fff8f1; + --ui-color-yellow-200: #fff1e2; + --ui-color-yellow-300: #ffe2c2; + --ui-color-yellow-400: #ffc584; + --ui-color-yellow-500: #ff9f33; + --ui-color-yellow-600: #ce8029; + --ui-color-yellow-700: #9d611f; --ui-color-yellow-main: var(--ui-color-yellow-500); - --ui-color-purple-100: #f4ecff; - --ui-color-purple-200: #e4d1ff; - --ui-color-purple-300: #d0b0ff; - --ui-color-purple-400: #bb8eff; - --ui-color-purple-500: #9b63f6; - --ui-color-purple-600: #7f42db; - --ui-color-purple-700: #5f2ca8; + --ui-color-purple-100: #faf8ff; + --ui-color-purple-200: #f6f1ff; + --ui-color-purple-300: #e2d4ff; + --ui-color-purple-400: #b390ff; + --ui-color-purple-500: #a074ff; + --ui-color-purple-600: #926ae8; + --ui-color-purple-700: #7252b5; --ui-color-purple-main: var(--ui-color-purple-500); - --ui-color-blue-100: #e8f2ff; - --ui-color-blue-200: #cfe4ff; - --ui-color-blue-300: #9dc8ff; - --ui-color-blue-400: #6aacff; - --ui-color-blue-500: #3c8cff; - --ui-color-blue-600: #226fe0; - --ui-color-blue-700: #1650aa; + --ui-color-blue-100: #eff7ff; + --ui-color-blue-200: #dfefff; + --ui-color-blue-300: #b8e0ff; + --ui-color-blue-400: #78c7ff; + --ui-color-blue-500: #4cb8ff; + --ui-color-blue-600: #0693f1; + --ui-color-blue-700: #0076ce; --ui-color-blue-main: var(--ui-color-blue-500); - --ui-color-red-100: #ffe8e8; - --ui-color-red-200: #ffc9c9; - --ui-color-red-300: #ffa3a3; - --ui-color-red-400: #ff7a7a; - --ui-color-red-500: #f25555; - --ui-color-red-600: #cc3f3f; + --ui-color-red-100: #feefef; + --ui-color-red-200: #fdc7c7; + --ui-color-red-300: #ff97a0; + --ui-color-red-400: #f15d64; + --ui-color-red-500: #ef4149; + --ui-color-red-600: #bc292e; --ui-color-red-main: var(--ui-color-red-500); - --ui-color-green-100: #e8fbef; - --ui-color-green-200: #c6f2d5; - --ui-color-green-300: #95e3b2; - --ui-color-green-400: #62d48d; - --ui-color-green-500: #33bf68; - --ui-color-green-600: #23994f; + --ui-color-green-100: #e0f8e3; + --ui-color-green-200: #cbf1cd; + --ui-color-green-300: #b0ea90; + --ui-color-green-400: #90e05a; + --ui-color-green-500: #63ce29; + --ui-color-green-600: #3ca80c; --ui-color-green-main: var(--ui-color-green-500); --ui-color-grey-100: #ffffff; --ui-color-grey-200: #fbfcfd; --ui-color-grey-300: #f6f8fa; --ui-color-grey-400: #eaeff3; - --ui-color-grey-500: #d0d7de; - --ui-color-grey-600: #c0c8d0; + --ui-color-grey-500: #d9dfe5; + --ui-color-grey-600: #cbd2d8; --ui-color-grey-700: #a7b1bb; - --ui-color-grey-800: #7f8a96; + --ui-color-grey-800: #6e7781; --ui-color-grey-900: #57606a; --ui-color-grey-1000: #24292f; --ui-color-primary-100: var(--ui-color-turquoise-100); @@ -107,10 +136,10 @@ --ui-spacing-xl: 16px; --ui-font-size-2xs: 10px; --ui-font-size-xs: 12px; - --ui-font-size-sm: 14px; - --ui-font-size-base: 16px; - --ui-font-size-lg: 18px; - --ui-font-size-xl: 20px; + --ui-font-size-sm: 13px; + --ui-font-size-base: 14px; + --ui-font-size-lg: 15px; + --ui-font-size-xl: 16px; --ui-font-size-2xl: 20px; --ui-font-main: Inter, diff --git a/ui/prototype/src/utils/format.ts b/ui/prototype/src/utils/format.ts new file mode 100644 index 0000000000..189a4ae329 --- /dev/null +++ b/ui/prototype/src/utils/format.ts @@ -0,0 +1,9 @@ +export function humanizeCount(value: number): string { + if (value >= 10000) return `${Math.round(value / 1000)}k` + if (value >= 1000) return `${(value / 1000).toFixed(1)}k` + return String(value) +} + +export function pluralize(value: number, word: string): string { + return `${value} ${word}${value === 1 ? '' : 's'}` +} diff --git a/ui/skills/prototype-maintenance/SKILL.md b/ui/skills/prototype-maintenance/SKILL.md deleted file mode 100644 index 03faeed09d..0000000000 --- a/ui/skills/prototype-maintenance/SKILL.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -name: prototype-maintenance -description: Use when creating, updating, or validating Builder UI prototypes under ui/prototype from Pencil design changes. Applies when the task mentions prototype, ui/prototype, Pencil page changes, syncing a .pen page to a runnable preview, or keeping the prototype aligned with the real Builder frontend. ---- - -# Prototype Maintenance - -Use this skill when maintaining `ui/prototype` for Builder design work. - -## Goal - -`ui/prototype` is a runnable product prototype, not an isolated static demo. It should reflect the current Pencil design change while staying aligned with the real Builder frontend structure and behavior. - -## Required Workflow - -1. Start from the real frontend. - - Inspect the corresponding implementation in the current Builder frontend before editing prototype code. - - Reuse the same page structure, routing model, component boundaries, styling approach, and interaction logic where practical. - - Do not invent a standalone demo architecture. - -2. Ensure the target prototype surface exists. - - If `ui/prototype` does not have the page or UI being changed, initialize it from the current real frontend structure. - - If it exists but has drifted from the real frontend organization, align the structure first, then apply the design change. - -3. Apply only the current design change. - - Sync the latest relevant Pencil page change into `ui/prototype`. - - If only `tutorial.pen` changed, only override the tutorials surface. - - Other pages, routes, and features must continue to use or mirror the original real frontend behavior and remain accessible. - -4. Keep edits scoped. - - Write prototype-related changes only under `ui/` unless the user explicitly authorizes a broader scope. - - Do not modify `spx-gui/` or other non-`ui/` directories by default. - -5. Make the preview runnable. - - `ui/prototype` must be able to start a frontend preview environment from inside that directory. - - The preview should behave like Builder's existing frontend preview environment, with only the intended prototype surface changed. - -## Validation - -Before claiming completion, run the relevant prototype checks. - -At minimum, verify: - -- The preview environment starts. -- The changed page is accessible. -- Important unchanged pages still load. -- Navigation, search, course-card navigation, project pages, and other key flows still work when relevant. -- The changed page visually matches the latest Pencil design. - -If preview behavior is broken, debug it before finishing. The target state is: real Builder frontend preview behavior plus local prototype overrides for the changed design surface. - -## Output Expectations - -When reporting the result, include: - -- Which Pencil page or design surface was synced. -- Which real frontend files or structures were used as reference. -- Which `ui/` files changed. -- Which validation steps were run, and any remaining gaps.