Skip to content

Commit 65c0f04

Browse files
committed
feat(projects): add ThemeCustomize
1 parent 0a87eda commit 65c0f04

File tree

19 files changed

+453
-73
lines changed

19 files changed

+453
-73
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script setup lang="ts">
2+
defineOptions({
3+
name: 'AppLogo'
4+
});
5+
</script>
6+
7+
<template>
8+
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg">
9+
<path
10+
d="M81.28 55.9c-.1-11.67-2.93-22.55-9.37-32.38-1-1.5-2.14-2.86-2.5-4.71a8.1 8.1 0 014-8.61 7.89 7.89 0 019.3 1.23 35.999 35.999 0 015.9 8.83 75.18 75.18 0 018.44 28.58 83.211 83.211 0 01-5.23 36.74 102.983 102.983 0 01-3 7.28 1.2 1.2 0 000 1.41c9.58 13.3 21.76 23 37.85 27.24a54.37 54.37 0 0019.68 1.57 7.72 7.72 0 018.36 6.9 7.903 7.903 0 01-6.7 9 64.744 64.744 0 01-23-1.33 77.68 77.68 0 01-36.93-19.88 93.628 93.628 0 01-11.91-13.71 2.18 2.18 0 00-2.3-1.06 72.744 72.744 0 00-27.38 7.55c-11.6 6-20.67 14.58-26.4 26.45a10.134 10.134 0 01-3.7 4.7 8 8 0 01-9.19-.7 7.86 7.86 0 01-2.36-9.28 60.324 60.324 0 018.72-14.52c12.2-15.43 28.21-24.59 47.32-28.57A85.085 85.085 0 0173.07 87c.524.015 1-.307 1.18-.8a76.06 76.06 0 006.53-22.3c.351-2.652.518-5.325.5-8z"
11+
fill="currentColor"
12+
/>
13+
<path
14+
d="M136.26 108.34a44.742 44.742 0 01-11.13-2.87 46.108 46.108 0 01-19.66-13.76 8 8 0 015.72-13.22 7.93 7.93 0 016.54 2.93 33.27 33.27 0 0018.87 10.75c1.546.155 3.058.553 4.48 1.18a8.08 8.08 0 013.84 9.21c-.92 3.52-4.13 5.81-8.66 5.78zm-80.6-75.02a7.61 7.61 0 016.64 5 49.139 49.139 0 013.64 17 46.33 46.33 0 01-2.46 17.28c-2 5.77-8.24 7.79-12.89 4.15a8.1 8.1 0 01-2.39-9 31.679 31.679 0 001.68-12.36 35.77 35.77 0 00-2.43-11c-2.1-5.45 1.75-11.07 8.21-11.07zm22.26 93.25a8 8 0 01-6.68 7.86 32.88 32.88 0 00-19.7 12.19 8.13 8.13 0 01-11.21 1.62 8 8 0 01-1.41-11.58A51.043 51.043 0 0154 123.81a45.842 45.842 0 0114-5.1c5.35-1.04 9.91 2.56 9.92 7.86z"
15+
fill="currentColor"
16+
/>
17+
</svg>
18+
</template>

docs/.vitepress/theme/layout/components/navbar.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ watch(path, () => {
7070
</SDropdownMenu>
7171
</template>
7272
<SSeparator class="mx-4 h-4" decorative orientation="vertical" />
73+
<slot name="theme-customize" />
7374
<ThemeToggle />
7475
<SSeparator class="mx-4 h-4" decorative orientation="vertical" />
7576
<SButtonIcon v-for="link in theme.socialLinks" :key="link.link" @click="openLink(link.link)">
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<script lang="ts" setup>
2+
import { useVModel } from '@vueuse/core';
3+
import { SButton, SLabel } from 'soy-ui';
4+
import { builtinColorMap, builtinRadiuses } from '@soybean-ui/unocss-preset';
5+
import type { ThemeConfigColor } from '@soybean-ui/unocss-preset';
6+
import { Check } from 'lucide-vue-next';
7+
8+
defineOptions({
9+
name: 'ThemeCustomize'
10+
});
11+
12+
interface ThemeCustomizeProps {
13+
color: ThemeConfigColor;
14+
radius: number;
15+
}
16+
17+
const props = defineProps<ThemeCustomizeProps>();
18+
19+
type ThemeCustomizeEmits = {
20+
(e: 'update:theme', value: ThemeConfigColor): void;
21+
(e: 'update:radius', value: number): void;
22+
};
23+
24+
const emit = defineEmits<ThemeCustomizeEmits>();
25+
26+
const color = useVModel(props, 'color', emit);
27+
const radius = useVModel(props, 'radius', emit);
28+
29+
function setTheme(item: ThemeConfigColor) {
30+
color.value = item;
31+
}
32+
33+
function setRadius(item: number) {
34+
radius.value = item;
35+
}
36+
</script>
37+
38+
<template>
39+
<div class="p-4">
40+
<div class="grid space-y-1">
41+
<h1 class="text-md text-foreground font-semibold">Customize</h1>
42+
<p class="text-xs text-muted-foreground">Pick a style and color for your components.</p>
43+
</div>
44+
<div class="pt-6 space-y-1.5">
45+
<SLabel for="color" class="text-xs">Color</SLabel>
46+
<div class="grid grid-cols-3 gap-2 py-1.5">
47+
<SButton
48+
v-for="(value, key) in builtinColorMap"
49+
:key="key"
50+
:variant="color === key ? 'outline' : 'pure'"
51+
class="h-8 justify-start px-3"
52+
@click="setTheme(key)"
53+
>
54+
<span
55+
class="h-5 w-5 flex shrink-0 items-center justify-center rounded-full"
56+
:style="{ backgroundColor: `hsl(${value})` }"
57+
>
58+
<Check v-if="color === key" class="h-3 w-3 text-white" />
59+
</span>
60+
<span class="ml-2 text-xs capitalize">
61+
{{ key }}
62+
</span>
63+
</SButton>
64+
</div>
65+
</div>
66+
<div class="pt-6 space-y-1.5">
67+
<SLabel for="radius" class="text-xs">Radius</SLabel>
68+
<div class="grid grid-cols-5 gap-2 py-1.5">
69+
<SButton
70+
v-for="(r, index) in builtinRadiuses"
71+
:key="index"
72+
:variant="r === radius ? 'outline' : 'pure'"
73+
class="h-8 justify-center px-3"
74+
@click="setRadius(r)"
75+
>
76+
<span class="text-xs">{{ r }}</span>
77+
</SButton>
78+
</div>
79+
</div>
80+
</div>
81+
</template>
Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,77 @@
11
<script setup lang="ts">
22
import { useData } from 'vitepress';
3-
import { toRefs } from 'vue';
3+
import { computed, ref, toRefs } from 'vue';
44
import { useScroll } from '@vueuse/core';
5+
import { SButtonIcon, SConfigProvider, SPopover } from 'soy-ui';
6+
import type { ConfigProviderProps } from 'soy-ui';
7+
import type { ThemeConfigColor } from '@soybean-ui/unocss-preset';
8+
import { SwatchBook } from 'lucide-vue-next';
9+
import AppLogo from './components/app-logo.vue';
510
import Home from './components/home.vue';
611
import Navbar from './components/navbar.vue';
712
import Docs from './components/docs.vue';
13+
import ThemeCustomize from './components/theme-customize.vue';
814
9-
const { site, theme, frontmatter } = useData();
15+
const { site, frontmatter } = useData();
1016
1117
const { arrivedState } = useScroll(globalThis.window);
1218
1319
const { top } = toRefs(arrivedState);
20+
21+
const color = ref<ThemeConfigColor>('default');
22+
const radius = ref(0.5);
23+
24+
const configProviderProps = computed<ConfigProviderProps>(() => ({
25+
theme: {
26+
color: color.value,
27+
radius: radius.value
28+
}
29+
}));
1430
</script>
1531

1632
<template>
17-
<div class="flex-c items-center">
18-
<header
19-
class="sticky top-0 z-10 h-17 w-full py-4 transition-all duration-500"
20-
:class="[
21-
top
22-
? 'bg-transparent backdrop-blur-0'
23-
: 'bg-background/90 backdrop-blur supports-[backdrop-filter]:bg-background/90'
24-
]"
25-
>
26-
<div class="mx-auto max-w-1440px flex-y-center justify-between px-6">
27-
<div class="w-full flex-y-center justify-between gap-8 md:justify-unset">
28-
<a href="/" class="flex-y-center gap-2">
29-
<img class="w-8 md:w-9" alt="Soybean UI logo" :src="theme.logo" />
30-
<span class="text-xl font-bold md:text-2xl">{{ site.title }}</span>
31-
</a>
32-
<!-- <SearchTrigger /> -->
33+
<SConfigProvider v-bind="configProviderProps">
34+
<div class="flex-c items-center">
35+
<header
36+
class="sticky top-0 z-10 h-17 w-full py-4 transition-all duration-500"
37+
:class="[
38+
top
39+
? 'bg-transparent backdrop-blur-0'
40+
: 'bg-background/90 backdrop-blur supports-[backdrop-filter]:bg-background/90'
41+
]"
42+
>
43+
<div class="mx-auto max-w-1440px flex-y-center justify-between px-6">
44+
<div class="w-full flex-y-center justify-between gap-8 md:justify-unset">
45+
<a href="/" class="flex-y-center gap-2">
46+
<AppLogo class="size-8 text-primary md:size-9" />
47+
<span class="text-xl font-bold md:text-2xl">{{ site.title }}</span>
48+
</a>
49+
<!-- <SearchTrigger /> -->
50+
</div>
51+
52+
<Navbar>
53+
<template #theme-customize>
54+
<SPopover content-class="z-15" side="bottom" align="end">
55+
<template #trigger>
56+
<SButtonIcon size="lg" class="mr-3">
57+
<SwatchBook />
58+
</SButtonIcon>
59+
</template>
60+
<ThemeCustomize v-model:color="color" v-model:radius="radius" />
61+
</SPopover>
62+
</template>
63+
</Navbar>
3364
</div>
65+
</header>
3466

35-
<Navbar />
67+
<div v-if="frontmatter.layout === 'home'" class="size-full flex-c flex-1 justify-between">
68+
<Home />
69+
<div></div>
3670
</div>
37-
</header>
3871

39-
<div v-if="frontmatter.layout === 'home'" class="size-full flex-c flex-1 justify-between">
40-
<Home />
41-
<div></div>
42-
</div>
43-
44-
<div v-else class="w-full flex-grow">
45-
<Docs />
72+
<div v-else class="w-full flex-grow">
73+
<Docs />
74+
</div>
4675
</div>
47-
</div>
76+
</SConfigProvider>
4877
</template>

docs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"preview": "vitepress preview"
1616
},
1717
"dependencies": {
18+
"@soybean-ui/unocss-preset": "workspace:*",
1819
"@vueuse/core": "13.0.0",
1920
"lucide-vue-next": "0.482.0",
2021
"soy-ui": "workspace:*",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"lint": "eslint . --fix",
3535
"prepare": "simple-git-hooks",
3636
"preview": "vite preview",
37+
"preview:docs": "pnpm -r --filter='./docs' run preview",
3738
"publish-pkg": "pnpm -r publish --access public",
3839
"release": "soy release",
3940
"restore": "tsx scripts/restore.ts",
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<script setup lang="ts">
2+
import { computed, toRefs, watch } from 'vue';
3+
import { useStyleTag } from '@vueuse/core';
4+
import { ConfigProvider, useOmitForwardProps } from '@soybean-ui/primitives';
5+
import { builtinColors, generateCSSVars } from '@soybean-ui/unocss-preset';
6+
import type { ThemeOptions } from '@soybean-ui/unocss-preset';
7+
import { provideConfigProviderContext } from './context';
8+
import type { ConfigProviderProps } from './types';
9+
10+
defineOptions({
11+
name: 'SConfigProvider',
12+
inheritAttrs: false
13+
});
14+
15+
const props = withDefaults(defineProps<ConfigProviderProps>(), {
16+
theme: () => ({ color: 'default' }),
17+
size: 'md'
18+
});
19+
20+
const delegatedProps = useOmitForwardProps(props, ['theme', 'size']);
21+
22+
const { theme, size } = toRefs(props);
23+
24+
provideConfigProviderContext({ theme, size });
25+
26+
const cssVars = computed(() => {
27+
const keys = Object.keys(theme.value);
28+
29+
if (!keys.length) return '';
30+
31+
if (keys.length === 1 && keys.includes('color')) {
32+
if (typeof theme.value.color === 'string' && builtinColors.includes(theme.value.color)) {
33+
return '';
34+
}
35+
}
36+
37+
return generateCSSVars(theme.value);
38+
});
39+
40+
useStyleTag(cssVars, { id: '__SOYBEAN_UI_THEME_VARS__' });
41+
42+
function getThemeName(color: ThemeOptions['color']) {
43+
let themeName = 'default';
44+
45+
if (typeof color === 'string') {
46+
themeName = color;
47+
}
48+
49+
if (typeof color === 'object') {
50+
if ('base' in color) {
51+
themeName = color.base || color.name;
52+
} else {
53+
themeName = color.name;
54+
}
55+
}
56+
57+
return themeName;
58+
}
59+
60+
function addThemeClass(newThemeName: string, oldThemeName: string) {
61+
if (newThemeName === oldThemeName) {
62+
if (newThemeName === 'default') {
63+
document.documentElement.classList.add(`theme-${newThemeName}`);
64+
}
65+
66+
return;
67+
}
68+
69+
document.documentElement.classList.add(`theme-${newThemeName}`);
70+
document.documentElement.classList.remove(`theme-${oldThemeName}`);
71+
}
72+
73+
watch(
74+
() => theme.value.color,
75+
(newVal, oldVal) => {
76+
const newThemeName = getThemeName(newVal);
77+
const oldThemeName = getThemeName(oldVal);
78+
79+
addThemeClass(newThemeName, oldThemeName);
80+
},
81+
{ flush: 'post' }
82+
);
83+
</script>
84+
85+
<template>
86+
<ConfigProvider v-bind="delegatedProps">
87+
<slot />
88+
</ConfigProvider>
89+
</template>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { createContext } from '@soybean-ui/primitives';
2+
import type { ConfigProviderContext } from './types';
3+
4+
export const [provideConfigProviderContext, injectConfigProviderContext] =
5+
createContext<ConfigProviderContext>('ConfigProvider');
6+
7+
export function useConfigProvider(required = false) {
8+
const context = injectConfigProviderContext();
9+
10+
if (required && !context) {
11+
throw new Error('ConfigProviderContext is not provided');
12+
}
13+
14+
return context;
15+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import SConfigProvider from './config-provider.vue';
2+
import { useConfigProvider } from './context';
3+
4+
export { SConfigProvider, useConfigProvider };
5+
6+
export * from './types';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Ref } from 'vue';
2+
import type { ConfigProviderProps as _ConfigProviderProps } from '@soybean-ui/primitives';
3+
import type { ThemeSize } from '@soybean-ui/variants';
4+
import type { ThemeOptions } from '@soybean-ui/unocss-preset';
5+
6+
export interface ConfigProviderProps extends _ConfigProviderProps {
7+
/** The theme options. */
8+
theme?: ThemeOptions;
9+
/** The size options. */
10+
size?: ThemeSize;
11+
}
12+
13+
export interface ConfigProviderContext {
14+
theme: Ref<ThemeOptions | undefined>;
15+
size: Ref<ThemeSize | undefined>;
16+
}

packages/soy-ui/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export * from './components/chip';
1313
export * from './components/collapsible';
1414
export * from './components/combobox';
1515
export * from './components/command';
16+
export * from './components/config-provider';
1617
export * from './components/context-menu';
1718
export * from './components/dialog';
1819
export * from './components/drawer';

0 commit comments

Comments
 (0)