Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/material/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './default-props-map';
export * from './materials';
export * from './materials-meta';
export * from './materials-protocol';
export * from './materials-theme';
export * from './merge-materials';
23 changes: 23 additions & 0 deletions packages/core/src/material/materials-theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type ThemeColorScheme = 'light' | 'dark';

export interface ThemeDescriptor {
id: string;
colorScheme?: ThemeColorScheme;
}

export interface ThemeApplyContext {
systemColorScheme: ThemeColorScheme;
}

export type ThemeDisposer = () => void;

export interface ThemeApplyResult {
descriptor: ThemeDescriptor;
dispose: ThemeDisposer;
Root?: unknown;
}

export interface IMaterialsTheme {
themes?: ThemeDescriptor[];
apply(theme: string, ctx: ThemeApplyContext): ThemeApplyResult;
}
3 changes: 3 additions & 0 deletions packages/core/src/material/materials.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { IMaterialsTheme } from './materials-theme';

export type IMaterialComponent = unknown;

export type IMaterialsMap = Record<string, IMaterialComponent>;
Expand All @@ -6,5 +8,6 @@ export interface IMaterials {
components?: IMaterialsMap;
requiredCompleteFieldSelectors?: string[];
defaultPropsMap?: Record<string, any>;
theme?: IMaterialsTheme | IMaterialsTheme[];
[key: string]: any;
}
61 changes: 48 additions & 13 deletions packages/core/src/material/merge-materials.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,55 @@
import type { IMaterials } from './materials';
import type { IMaterialsTheme } from './materials-theme';

export function mergeMaterials(...list: IMaterials[]): IMaterials {
const result: IMaterials = {
components: {},
defaultPropsMap: {},
requiredCompleteFieldSelectors: [],
const KNOWN_KEYS = ['components', 'requiredCompleteFieldSelectors', 'defaultPropsMap', 'theme'];
Comment thread
gimmyhehe marked this conversation as resolved.
Outdated

export function mergeMaterials(...sources: (IMaterials | undefined)[]): IMaterials {
const components: Record<string, unknown> = {};
const requiredCompleteFieldSelectors: string[] = [];
const defaultPropsMap: Record<string, any> = {};
const themes: IMaterialsTheme[] = [];
const seenThemes = new Set<IMaterialsTheme>();
const extra: Record<string, unknown> = {};

for (const src of sources) {
if (!src) {
continue;
}
Object.assign(components, src.components ?? {});
for (const selector of src.requiredCompleteFieldSelectors ?? []) {
if (!requiredCompleteFieldSelectors.includes(selector)) {
requiredCompleteFieldSelectors.push(selector);
}
}
Object.assign(defaultPropsMap, src.defaultPropsMap ?? {});
if (src.theme) {
const arr = Array.isArray(src.theme) ? src.theme : [src.theme];
for (const theme of arr) {
if (theme && !seenThemes.has(theme)) {
seenThemes.add(theme);
themes.push(theme);
}
}
}
for (const key of Object.keys(src)) {
if (!KNOWN_KEYS.includes(key)) {
extra[key] = (src as Record<string, unknown>)[key];
}
}
}

const merged: IMaterials = {
components,
requiredCompleteFieldSelectors,
defaultPropsMap,
...extra,
};

for (const item of list) {
if (!item) continue;
Object.assign(result.components!, item.components ?? {});
Object.assign(result.defaultPropsMap!, item.defaultPropsMap ?? {});
result.requiredCompleteFieldSelectors!.push(
...(item.requiredCompleteFieldSelectors ?? []),
);
if (themes.length === 1) {
merged.theme = themes[0];
} else if (themes.length > 1) {
merged.theme = themes;
}

return result;
return merged;
}
6 changes: 3 additions & 3 deletions packages/frameworks/vue/src/chat/GenuiChat.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
UserItem,
UserTextItem,
} from '@opentiny/tiny-robot';
import { ref, watch, computed, h, inject, provide } from 'vue';
import { ref, watch, computed, h, inject } from 'vue';
import type { Ref, Component } from 'vue';
import { CustomModelProvider } from './CustomModelProvider';
import { scrollEnd, throttle, toSlotFunction } from './chat-utils';
Expand All @@ -37,7 +37,7 @@ import { IResponseHandler, defaultResponseHandlers } from './response-handler';

const props = defineProps<IChatProps>();

const genuiConfig: any = inject(GENUI_CONFIG, null);
const genuiConfig = inject(GENUI_CONFIG);
const { t } = useI18n();

const isAllowFiles = computed(() => {
Expand Down Expand Up @@ -484,7 +484,7 @@ defineExpose({
<template>
<div
class="tg-chat-container"
:class="{ 'dark': genuiConfig?.theme === 'dark' }"
:class="{ 'dark': genuiConfig?.colorScheme === 'dark' }"
:style="!props.chatConfig?.showThinkingResult ? { '--thinking-display': 'none' } : {}"
>
<div
Expand Down
154 changes: 93 additions & 61 deletions packages/frameworks/vue/src/config-provider/ConfigProvider.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
<script setup lang="ts">
import { TinyConfigProvider } from '@opentiny/vue';
import { ThemeProvider } from '@opentiny/tiny-robot';
import ThemeTool, { tinyDarkTheme, tinyOldTheme } from '@opentiny/vue-theme/theme-tool';
import { watch, provide, computed, onMounted, ref, inject } from 'vue';
import type { IMaterials } from '@opentiny/genui-sdk-core';
import {
type IMaterials,
type IMaterialsTheme,
type ThemeApplyResult,
type ThemeColorScheme,
} from '@opentiny/genui-sdk-core';
import {
watch,
provide,
inject,
computed,
ref,
shallowRef,
onBeforeUnmount,
defineComponent,
h,
type Component,
type PropType,
type VNode,
} from 'vue';
import { RENDERER_SETTINGS_KEY } from '@opentiny/tiny-schema-renderer';
import { I18nMessages, useI18n } from '../chat/i18n';
import { GENUI_I18N, GENUI_CONFIG, GENUI_MATERIALS } from './injection-tokens';
Expand All @@ -13,19 +29,14 @@ import type { NotifyHandler } from './notify.types';
export type { NotifyHandler };

export interface ConfigProviderProps {
theme?: 'light' | 'dark' | 'lite' | 'auto';
theme?: string;
id?: string;
locale?: string;
i18n?: I18nMessages;
materials?: IMaterials;
notify?: NotifyHandler;
}

interface IRobotProviderProps {
colorMode: 'dark' | 'light';
targetElement?: string;
}

const props = withDefaults(defineProps<ConfigProviderProps>(), {
id: 'tiny-genui-config-provider',
locale: 'zh_CN',
Expand All @@ -34,35 +45,24 @@ const props = withDefaults(defineProps<ConfigProviderProps>(), {
const i18n = useI18n();
provide(GENUI_I18N, i18n);

const transformTheme = (themeConfig: any) => {
const newThemeConfig = structuredClone(themeConfig);
newThemeConfig.css = newThemeConfig.css.replaceAll(':host', `#${props.id}`).replaceAll(':root', `#${props.id}`);
return newThemeConfig;
};

const themeMap: Record<string, any> = {
dark: transformTheme(tinyDarkTheme),
lite: transformTheme(tinyOldTheme),
light: { css: ' ' },
};

const themeTool = new ThemeTool();

const { theme: mediaTheme } = useMediaTheme();

const actualTheme = computed(() => {
if (props.theme === 'auto') {
return mediaTheme.value;
const materialThemes = computed<IMaterialsTheme[]>(() => {
const theme = props.materials?.theme;
if (!theme) {
return [];
}
return props.theme;
return Array.isArray(theme) ? theme : [theme];
});

const genuiConfig = computed(() => {
return {
theme: actualTheme.value,
id: props.id,
};
});
const theme = computed(() => props.theme || 'light');

const colorScheme = ref<ThemeColorScheme>('light');

const genuiConfig = computed(() => ({
colorScheme: colorScheme.value,
id: props.id,
}));

provide(GENUI_CONFIG, genuiConfig);

Expand All @@ -71,10 +71,7 @@ watch(() => props.materials, (newVal) => {
Object.assign(internalMaterials, newVal);
}, { immediate: true });

provide(
GENUI_MATERIALS,
internalMaterials,
);
provide(GENUI_MATERIALS, internalMaterials);

const parentRendererSettings = inject(RENDERER_SETTINGS_KEY, {}) as Record<string, any>;
const rendererSettings = {
Expand All @@ -100,39 +97,74 @@ watch(
{ immediate: true },
);

watch(
() => actualTheme.value,
(newVal) => {
const themeConfig = themeMap[newVal] || themeMap.light;
themeTool.changeTheme(themeConfig);
const ThemeRoots = defineComponent({
name: 'ThemeRoots',
props: {
roots: { type: Array as PropType<Component[]>, required: true },
},
setup(props, { slots }) {
return () => {
const children = slots.default?.() ?? [];
return props.roots.reduceRight<VNode | VNode[]>(
(acc, root) => h(root, {}, () => acc),
children,
);
};
},
{
immediate: true,
});

const themeRoots = shallowRef<Component[]>([]);

let applied: ThemeApplyResult[] = [];

function clearTheme() {
const pending = applied;
applied = [];
pending.forEach((item) => item.dispose());
}

watch(
() => [materialThemes.value, theme.value, mediaTheme.value, props.id] as const,
([apis, themeValue, systemColorScheme]) => {
clearTheme();
// 原始 theme(含 auto)原样下发,物料用 ctx.systemColorScheme 自行解析
const results: ThemeApplyResult[] = [];
const roots: Component[] = [];

for (const api of apis) {
const result = api.apply(themeValue, { systemColorScheme });
if (result.Root) {
roots.push(result.Root as Component);
}
results.push(result);
applied.push(result);
}

themeRoots.value = roots;
// 取第一个声明了 colorScheme 的落地结果(first-wins),否则跟随系统
colorScheme.value =
results.find((result) => result.descriptor.colorScheme)?.descriptor.colorScheme ??
systemColorScheme;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
},
{ immediate: true },
);

const providerRef = ref();
onMounted(() => {
providerRef.value?.$el.classList.remove('tiny-config-provider');
});
onBeforeUnmount(clearTheme);

const robotProviderProps = computed(() => {
const providerProps: IRobotProviderProps = {
colorMode: actualTheme.value === 'dark' ? 'dark' : 'light',
};
if (genuiConfig?.value?.id) {
providerProps.targetElement = '#' + genuiConfig.value.id;
}
return providerProps;
});
const robotProviderProps = computed(() => ({
colorMode: colorScheme.value,
targetElement: `#${props.id}`,
}));
</script>

<template>
<TinyConfigProvider ref="providerRef" class="tg-config-provider" :id="props.id">
<div :id="props.id" class="tg-config-provider">
<ThemeProvider v-bind="robotProviderProps">
<slot />
<ThemeRoots :roots="themeRoots">
<slot />
</ThemeRoots>
</ThemeProvider>
</TinyConfigProvider>
</div>
</template>

<style scoped>
Expand Down
12 changes: 8 additions & 4 deletions packages/frameworks/vue/src/config-provider/injection-tokens.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { InjectionKey } from 'vue';
import type { IMaterials } from '@opentiny/genui-sdk-core';
import type { InjectionKey, ComputedRef } from 'vue';
import type { IMaterials, ThemeColorScheme } from '@opentiny/genui-sdk-core';

export const GENUI_I18N = Symbol('GENUI_I18N');
export const GENUI_CONFIG = Symbol('GENUI_CONFIG');
export interface GenuiConfigState {
colorScheme: ThemeColorScheme;
id: string;
}

export const GENUI_I18N = Symbol('GENUI_I18N');
export const GENUI_CONFIG: InjectionKey<ComputedRef<GenuiConfigState>> = Symbol('GENUI_CONFIG');
export const GENUI_MATERIALS: InjectionKey<IMaterials> = Symbol('GENUI_MATERIALS');
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { buildMaterialDefaultValueMap, type IMaterials } from '@opentiny/genui-sdk-core';
import { materialsMeta } from '../meta';
import { components } from './components';
import { createElementPlusMaterialsTheme } from './theme';

const standardRequiredCompleteFieldSelectors = ['[componentName=ElCard] > props > shadow'];

export const materials: IMaterials = {
components,
requiredCompleteFieldSelectors: standardRequiredCompleteFieldSelectors,
defaultPropsMap: buildMaterialDefaultValueMap(materialsMeta),
theme: createElementPlusMaterialsTheme(),
};
Loading
Loading