@givolt/i3-composer-agent 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +1 -1
- package/.claude/commands/i3-prototype.md +5 -7
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.gemini/commands/i3-prototype.toml +5 -7
- package/README.md +12 -7
- package/bin/i3-composer-agent.cjs +33 -2
- package/bin/install.cjs +14 -0
- package/bin/native-migration.cjs +27 -3
- package/commands/i3-prototype.toml +5 -7
- package/docs/codebuddy-setup.md +65 -2
- package/docs/codex-setup.md +4 -0
- package/docs/other-hosts.md +1 -1
- package/docs/ui-prototype-native/MAIN-SESSION-REPORT.md +66 -0
- package/docs/ui-prototype-native/USAGE.md +91 -0
- package/docs/workbuddy-setup.md +4 -0
- package/evals/cases/i3-frontend-ui-engineering.json +16 -0
- package/evals/cases/i3-ui-prototype.json +109 -0
- package/evals/cases/i3-using-agent-skills.json +28 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css +3 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js +8 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json +67 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/index.html +89 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/readme.md +52 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/template.json +8 -0
- package/evals/zh-CN/cases/i3-frontend-ui-engineering.json +17 -0
- package/evals/zh-CN/cases/i3-ui-prototype.json +115 -0
- package/evals/zh-CN/cases/i3-using-agent-skills.json +30 -0
- package/package.json +35 -1
- package/plugin.json +1 -1
- package/scripts/design-provider-discovery-test.js +190 -0
- package/scripts/design-provider-test.js +75 -0
- package/scripts/enterprise-template-test.js +67 -1
- package/scripts/lib/prototype-fixture.cjs +17 -0
- package/scripts/prototype-locations-test.js +58 -0
- package/scripts/ui-prototype-content-test.js +123 -0
- package/scripts/ui-prototype-dependencies-test.js +213 -0
- package/scripts/ui-prototype-devtools-test.js +53 -0
- package/scripts/ui-prototype-native-test.js +504 -0
- package/scripts/ui-prototype-template-test.js +261 -0
- package/scripts/ui-prototype-test.js +10 -0
- package/skills/i3-ui-prototype/SKILL.md +142 -23
- package/skills/i3-ui-prototype/assets/capabilities/management.json +294 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/acceptance.json +51 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/component-map.json +13 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/index.html +11 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package-lock.json +2298 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package.json +22 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/public/prototype-config.json +6 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/App.vue +136 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/components/DeviceEditor.vue +111 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/config.ts +19 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/content.ts +28 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/devices.ts +50 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/theme.ts +25 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/main.ts +18 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/style.css +50 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/useDevices.ts +66 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/devices.test.ts +44 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/theme.test.ts +26 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tsconfig.json +15 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/vite.config.ts +4 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/profile.json +174 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/readme.md +61 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/index.html +11 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package-lock.json +2298 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package.json +22 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/public/prototype-config.json +21 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +51 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +18 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/theme.ts +25 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/main.ts +18 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +13 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/theme.test.ts +26 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tsconfig.json +15 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/vite.config.ts +4 -0
- package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +737 -0
- package/skills/i3-ui-prototype/assets/optional-skills/sources.json +27 -0
- package/skills/i3-ui-prototype/assets/templates/catalog.js +149 -0
- package/skills/i3-ui-prototype/assets/templates/catalog.json +3 -2
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/capability-definition.json +294 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/design-system.md +3 -3
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/index.html +507 -2
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/manifest.json +1 -1
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline-build.json +265 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline.js +8888 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template.json +9 -0
- package/skills/i3-ui-prototype/assets/templates/gallery/index.html +6 -6
- package/skills/i3-ui-prototype/assets/templates/index.html +3 -0
- package/skills/i3-ui-prototype/assets/templates/serve-gallery.cjs +7 -0
- package/skills/i3-ui-prototype/references/artifact-contract.md +25 -0
- package/skills/i3-ui-prototype/references/content-template.md +76 -0
- package/skills/i3-ui-prototype/references/enterprise-admin.md +9 -3
- package/skills/i3-ui-prototype/references/entry-contract.md +71 -0
- package/skills/i3-ui-prototype/references/expert-adapter.md +39 -5
- package/skills/i3-ui-prototype/references/framework-contract.md +76 -0
- package/skills/i3-ui-prototype/references/interaction-acceptance.md +165 -0
- package/skills/i3-ui-prototype/references/legacy-native.md +58 -0
- package/skills/i3-ui-prototype/references/optional-skills.md +59 -0
- package/skills/i3-ui-prototype/references/template-adapters.md +1 -1
- package/skills/i3-ui-prototype/references/workflow-contract.md +58 -4
- package/skills/i3-ui-prototype/scripts/adapters/easel-html.cjs +3 -3
- package/skills/i3-ui-prototype/scripts/content-template.cjs +98 -0
- package/skills/i3-ui-prototype/scripts/design-provider-discovery.cjs +128 -0
- package/skills/i3-ui-prototype/scripts/enterprise-template.cjs +30 -14
- package/skills/i3-ui-prototype/scripts/manage-baseline.cjs +1 -1
- package/skills/i3-ui-prototype/scripts/native-browser.cjs +785 -0
- package/skills/i3-ui-prototype/scripts/native-build.cjs +91 -0
- package/skills/i3-ui-prototype/scripts/native-delivery.cjs +91 -0
- package/skills/i3-ui-prototype/scripts/native-devtools.cjs +91 -0
- package/skills/i3-ui-prototype/scripts/native-files.cjs +35 -0
- package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +244 -0
- package/skills/i3-ui-prototype/scripts/native-pilot.cjs +34 -0
- package/skills/i3-ui-prototype/scripts/optional-skill-install.cjs +203 -0
- package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +81 -0
- package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +50 -26
- package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +65 -0
- package/skills/i3-ui-prototype/scripts/sync-template.cjs +19 -0
- package/skills/i3-ui-prototype/scripts/template-contract.cjs +398 -0
- package/skills/i3-ui-prototype/scripts/validate-result.cjs +117 -0
- 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
|
+
}
|
package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/public/prototype-config.json
ADDED
|
@@ -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,51 @@
|
|
|
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
|
+
const props = defineProps<{ config: PrototypeConfig }>()
|
|
9
|
+
const storage = safeThemeStorage(() => window.localStorage)
|
|
10
|
+
const key = themeStorageKey(props.config.storageNamespace)
|
|
11
|
+
const selected = ref<ThemeId>(loadThemePreference(storage, key))
|
|
12
|
+
const activePage = ref(props.config.pages[0]?.id)
|
|
13
|
+
const warning = ref('')
|
|
14
|
+
const options = THEME_IDS.map(value => ({ value, label: THEME_PRESETS[value].label }))
|
|
15
|
+
const media = matchMedia('(prefers-reduced-motion: reduce)')
|
|
16
|
+
const reduced = ref(media.matches)
|
|
17
|
+
const motionChanged = (event: MediaQueryListEvent) => { reduced.value = event.matches }
|
|
18
|
+
media.addEventListener('change', motionChanged)
|
|
19
|
+
onBeforeUnmount(() => media.removeEventListener('change', motionChanged))
|
|
20
|
+
const preset = computed(() => THEME_PRESETS[selected.value])
|
|
21
|
+
const tokens = computed(() => ({
|
|
22
|
+
algorithm: preset.value.mode === 'dark' ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
|
|
23
|
+
token: { colorPrimary: preset.value.primary, colorBgBase: preset.value.bg, colorBgContainer: preset.value.surface,
|
|
24
|
+
colorBgElevated: preset.value.surface, colorText: preset.value.text, colorTextSecondary: preset.value.muted,
|
|
25
|
+
colorBorder: preset.value.border, colorTextLightSolid: preset.value.onPrimary, motion: !reduced.value },
|
|
26
|
+
}))
|
|
27
|
+
function choose(value: unknown) {
|
|
28
|
+
if (typeof value !== 'string' || !THEME_IDS.includes(value as ThemeId)) return
|
|
29
|
+
selected.value = value as ThemeId
|
|
30
|
+
warning.value = saveThemePreference(storage, key, selected.value) ? '' : '主题偏好无法保存,本页仍可切换。'
|
|
31
|
+
}
|
|
32
|
+
watch(preset, value => {
|
|
33
|
+
document.documentElement.dataset.prototypeTheme = selected.value
|
|
34
|
+
document.documentElement.style.colorScheme = value.mode
|
|
35
|
+
}, { immediate: true })
|
|
36
|
+
</script>
|
|
37
|
+
<template>
|
|
38
|
+
<ConfigProvider :locale="zhCN" :theme="tokens">
|
|
39
|
+
<div class="prototype-shell" :style="{ background: preset.bg, color: preset.text }">
|
|
40
|
+
<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>
|
|
41
|
+
<main :style="{ background: preset.surface, borderColor: preset.border }">
|
|
42
|
+
<Alert v-if="warning" type="warning" :title="warning" show-icon />
|
|
43
|
+
<Tabs v-model:active-key="activePage">
|
|
44
|
+
<TabPane v-for="page in config.pages" :key="page.id" :tab="page.title">
|
|
45
|
+
<section :aria-label="page.title"><h2>{{ page.title }}</h2><Alert type="warning" title="业务页面待实现" description="按固定需求、完整内容模板和通用能力清单实现此页,再进行交互验收。" show-icon /></section>
|
|
46
|
+
</TabPane>
|
|
47
|
+
</Tabs>
|
|
48
|
+
</main>
|
|
49
|
+
</div>
|
|
50
|
+
</ConfigProvider>
|
|
51
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { resolveThemeConfig } from './domain/theme'
|
|
2
|
+
import type { ThemeBinding } from './domain/theme'
|
|
3
|
+
export interface PrototypeConfig { instanceId: string; storageNamespace: string; mockVersion: string; title?: string; pages: { id: string; title: string }[]; theme?: ThemeBinding }
|
|
4
|
+
export interface PrototypeMeta extends PrototypeConfig { candidateId: string }
|
|
5
|
+
declare global { interface Window { __PROTOTYPE_META__: PrototypeMeta } }
|
|
6
|
+
|
|
7
|
+
export async function loadConfig(): Promise<PrototypeConfig> {
|
|
8
|
+
const response = await fetch(`${import.meta.env.BASE_URL}prototype-config.json`, { cache: 'no-store' })
|
|
9
|
+
if (!response.ok) throw new Error('无法读取原型配置,请检查当前预览构建。')
|
|
10
|
+
const config: unknown = await response.json()
|
|
11
|
+
if (!config || typeof config !== 'object') throw new Error('原型配置无效。')
|
|
12
|
+
const value = config as Record<string, unknown>
|
|
13
|
+
for (const key of ['instanceId', 'storageNamespace', 'mockVersion']) {
|
|
14
|
+
if (typeof value[key] !== 'string' || !(value[key] as string).trim()) throw new Error(`原型配置缺少 ${key}。`)
|
|
15
|
+
}
|
|
16
|
+
resolveThemeConfig(value.theme)
|
|
17
|
+
return value as unknown as PrototypeConfig
|
|
18
|
+
}
|
|
@@ -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,13 @@
|
|
|
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; } }
|
|
@@ -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
|
+
}
|