@givolt/i3-composer-agent 2.3.1 → 2.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude/commands/i3-prototype.md +5 -7
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/plugin.json +1 -1
  5. package/.codex-plugin/plugin.json +1 -1
  6. package/.gemini/commands/i3-prototype.toml +5 -7
  7. package/README.md +16 -7
  8. package/bin/agent-targets.json +4 -2
  9. package/bin/i3-composer-agent.cjs +74 -7
  10. package/bin/install.cjs +16 -1
  11. package/bin/native-layout.cjs +1 -1
  12. package/bin/native-migration.cjs +27 -3
  13. package/bin/tool-selector.cjs +24 -7
  14. package/bin/wizard.cjs +89 -15
  15. package/commands/i3-prototype.toml +5 -7
  16. package/docs/codebuddy-setup.md +91 -3
  17. package/docs/codex-setup.md +6 -0
  18. package/docs/other-hosts.md +1 -1
  19. package/docs/ui-prototype-native/MAIN-SESSION-REPORT.md +66 -0
  20. package/docs/ui-prototype-native/USAGE.md +93 -0
  21. package/docs/workbuddy-setup.md +6 -0
  22. package/evals/cases/i3-frontend-ui-engineering.json +16 -0
  23. package/evals/cases/i3-ui-prototype.json +109 -0
  24. package/evals/cases/i3-using-agent-skills.json +28 -0
  25. package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css +3 -0
  26. package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js +8 -0
  27. package/evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json +67 -0
  28. package/evals/fixtures/i3-ui-prototype/admin-list/index.html +89 -0
  29. package/evals/fixtures/i3-ui-prototype/admin-list/readme.md +52 -0
  30. package/evals/fixtures/i3-ui-prototype/admin-list/template.json +8 -0
  31. package/evals/zh-CN/cases/i3-frontend-ui-engineering.json +17 -0
  32. package/evals/zh-CN/cases/i3-ui-prototype.json +115 -0
  33. package/evals/zh-CN/cases/i3-using-agent-skills.json +30 -0
  34. package/hooks/codebuddy-upstream.cjs +93 -0
  35. package/hooks/i3-prototype.cjs +12 -0
  36. package/hooks/manage.cjs +162 -0
  37. package/package.json +61 -1
  38. package/plugin.json +1 -1
  39. package/scripts/design-provider-test.js +7 -0
  40. package/scripts/enterprise-template-test.js +27 -1
  41. package/scripts/lib/prototype-fixture.cjs +17 -0
  42. package/scripts/prototype-cache-test.js +55 -0
  43. package/scripts/prototype-generation-entry-test.js +6 -0
  44. package/scripts/prototype-generation-evidence-test.js +10 -0
  45. package/scripts/prototype-generation-native-test.js +78 -0
  46. package/scripts/prototype-generation-test.js +99 -0
  47. package/scripts/prototype-inventory-test.js +43 -0
  48. package/scripts/prototype-locations-test.js +79 -0
  49. package/scripts/prototype-project-test.js +183 -0
  50. package/scripts/ui-prototype-codebuddy-hook-test.js +106 -0
  51. package/scripts/ui-prototype-content-test.js +123 -0
  52. package/scripts/ui-prototype-dependencies-test.js +213 -0
  53. package/scripts/ui-prototype-devtools-test.js +53 -0
  54. package/scripts/ui-prototype-execution-evidence-test.js +62 -0
  55. package/scripts/ui-prototype-execution-gates-test.js +66 -0
  56. package/scripts/ui-prototype-execution-policy-test.js +42 -0
  57. package/scripts/ui-prototype-execution-runtime-test.js +27 -0
  58. package/scripts/ui-prototype-ide-source-test.js +73 -0
  59. package/scripts/ui-prototype-native-test.js +504 -0
  60. package/scripts/ui-prototype-registration-test.js +53 -0
  61. package/scripts/ui-prototype-template-test.js +261 -0
  62. package/scripts/ui-prototype-test.js +10 -0
  63. package/scripts/ui-prototype-transcript-auto-test.js +67 -0
  64. package/skills/i3-frontend-ui-engineering/SKILL.md +1 -1
  65. package/skills/i3-incremental-implementation/SKILL.md +1 -1
  66. package/skills/i3-ui-prototype/SKILL.md +158 -19
  67. package/skills/i3-ui-prototype/assets/capabilities/generation.json +833 -0
  68. package/skills/i3-ui-prototype/assets/capabilities/management.json +294 -0
  69. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/acceptance.json +51 -0
  70. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/component-map.json +13 -0
  71. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/generation-component-map.json +300 -0
  72. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/index.html +11 -0
  73. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package-lock.json +2298 -0
  74. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package.json +22 -0
  75. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/public/prototype-config.json +6 -0
  76. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/App.vue +136 -0
  77. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/components/DeviceEditor.vue +111 -0
  78. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/config.ts +19 -0
  79. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/content.ts +28 -0
  80. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/devices.ts +50 -0
  81. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/theme.ts +25 -0
  82. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/main.ts +18 -0
  83. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/style.css +50 -0
  84. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/useDevices.ts +66 -0
  85. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/devices.test.ts +44 -0
  86. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/theme.test.ts +26 -0
  87. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tsconfig.json +15 -0
  88. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/vite.config.ts +4 -0
  89. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/profile.json +174 -0
  90. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/readme.md +61 -0
  91. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/index.html +11 -0
  92. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package-lock.json +2298 -0
  93. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package.json +22 -0
  94. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/public/prototype-config.json +21 -0
  95. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +62 -0
  96. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/PrototypeShell.vue +59 -0
  97. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +23 -0
  98. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/presentation.ts +29 -0
  99. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/theme.ts +25 -0
  100. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/main.ts +18 -0
  101. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +26 -0
  102. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/presentation.test.ts +23 -0
  103. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/theme.test.ts +26 -0
  104. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tsconfig.json +15 -0
  105. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/vite.config.ts +4 -0
  106. package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +13 -0
  107. package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +739 -0
  108. package/skills/i3-ui-prototype/assets/optional-skills/sources.json +27 -0
  109. package/skills/i3-ui-prototype/assets/templates/catalog.js +149 -0
  110. package/skills/i3-ui-prototype/assets/templates/catalog.json +2 -1
  111. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/capability-definition.json +294 -0
  112. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/component-inventory.json +707 -0
  113. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/generation.json +49 -0
  114. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/index.html +507 -2
  115. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline-build.json +265 -0
  116. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline.js +8888 -0
  117. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template-inventory.json +28791 -0
  118. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template.json +9 -0
  119. package/skills/i3-ui-prototype/assets/templates/gallery/index.html +5 -5
  120. package/skills/i3-ui-prototype/assets/templates/generation-options.js +42 -0
  121. package/skills/i3-ui-prototype/assets/templates/index.html +3 -0
  122. package/skills/i3-ui-prototype/assets/templates/presentation.html +17 -0
  123. package/skills/i3-ui-prototype/references/artifact-contract.md +26 -1
  124. package/skills/i3-ui-prototype/references/content-template.md +81 -0
  125. package/skills/i3-ui-prototype/references/enterprise-admin.md +6 -0
  126. package/skills/i3-ui-prototype/references/entry-contract.md +71 -0
  127. package/skills/i3-ui-prototype/references/execution-evidence.md +66 -0
  128. package/skills/i3-ui-prototype/references/expert-adapter.md +33 -5
  129. package/skills/i3-ui-prototype/references/framework-contract.md +76 -0
  130. package/skills/i3-ui-prototype/references/generation-contract.md +122 -0
  131. package/skills/i3-ui-prototype/references/interaction-acceptance.md +169 -0
  132. package/skills/i3-ui-prototype/references/legacy-native.md +58 -0
  133. package/skills/i3-ui-prototype/references/optional-skills.md +63 -0
  134. package/skills/i3-ui-prototype/references/project-layout.md +92 -0
  135. package/skills/i3-ui-prototype/references/template-adapters.md +7 -1
  136. package/skills/i3-ui-prototype/references/workflow-contract.md +62 -4
  137. package/skills/i3-ui-prototype/scripts/adapters/easel-html.cjs +1 -1
  138. package/skills/i3-ui-prototype/scripts/browser-workspace.cjs +72 -0
  139. package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +75 -0
  140. package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +157 -0
  141. package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +69 -0
  142. package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +123 -0
  143. package/skills/i3-ui-prototype/scripts/content-template.cjs +98 -0
  144. package/skills/i3-ui-prototype/scripts/enterprise-template.cjs +13 -6
  145. package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +43 -0
  146. package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +146 -0
  147. package/skills/i3-ui-prototype/scripts/execution-gates.cjs +147 -0
  148. package/skills/i3-ui-prototype/scripts/execution-policy.cjs +41 -0
  149. package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +60 -0
  150. package/skills/i3-ui-prototype/scripts/generation-assets.cjs +18 -0
  151. package/skills/i3-ui-prototype/scripts/generation-contract.cjs +214 -0
  152. package/skills/i3-ui-prototype/scripts/generation-evidence.cjs +50 -0
  153. package/skills/i3-ui-prototype/scripts/generation-replan.cjs +34 -0
  154. package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +16 -0
  155. package/skills/i3-ui-prototype/scripts/manage-baseline.cjs +1 -1
  156. package/skills/i3-ui-prototype/scripts/native-browser.cjs +785 -0
  157. package/skills/i3-ui-prototype/scripts/native-build.cjs +91 -0
  158. package/skills/i3-ui-prototype/scripts/native-delivery.cjs +103 -0
  159. package/skills/i3-ui-prototype/scripts/native-devtools.cjs +92 -0
  160. package/skills/i3-ui-prototype/scripts/native-files.cjs +35 -0
  161. package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +292 -0
  162. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +50 -0
  163. package/skills/i3-ui-prototype/scripts/optional-skill-install.cjs +203 -0
  164. package/skills/i3-ui-prototype/scripts/project-registry.cjs +126 -0
  165. package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +81 -0
  166. package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +87 -0
  167. package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +31 -13
  168. package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +69 -0
  169. package/skills/i3-ui-prototype/scripts/sync-template.cjs +19 -0
  170. package/skills/i3-ui-prototype/scripts/template-contract.cjs +407 -0
  171. package/skills/i3-ui-prototype/scripts/template-inventory.cjs +78 -0
  172. package/skills/i3-ui-prototype/scripts/validate-result.cjs +117 -0
  173. package/skills/i3-using-agent-skills/SKILL.md +2 -5
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "business-antdv-prototype",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "packageManager": "npm@11.12.1",
7
+ "engines": { "node": ">=22.18.0", "npm": ">=10" },
8
+ "scripts": {
9
+ "dev": "vite --host 127.0.0.1",
10
+ "typecheck": "vue-tsc --noEmit",
11
+ "test": "node --experimental-strip-types --test tests/*.test.ts",
12
+ "build": "vue-tsc --noEmit && vite build",
13
+ "preview": "vite preview --host 127.0.0.1"
14
+ },
15
+ "dependencies": { "antdv-next": "1.5.3", "vue": "3.5.42" },
16
+ "devDependencies": {
17
+ "@vitejs/plugin-vue": "6.0.4",
18
+ "typescript": "5.9.3",
19
+ "vite": "7.3.1",
20
+ "vue-tsc": "3.3.13"
21
+ }
22
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "instanceId": "business-preview",
3
+ "storageNamespace": "prototype:business-preview",
4
+ "mockVersion": "business-v1",
5
+ "title": "业务原型",
6
+ "pages": [
7
+ {
8
+ "id": "business",
9
+ "title": "业务页面"
10
+ }
11
+ ],
12
+ "theme": {
13
+ "defaultTheme": "light",
14
+ "presets": [
15
+ "light",
16
+ "dark",
17
+ "fresh",
18
+ "tech"
19
+ ]
20
+ }
21
+ }
@@ -0,0 +1,62 @@
1
+ <script setup lang="ts">
2
+ import { computed, onBeforeUnmount, ref, watch } from 'vue'
3
+ import { Alert, ConfigProvider, Select, Tabs, TabPane, theme as antdTheme } from 'antdv-next'
4
+ import zhCN from 'antdv-next/locale/zh_CN'
5
+ import type { PrototypeConfig } from './config'
6
+ import { THEME_IDS, THEME_PRESETS, loadThemePreference, safeThemeStorage, saveThemePreference, themeStorageKey } from './domain/theme'
7
+ import type { ThemeId } from './domain/theme'
8
+ import PrototypeShell from './PrototypeShell.vue'
9
+ import { presentationTokens, resolvePresentation } from './domain/presentation'
10
+ const props = defineProps<{ config: PrototypeConfig }>()
11
+ const storage = safeThemeStorage(() => window.localStorage)
12
+ const key = themeStorageKey(props.config.storageNamespace)
13
+ const selected = ref<ThemeId>(loadThemePreference(storage, key))
14
+ const activePage = ref(props.config.pages[0]?.id)
15
+ const warning = ref('')
16
+ const options = THEME_IDS.map(value => ({ value, label: THEME_PRESETS[value].label }))
17
+ const media = matchMedia('(prefers-reduced-motion: reduce)')
18
+ const reduced = ref(media.matches)
19
+ const motionChanged = (event: MediaQueryListEvent) => { reduced.value = event.matches }
20
+ media.addEventListener('change', motionChanged)
21
+ onBeforeUnmount(() => media.removeEventListener('change', motionChanged))
22
+ const preset = computed(() => THEME_PRESETS[selected.value])
23
+ const recipe = computed(() => props.config.presentation ? presentationTokens(resolvePresentation(props.config.presentation)) : undefined)
24
+ const variables = computed(() => ({ '--prototype-bg': preset.value.bg, '--prototype-surface': preset.value.surface, '--prototype-text': preset.value.text, '--prototype-border': preset.value.border, '--prototype-radius': `${recipe.value?.borderRadius ?? 10}px` }))
25
+ const tokens = computed(() => ({
26
+ algorithm: preset.value.mode === 'dark' ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
27
+ token: { colorPrimary: preset.value.primary, colorBgBase: preset.value.bg, colorBgContainer: preset.value.surface,
28
+ colorBgElevated: preset.value.surface, colorText: preset.value.text, colorTextSecondary: preset.value.muted,
29
+ colorBorder: preset.value.border, colorTextLightSolid: preset.value.onPrimary, motion: !reduced.value,
30
+ ...(recipe.value ? { borderRadius: recipe.value.borderRadius, controlHeight: recipe.value.controlHeight } : {}) },
31
+ }))
32
+ function choose(value: unknown) {
33
+ if (typeof value !== 'string' || !THEME_IDS.includes(value as ThemeId)) return
34
+ selected.value = value as ThemeId
35
+ warning.value = saveThemePreference(storage, key, selected.value) ? '' : '主题偏好无法保存,本页仍可切换。'
36
+ }
37
+ watch(preset, value => {
38
+ document.documentElement.dataset.prototypeTheme = selected.value
39
+ document.documentElement.style.colorScheme = value.mode
40
+ }, { immediate: true })
41
+ </script>
42
+ <template>
43
+ <ConfigProvider :locale="zhCN" :theme="tokens" :component-size="recipe?.componentSize">
44
+ <div v-if="config.presentation" :style="variables">
45
+ <PrototypeShell :config="config" :menu-theme="preset.mode">
46
+ <template #appearance><div class="theme-picker"><label for="prototype-theme">主题</label><Select id="prototype-theme" aria-label="主题" :value="selected" :options="options" @change="choose" /></div></template>
47
+ <template #default="{ page }"><Alert v-if="warning" type="warning" :title="warning" show-icon /><section :aria-label="page.title"><h2>{{ page.title }}</h2><Alert type="warning" title="业务页面待实现" description="按固定的页面模式、字段、操作与验收合同实现此页。" show-icon /></section></template>
48
+ </PrototypeShell>
49
+ </div>
50
+ <div v-else class="prototype-shell" :style="{ background: preset.bg, color: preset.text }">
51
+ <header><h1>{{ config.title }}</h1><div class="theme-picker"><label for="prototype-theme">主题</label><Select id="prototype-theme" aria-label="主题" :value="selected" :options="options" @change="choose" /></div></header>
52
+ <main :style="{ background: preset.surface, borderColor: preset.border }">
53
+ <Alert v-if="warning" type="warning" :title="warning" show-icon />
54
+ <Tabs v-model:active-key="activePage">
55
+ <TabPane v-for="page in config.pages" :key="page.id" :tab="page.title">
56
+ <section :aria-label="page.title"><h2>{{ page.title }}</h2><Alert type="warning" title="业务页面待实现" description="按固定需求、完整内容模板和通用能力清单实现此页,再进行交互验收。" show-icon /></section>
57
+ </TabPane>
58
+ </Tabs>
59
+ </main>
60
+ </div>
61
+ </ConfigProvider>
62
+ </template>
@@ -0,0 +1,59 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
3
+ import { Button, Drawer, Layout, LayoutContent, LayoutHeader, LayoutSider, Menu, Result } from 'antdv-next'
4
+ import type { MenuProps } from 'antdv-next'
5
+ import type { PrototypeConfig } from './config'
6
+ import { navigationMenus, pageFromHash, presentationTokens, resolvePresentation } from './domain/presentation'
7
+ import type { NavigationItem } from './domain/presentation'
8
+ const props = defineProps<{ config: PrototypeConfig; menuTheme?: 'light' | 'dark' }>()
9
+ const presentation = computed(() => resolvePresentation(props.config.presentation))
10
+ const recipe = computed(() => presentationTokens(presentation.value))
11
+ const active = ref(pageFromHash(location.hash, props.config.pages))
12
+ const current = computed(() => props.config.pages.find(page => page.id === active.value))
13
+ const items = computed(() => props.config.navigation || props.config.pages.map(page => ({ key: page.id, label: page.title })))
14
+ const menus = computed(() => navigationMenus(items.value, presentation.value.layout, active.value))
15
+ const collapsed = ref(false), mobileOpen = ref(false), focusAfterClose = ref(false)
16
+ const opened = ref<string[]>([])
17
+ function ancestors(nodes: NavigationItem[], target: string | undefined, parents: string[] = []): string[] | undefined { for (const node of nodes) { if (node.key === target) return parents; if (node.children) { const found = ancestors(node.children, target, [...parents, node.key]); if (found) return found } } }
18
+ watch(active, value => { opened.value = [...new Set([...opened.value, ...(ancestors(items.value, value) || [])])] }, { immediate: true })
19
+ const media = matchMedia('(max-width: 767px)'), narrow = ref(media.matches)
20
+ const onResize = (event: MediaQueryListEvent) => { narrow.value = event.matches; if (!event.matches) mobileOpen.value = false }
21
+ const onHash = () => { active.value = pageFromHash(location.hash, props.config.pages) }
22
+ media.addEventListener('change', onResize); window.addEventListener('hashchange', onHash); window.addEventListener('popstate', onHash)
23
+ onBeforeUnmount(() => { media.removeEventListener('change', onResize); window.removeEventListener('hashchange', onHash); window.removeEventListener('popstate', onHash) })
24
+ function navigate(event: { key: string | number }) {
25
+ const id = String(event.key)
26
+ if (!props.config.pages.some(page => page.id === id)) return
27
+ history.pushState(null, '', `#/${encodeURIComponent(id)}`); active.value = id
28
+ if (mobileOpen.value) { focusAfterClose.value = true; mobileOpen.value = false } else nextTick(focusMain)
29
+ }
30
+ function afterMenuChange(open: boolean) { if (!open && focusAfterClose.value) { focusAfterClose.value = false; nextTick(focusMain) } }
31
+ function focusMain() { document.getElementById('prototype-main')?.focus() }
32
+ function nativeItems(nodes: NavigationItem[]): NonNullable<MenuProps['items']> { return nodes.map(node => node.children ? { key: node.key, label: node.label, children: nativeItems(node.children) } : { key: node.key, label: node.label }) }
33
+ const side = computed(() => ['sidebar','mixed'].includes(presentation.value.layout))
34
+ const top = computed(() => ['topbar','mixed'].includes(presentation.value.layout))
35
+ </script>
36
+ <template>
37
+ <Layout class="generation-shell" :class="[`layout-${presentation.layout}`, `style-${presentation.style}`, `density-${presentation.density}`]" :style="{ '--prototype-spacing': `${recipe.spacing}px` }" :data-layout="presentation.layout" :data-style="presentation.style" :data-density="presentation.density">
38
+ <a class="skip-link" href="#prototype-main" @click.prevent="focusMain">跳到页面内容</a>
39
+ <LayoutHeader class="generation-header">
40
+ <div class="generation-brand"><Button v-if="narrow" aria-label="打开导航" :aria-expanded="mobileOpen" @click="mobileOpen = true">菜单</Button><strong>{{ config.title }}</strong></div>
41
+ <slot name="appearance" />
42
+ </LayoutHeader>
43
+ <nav v-if="top && !narrow" aria-label="顶部导航" class="generation-topnav"><Menu :theme="menuTheme || 'light'" mode="horizontal" :items="nativeItems(menus.top)" :selected-keys="menus.topSelected ? [menus.topSelected] : []" @click="navigate" /></nav>
44
+ <Layout class="generation-body">
45
+ <LayoutSider v-if="side && !narrow" class="generation-sidebar" :width="224" :collapsed-width="72" :collapsed="collapsed" :theme="menuTheme || 'light'">
46
+ <Button block :aria-expanded="!collapsed" :aria-label="collapsed ? '展开侧栏' : '收起侧栏'" @click="collapsed = !collapsed">{{ collapsed ? '展开' : '收起侧栏' }}</Button>
47
+ <nav aria-label="侧栏导航"><Menu :theme="menuTheme || 'light'" v-model:open-keys="opened" mode="inline" :inline-collapsed="collapsed" :items="nativeItems(menus.side)" :selected-keys="active ? [active] : []" @click="navigate" /></nav>
48
+ </LayoutSider>
49
+ <LayoutContent id="prototype-main" class="generation-content" tabindex="-1">
50
+ <nav v-if="presentation.layout === 'focused' && !narrow && config.pages.length > 1" aria-label="工作区页面"><Menu :theme="menuTheme || 'light'" mode="horizontal" :items="nativeItems(items)" :selected-keys="active ? [active] : []" @click="navigate" /></nav>
51
+ <slot v-if="current" :page="current" />
52
+ <Result v-else status="404" title="页面不存在" sub-title="当前地址不在本原型的页面范围内。"><template #extra><Button @click="navigate({ key: config.pages[0].id })">返回首页</Button></template></Result>
53
+ </LayoutContent>
54
+ </Layout>
55
+ <Drawer :open="mobileOpen" title="导航" placement="left" :width="288" :mask="{ enabled: true, blur: true }" destroy-on-hidden @after-open-change="afterMenuChange" @close="mobileOpen = false">
56
+ <nav aria-label="移动导航"><Menu :theme="menuTheme || 'light'" v-model:open-keys="opened" mode="inline" :items="nativeItems(items)" :selected-keys="active ? [active] : []" @click="navigate" /></nav>
57
+ </Drawer>
58
+ </Layout>
59
+ </template>
@@ -0,0 +1,23 @@
1
+ import { resolveThemeConfig } from './domain/theme'
2
+ import type { ThemeBinding } from './domain/theme'
3
+ import { resolvePresentation } from './domain/presentation'
4
+ import type { NavigationItem, Presentation } from './domain/presentation'
5
+ export interface PrototypeConfig { instanceId: string; storageNamespace: string; mockVersion: string; title?: string; pages: { id: string; title: string }[]; theme?: ThemeBinding; presentation?: Presentation; navigation?: NavigationItem[]; generation?: { pages: unknown[] } }
6
+ export interface PrototypeMeta extends PrototypeConfig { candidateId: string }
7
+ declare global { interface Window { __PROTOTYPE_META__: PrototypeMeta } }
8
+
9
+ export async function loadConfig(): Promise<PrototypeConfig> {
10
+ const response = await fetch(`${import.meta.env.BASE_URL}prototype-config.json`, { cache: 'no-store' })
11
+ if (!response.ok) throw new Error('无法读取原型配置,请检查当前预览构建。')
12
+ const config: unknown = await response.json()
13
+ if (!config || typeof config !== 'object') throw new Error('原型配置无效。')
14
+ const value = config as Record<string, unknown>
15
+ for (const key of ['instanceId', 'storageNamespace', 'mockVersion']) {
16
+ if (typeof value[key] !== 'string' || !(value[key] as string).trim()) throw new Error(`原型配置缺少 ${key}。`)
17
+ }
18
+ resolveThemeConfig(value.theme)
19
+ if (value.presentation !== undefined) resolvePresentation(value.presentation)
20
+ if (!Array.isArray(value.pages) || !value.pages.length || value.pages.some(page => !page || typeof page.id !== 'string' || !page.id || typeof page.title !== 'string')) throw new Error('原型页面配置无效。')
21
+ if (new Set(value.pages.map(page => page.id)).size !== value.pages.length) throw new Error('原型页面标识重复。')
22
+ return value as unknown as PrototypeConfig
23
+ }
@@ -0,0 +1,29 @@
1
+ export interface Presentation { layout: 'sidebar' | 'topbar' | 'mixed' | 'focused'; style: 'business' | 'minimal' | 'soft'; density: 'compact' | 'standard' | 'comfortable' }
2
+ export interface NavigationItem { key: string; label: string; children?: NavigationItem[] }
3
+ export function navigationMenus(items: NavigationItem[], layout: Presentation['layout'], active?: string) {
4
+ const first = (item: NavigationItem): string => item.children?.length ? first(item.children[0]) : item.key
5
+ const contains = (item: NavigationItem): boolean => item.key === active || !!item.children?.some(contains)
6
+ const groups = items.some(item => item.children) ? items : [{ key: '@pages', label: '业务页面', children: items }]
7
+ const current = groups.find(contains) || groups[0]
8
+ return { top: layout === 'mixed' ? groups.map(item => ({ key: first(item), label: item.label })) : items,
9
+ side: layout === 'mixed' ? current?.children || [current] : items,
10
+ topSelected: layout === 'mixed' && current ? first(current) : active }
11
+ }
12
+ export function resolvePresentation(value: unknown): Presentation {
13
+ if (!value || typeof value !== 'object') throw new Error('缺少已固定的页面呈现选择。')
14
+ const item = value as Record<string, unknown>
15
+ for (const [key, choices] of Object.entries({ layout: ['sidebar', 'topbar', 'mixed', 'focused'], style: ['business', 'minimal', 'soft'], density: ['compact', 'standard', 'comfortable'] })) {
16
+ if (!choices.includes(item[key] as string)) throw new Error(`不支持的呈现选择:${key}`)
17
+ }
18
+ return item as unknown as Presentation
19
+ }
20
+ export function presentationTokens(value: Presentation) {
21
+ return { borderRadius: { business: 6, minimal: 4, soft: 10 }[value.style],
22
+ controlHeight: { compact: 28, standard: 32, comfortable: 40 }[value.density],
23
+ spacing: { compact: 12, standard: 20, comfortable: 28 }[value.density],
24
+ componentSize: { compact: 'small', standard: 'middle', comfortable: 'large' }[value.density] as 'small' | 'middle' | 'large' }
25
+ }
26
+ export function pageFromHash(hash: string, pages: { id: string }[]): string | undefined {
27
+ if (!hash || hash === '#/' || hash === '#') return pages[0]?.id
28
+ try { const id = decodeURIComponent(hash.replace(/^#\/?/, '')); return pages.some(page => page.id === id) ? id : undefined } catch { return undefined }
29
+ }
@@ -0,0 +1,25 @@
1
+ export type ThemeId = 'light' | 'dark' | 'fresh' | 'tech'
2
+ export interface ThemeBinding { defaultTheme: 'light'; presets: ThemeId[] }
3
+ export interface ThemeStorage { getItem(key: string): string | null; setItem(key: string, value: string): void }
4
+ export const THEME_IDS: ThemeId[] = ['light', 'dark', 'fresh', 'tech']
5
+ export const THEME_PRESETS = {
6
+ light: { label: '明亮', mode: 'light', primary: '#155bc2', bg: '#f4f7fc', surface: '#ffffff', subtle: '#f8faff', text: '#172b45', muted: '#506174', border: '#b9c8da', accent: '#155bc2', onPrimary: '#ffffff', error: '#b4232c', success: '#166534' },
7
+ dark: { label: '暗黑', mode: 'dark', primary: '#8fb8ff', bg: '#15181d', surface: '#20242b', subtle: '#1b2028', text: '#edf1f7', muted: '#b2bece', border: '#667387', accent: '#a6c7ff', onPrimary: '#101c2c', error: '#ffb4ab', success: '#9bddad' },
8
+ fresh: { label: '清新', mode: 'light', primary: '#0c6a55', bg: '#edf8f4', surface: '#fbfffd', subtle: '#e6f4ed', text: '#173b31', muted: '#4c665c', border: '#a4c4b5', accent: '#0c6a55', onPrimary: '#ffffff', error: '#b4232c', success: '#166534' },
9
+ tech: { label: '科技', mode: 'dark', primary: '#55ddc6', bg: '#07131f', surface: '#102536', subtle: '#0b1c2b', text: '#e1f4f7', muted: '#a0bdca', border: '#5b8294', accent: '#55ddc6', onPrimary: '#071b20', error: '#ffb4ab', success: '#9bddad' },
10
+ } as const
11
+ export function resolveThemeConfig(value: unknown): ThemeBinding | null {
12
+ if (value === undefined) return null
13
+ const config=value as Partial<ThemeBinding> | null
14
+ if (!config || config.defaultTheme !== 'light' || !Array.isArray(config.presets) || config.presets.length !== 4 || new Set(config.presets).size !== 4 || !THEME_IDS.every(id => config.presets!.includes(id))) throw new Error('主题配置必须包含明亮、暗黑、清新、科技且默认明亮。')
15
+ return { defaultTheme: 'light', presets: [...THEME_IDS] }
16
+ }
17
+ export const themeStorageKey = (namespace: string) => `${namespace}:ui-theme:v1`
18
+ export function safeThemeStorage(access: () => ThemeStorage): ThemeStorage | null { try { return access() } catch { return null } }
19
+ export function loadThemePreference(storage: ThemeStorage | null, key: string): ThemeId {
20
+ try { const value=JSON.parse(storage?.getItem(key) ?? 'null'); if (value?.version===1 && THEME_IDS.includes(value.themeId)) return value.themeId } catch { /* Damaged or denied storage uses the explicit default. */ }
21
+ return 'light'
22
+ }
23
+ export function saveThemePreference(storage: ThemeStorage | null, key: string, themeId: ThemeId): boolean {
24
+ try { if (!storage) return false; storage.setItem(key,JSON.stringify({version:1,themeId})); return true } catch { return false }
25
+ }
@@ -0,0 +1,18 @@
1
+ import { createApp } from 'vue'
2
+ import 'antdv-next/dist/reset.css'
3
+ import App from './App.vue'
4
+ import { loadConfig } from './config'
5
+ import './style.css'
6
+
7
+ loadConfig().then(config => {
8
+ window.__PROTOTYPE_META__ = { ...config, candidateId: import.meta.env.VITE_CANDIDATE_ID || 'unverified-development' }
9
+ createApp(App, { config }).mount('#app')
10
+ }).catch(error => {
11
+ const container = document.querySelector('#app')!
12
+ const heading = document.createElement('h1')
13
+ heading.textContent = '原型无法启动'
14
+ const message = document.createElement('p')
15
+ message.textContent = error instanceof Error ? error.message : '配置读取失败。'
16
+ container.setAttribute('role', 'alert')
17
+ container.append(heading, message)
18
+ })
@@ -0,0 +1,26 @@
1
+ * { box-sizing: border-box; }
2
+ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif; }
3
+ .prototype-shell { min-height: 100dvh; padding: 24px; }
4
+ header { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin: 0 auto 20px; max-width: 1440px; }
5
+ h1 { margin: 0; font-size: 24px; }
6
+ h2 { margin-top: 0; font-size: 20px; }
7
+ .theme-picker { display: flex; align-items: center; gap: 8px; }
8
+ .theme-picker .ant-select { min-width: 100px; }
9
+ main { max-width: 1440px; margin: auto; padding: 20px; border: 1px solid; border-radius: 10px; }
10
+ section { min-height: 240px; }
11
+ :focus-visible { outline: 3px solid currentColor; outline-offset: 3px; }
12
+ @media (max-width: 600px) { .prototype-shell { padding: 12px; } main { padding: 12px; } h1 { font-size: 20px; } }
13
+ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; transition-duration: .01ms !important; } }
14
+ .generation-shell { min-height: 100dvh; background: var(--prototype-bg); color: var(--prototype-text); }
15
+ .generation-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; width: 100%; max-width: none; min-height: 64px; height: auto; margin: 0; padding: 12px 24px; line-height: 1.5; background: var(--prototype-surface); color: inherit; border-bottom: 1px solid var(--prototype-border); }
16
+ .generation-brand { display: flex; align-items: center; gap: 12px; font-size: 18px; }
17
+ .generation-body { min-width: 0; background: var(--prototype-bg); }
18
+ .generation-sidebar { padding: 12px 8px; background: var(--prototype-surface) !important; border-right: 1px solid var(--prototype-border); }
19
+ .generation-topnav { background: var(--prototype-surface); }
20
+ .generation-content { min-width: 0; max-width: none; margin: var(--prototype-spacing); padding: var(--prototype-spacing); border: 1px solid var(--prototype-border); border-radius: var(--prototype-radius); background: var(--prototype-surface); color: inherit; }
21
+ .layout-focused .generation-content { width: calc(100% - 2 * var(--prototype-spacing)); max-width: 1200px; margin: var(--prototype-spacing) auto; }
22
+ .style-minimal .generation-content { border-color: transparent; }
23
+ .style-soft .generation-content { box-shadow: 0 4px 20px rgb(0 0 0 / 6%); }
24
+ .skip-link { position: fixed; top: 8px; left: 8px; z-index: 2000; padding: 8px 12px; background: var(--prototype-surface); color: var(--prototype-text); transform: translateY(-200%); }
25
+ .skip-link:focus { transform: none; }
26
+ @media (max-width: 767px) { .generation-header { padding: 12px; } .generation-content { margin: 8px; padding: 12px; } }
@@ -0,0 +1,23 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { resolvePresentation, presentationTokens, pageFromHash, navigationMenus } from '../src/domain/presentation.ts'
4
+ test('every supported presentation combination has a usable native token recipe', () => {
5
+ for (const layout of ['sidebar','topbar','mixed','focused']) for (const style of ['business','minimal','soft']) for (const density of ['compact','standard','comfortable']) {
6
+ const value = resolvePresentation({ layout, style, density }), tokens = presentationTokens(value)
7
+ assert.ok(tokens.controlHeight > 0 && tokens.spacing > 0 && tokens.borderRadius >= 0)
8
+ }
9
+ assert.throws(() => resolvePresentation({ layout: 'lost', style: 'minimal', density: 'compact' }), /不支持/)
10
+ })
11
+ test('deep links retain declared page identity and reject unknown or malformed locations', () => {
12
+ const pages = [{ id: 'assets' }, { id: 'details' }]
13
+ assert.equal(pageFromHash('#/details', pages), 'details')
14
+ assert.equal(pageFromHash('', pages), 'assets')
15
+ assert.equal(pageFromHash('#/missing', pages), undefined)
16
+ assert.equal(pageFromHash('#/%broken', pages), undefined)
17
+ })
18
+ test('mixed navigation separates group entries from the selected group pages', () => {
19
+ const value = navigationMenus([{ key: '@assets', label: '资产', children: [{ key: 'list', label: '台账' }, { key: 'detail', label: '详情' }] }, { key: '@work', label: '作业', children: [{ key: 'jobs', label: '任务' }] }], 'mixed', 'detail')
20
+ assert.deepEqual(value.top.map(item => item.key), ['list','jobs'])
21
+ assert.deepEqual(value.side.map(item => item.key), ['list','detail'])
22
+ assert.equal(value.topSelected, 'list')
23
+ })
@@ -0,0 +1,26 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+ import { THEME_PRESETS, resolveThemeConfig, themeStorageKey, loadThemePreference, saveThemePreference, safeThemeStorage } from '../src/domain/theme.ts'
4
+
5
+ const binding = { defaultTheme: 'light', presets: ['light', 'dark', 'fresh', 'tech'] }
6
+ test('four named presets are explicit while an unbound instance stays legacy', () => {
7
+ assert.equal(resolveThemeConfig(undefined), null)
8
+ assert.deepEqual(resolveThemeConfig(binding), binding)
9
+ assert.deepEqual(Object.values(THEME_PRESETS).map(theme => theme.label), ['明亮', '暗黑', '清新', '科技'])
10
+ assert.deepEqual(Object.values(THEME_PRESETS).map(theme => theme.mode), ['light', 'dark', 'light', 'dark'])
11
+ for (const config of [{ ...binding, defaultTheme: 'dark' }, { ...binding, presets: [1,2,3,4] }, { ...binding, presets: ['light','dark','fresh','fresh'] }]) assert.throws(() => resolveThemeConfig(config))
12
+ })
13
+ test('theme preference is namespaced independently and damaged cache falls back', () => {
14
+ const values=new Map<string,string>(), storage={ getItem:(key:string)=>values.get(key)??null,setItem:(key:string,value:string)=>{values.set(key,value)} }
15
+ const a=themeStorageKey('instance-a'),b=themeStorageKey('instance-b')
16
+ assert.notEqual(a,b);assert.match(a,/:ui-theme:v1$/)
17
+ assert.equal(saveThemePreference(storage,a,'tech'),true)
18
+ assert.equal(loadThemePreference(storage,a),'tech');assert.equal(loadThemePreference(storage,b),'light')
19
+ for (const broken of ['{','null','{"version":1,"themeId":2}','{"version":2,"themeId":"tech"}','{"version":1,"themeId":"unknown"}']) { values.set(a,broken);assert.equal(loadThemePreference(storage,a),'light') }
20
+ })
21
+ test('denied storage never prevents local theme changes or returns a fake saved preference', () => {
22
+ const denied={getItem(){throw Error('denied')},setItem(){throw Error('denied')}}
23
+ assert.equal(safeThemeStorage(()=>{throw Error('getter denied')}),null)
24
+ assert.equal(loadThemePreference(denied,'key'),'light');assert.equal(loadThemePreference(null,'key'),'light')
25
+ assert.equal(saveThemePreference(denied,'key','dark'),false);assert.equal(saveThemePreference(null,'key','dark'),false)
26
+ })
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "skipLibCheck": true,
9
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
10
+ "types": ["vite/client"],
11
+ "allowImportingTsExtensions": true,
12
+ "verbatimModuleSyntax": true
13
+ },
14
+ "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
15
+ }
@@ -0,0 +1,4 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+
4
+ export default defineConfig({ plugins: [vue()], base: './', build: { manifest: true } })
@@ -0,0 +1,13 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "codebuddy-command-v1",
4
+ "nativeValidation": "pending-user",
5
+ "platforms": ["darwin", "linux"],
6
+ "events": ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"],
7
+ "transcriptFormat": "assistant-user-tool-use-jsonl-v1",
8
+ "products": {
9
+ "ide": {"settings": "settings.json", "configuration": "project-overrides-global", "transcriptRoot": "native-hook-transcript-path", "manualTranscriptRoot": true, "nativeReview": "reload-and-verify"},
10
+ "cli": {"settings": "settings.json", "configuration": "merged-project-first-binding", "transcriptRoot": "native-hook-transcript-path", "manualTranscriptRoot": true, "configRootEnvironment": "CODEBUDDY_CONFIG_DIR", "nativeReview": "/hooks"}
11
+ },
12
+ "sources": ["https://www.codebuddy.cn/docs/ide/Features/Hooks", "https://www.codebuddy.cn/docs/cli/hooks", "https://www.codebuddy.cn/docs/cli/env-vars"]
13
+ }