@givolt/i3-composer-agent 2.3.1 → 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 +31 -1
- package/plugin.json +1 -1
- package/scripts/design-provider-test.js +7 -0
- package/scripts/enterprise-template-test.js +27 -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 +138 -19
- 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/enterprise-admin/capability-definition.json +294 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/index.html +507 -2
- 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 +4 -4
- package/skills/i3-ui-prototype/assets/templates/index.html +3 -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 +6 -0
- package/skills/i3-ui-prototype/references/entry-contract.md +71 -0
- package/skills/i3-ui-prototype/references/expert-adapter.md +33 -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 +1 -1
- package/skills/i3-ui-prototype/scripts/content-template.cjs +98 -0
- package/skills/i3-ui-prototype/scripts/enterprise-template.cjs +13 -6
- 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 +26 -6
- 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
|
+
}
|