feat(theme-editor) (#25)

* wip(theme-editor)

* chore: make theme editor looks better

* feat(theme-editor): use controlled overrides

* fix(theme-editor): no invalid color

* chore: update seemly to support keywords color

* feat(theme-editor): add filter

* refactor(theme-editor): use popover

* fix(site): package bundled twice due to src code reference

* docs(back-top): avoid collision with theme-editor
This commit is contained in:
07akioni 2021-03-30 02:25:22 +08:00 committed by GitHub
parent abce6eece9
commit a4ec6e737d
17 changed files with 526 additions and 30 deletions

View File

@ -73,8 +73,7 @@
<script>
import { computed, ref } from 'vue'
import version from '../src/version'
import { useMessage } from '../src'
import { useMessage, version } from 'naive-ui'
import { i18n } from './utils/composables'
import {
useThemeName,

View File

@ -9,15 +9,17 @@
:date-locale="dateLocale"
:hljs="hljs"
>
<n-loading-bar-provider>
<n-message-provider>
<n-notification-provider>
<n-dialog-provider>
<Site />
</n-dialog-provider>
</n-notification-provider>
</n-message-provider>
</n-loading-bar-provider>
<n-theme-editor>
<n-loading-bar-provider>
<n-message-provider>
<n-notification-provider>
<n-dialog-provider>
<Site />
</n-dialog-provider>
</n-notification-provider>
</n-message-provider>
</n-loading-bar-provider>
</n-theme-editor>
</component>
</template>

View File

@ -1,7 +1,7 @@
import { createApp } from 'vue'
import naive, { NThemeEditor } from '../src/index'
import { installDemoComponents } from './setup'
import SiteRoot from './SiteRoot.vue'
import naive from '../src/index'
import { routes, zhDocRoutes, enDocRoutes } from './routes/routes'
import createDemoRouter from './routes/router'
@ -15,6 +15,7 @@ const router = createDemoRouter(app, routes)
app.use(router)
app.use(naive)
app.component('NThemeEditor', NThemeEditor)
installDemoComponents(app)
router.isReady().then(() => {

View File

@ -1,5 +1,5 @@
import { createApp } from 'vue'
import naive from 'naive-ui'
import naive, { NThemeEditor } from 'naive-ui'
import { installDemoComponents } from './setup'
import SiteRoot from './SiteRoot.vue'
import { routes } from './routes/routes'
@ -11,6 +11,7 @@ const router = createDemoRouter(app, routes)
app.use(router)
app.use(naive)
app.component('NThemeEditor', NThemeEditor)
installDemoComponents(app)
router.isReady().then(() => {

View File

@ -1,5 +1,6 @@
import { computed, ref, provide, reactive, toRef, inject } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useMemo } from 'vooks'
import {
NConfigProvider,
darkTheme,
@ -8,7 +9,7 @@ import {
dateEnUS,
dateZhCN,
useOsTheme
} from '../../src'
} from 'naive-ui'
import { TsConfigProvider } from '../../themes/tusimple/src'
import { i18n } from '../utils/composables'
import {
@ -34,7 +35,7 @@ export function siteSetup () {
}
})
// locale
const localeNameRef = computed({
const localeNameRef = useMemo({
get () {
return route.path.startsWith('/zh-CN') ? 'zh-CN' : 'en-US'
},
@ -45,12 +46,13 @@ export function siteSetup () {
const localeRef = computed(() => {
return localeNameRef.value === 'zh-CN' ? zhCN : enUS
})
const dateLocaleRef = computed(() => {
// useMemo
const dateLocaleRef = useMemo(() => {
return route.params.lang === 'zh-CN' ? dateZhCN : dateEnUS
})
// theme
const osThemeRef = useOsTheme()
const themeNameRef = computed({
const themeNameRef = useMemo({
get () {
switch (route.params.theme) {
case 'os-theme':

View File

@ -1,6 +1,5 @@
import { h } from 'vue'
import NCol from '../../src/legacy-grid/src/Col'
import NRow from '../../src/legacy-grid/src/Row'
import { NCol, NRow } from 'naive-ui'
export default {
name: 'ComponentDemos',

View File

@ -9,7 +9,7 @@
"dev": "npm run clean && npm run gen-version && cross-env NODE_ENV=development vite",
"build:js": "npm run gen-version && npm run clean && tsc -b --force tsconfig.esm.json && tsc -b --force tsconfig.cjs.json && node scripts/post-build",
"build:package": "npm run gen-version && npm run clean && node scripts/pre-build/deprecated-build-icons.js && tsc -b --force tsconfig.esm.json && tsc -b --force tsconfig.cjs.json && node scripts/post-build",
"build:site": "./scripts/pre-build-site/pre-build-site.sh && cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=4096 vite build && ./scripts/post-build-site/post-build-site.sh",
"build:site": "./scripts/pre-build-site/pre-build-site.sh && cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=4096 vite build",
"clean": "rm -rf site lib es node_modules/naive-ui themes/**/es themes/**/lib",
"release:package": "npm run build:package && npm publish --tag next",
"lint": "npm run lint:code && npm run lint:type",
@ -113,11 +113,11 @@
"evtd": "^0.1.1",
"highlight.js": "^10.7.1",
"lodash-es": "^4.17.21",
"seemly": "^0.1.18",
"seemly": "^0.1.19",
"treemate": "^0.2.4",
"vdirs": "^0.1.0",
"vfonts": "^0.1.0",
"vooks": "^0.1.3",
"vooks": "^0.1.5",
"vue": "^3.0.9",
"vue-router": "^4.0.5",
"vueuc": "^0.3.0"

View File

@ -145,9 +145,9 @@ const derived: ThemeCommonVars = {
closeColor: overlay(Number(base.alphaClose)),
closeColorPressed: overlay(Number(base.alphaClose) * 0.8),
closeColorDisabled: overlay(base.alpha4),
closeOpacity: Number(base.alphaClose),
closeOpacityHover: Number(base.alphaClose) * 1.25,
closeOpacityPressed: Number(base.alphaClose) * 0.8,
closeOpacity: base.alphaClose,
closeOpacityHover: String(Number(base.alphaClose) * 1.25),
closeOpacityPressed: String(Number(base.alphaClose) * 0.8),
// clear
clearColor: overlay(base.alpha4),

View File

@ -144,9 +144,9 @@ const derived = {
closeColor: neutral(Number(base.alphaClose)),
closeColorPressed: neutral(Number(base.alphaClose) * 1.25),
closeColorDisabled: neutral(base.alpha4),
closeOpacity: Number(base.alphaClose),
closeOpacityHover: Number(base.alphaClose) * 0.8,
closeOpacityPressed: Number(base.alphaClose) * 1.25,
closeOpacity: base.alphaClose,
closeOpacityHover: String(Number(base.alphaClose) * 0.8),
closeOpacityPressed: String(Number(base.alphaClose) * 1.25),
// clear
clearColor: neutral(base.alpha4),

View File

@ -3,5 +3,5 @@
BackTop will find its first scrollable ascendant element and listen scroll event on it.
```html
<n-back-top />
<n-back-top :right="96" />
```

View File

@ -3,5 +3,5 @@
BackTop 会找到首个可滚动的祖先元素并且监听它的滚动事件。
```html
<n-back-top />
<n-back-top :right="96" />
```

View File

@ -15,3 +15,5 @@ export { darkTheme, createTheme } from './themes'
export { c, cE, cM, cB, cNotM } from './_utils/cssr'
export { default as version } from './version'
export { NThemeEditor } from './theme-editor'

View File

@ -25,6 +25,7 @@ export default cB('scrollbar', `
`),
c('>', [
cB('scrollbar-content', `
box-sizing: border-box;
overflow: hidden;
min-width: 100%;
`)

View File

@ -0,0 +1 @@
export { default as NThemeEditor } from './src/ThemeEditor'

View File

@ -0,0 +1,347 @@
import {
h,
computed,
defineComponent,
renderSlot,
ref,
Fragment,
toRaw
} from 'vue'
import { cloneDeep } from 'lodash-es'
import { rgba } from 'seemly'
import {
GlobalTheme,
GlobalThemeOverrides,
NConfigProvider
} from '../../config-provider'
import { NPopover } from '../../popover'
import { NScrollbar } from '../../scrollbar'
import { NCollapse, NCollapseItem } from '../../collapse'
import { NInput } from '../../input'
import { NSpace } from '../../space'
import { lightTheme } from '../../themes/light'
import { useConfig } from '../../_mixins'
import { NEmpty } from '../../empty'
import { NElement } from '../../element'
import { NDivider } from '../../divider'
import { NButton } from '../../button'
const ColorWandIcon = (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{ width: '1em', height: '1em', color: 'currentColor' }}
>
<path
d="M13.5 1C13.7761 1 14 1.22386 14 1.5V2H14.5C14.7761 2 15 2.22386 15 2.5C15 2.77614 14.7761 3 14.5 3H14V3.5C14 3.77614 13.7761 4 13.5 4C13.2239 4 13 3.77614 13 3.5V3H12.5C12.2239 3 12 2.77614 12 2.5C12 2.22386 12.2239 2 12.5 2H13V1.5C13 1.22386 13.2239 1 13.5 1Z"
fill="currentColor"
></path>
<path
d="M3.5 3C3.77615 3 4 3.22386 4 3.5V4H4.5C4.77615 4 5 4.22386 5 4.5C5 4.77614 4.77615 5 4.5 5H4V5.5C4 5.77614 3.77615 6 3.5 6C3.22386 6 3 5.77614 3 5.5V5H2.5C2.22386 5 2 4.77614 2 4.5C2 4.22386 2.22386 4 2.5 4H3V3.5C3 3.22386 3.22386 3 3.5 3Z"
fill="currentColor"
></path>
<path
d="M12.5 12C12.7761 12 13 11.7761 13 11.5C13 11.2239 12.7761 11 12.5 11H12V10.5C12 10.2239 11.7761 10 11.5 10C11.2239 10 11 10.2239 11 10.5V11H10.5C10.2239 11 10 11.2239 10 11.5C10 11.7761 10.2239 12 10.5 12H11V12.5C11 12.7761 11.2239 13 11.5 13C11.7761 13 12 12.7761 12 12.5V12H12.5Z"
fill="currentColor"
></path>
<path
d="M8.72956 4.56346C9.4771 3.81592 10.6891 3.81592 11.4367 4.56347C12.1842 5.31102 12.1842 6.52303 11.4367 7.27058L4.26679 14.4404C3.51924 15.1879 2.30723 15.1879 1.55968 14.4404C0.812134 13.6928 0.812138 12.4808 1.55969 11.7333L8.72956 4.56346ZM8.25002 6.4572L2.26679 12.4404C1.90977 12.7974 1.90977 13.3763 2.26679 13.7333C2.62381 14.0903 3.20266 14.0903 3.55968 13.7333L9.54292 7.75009L8.25002 6.4572ZM10.25 7.04299L10.7295 6.56347C11.0866 6.20645 11.0866 5.6276 10.7296 5.27057C10.3725 4.91355 9.79368 4.91355 9.43666 5.27057L8.95713 5.7501L10.25 7.04299Z"
fill="currentColor"
></path>
</svg>
)
export default defineComponent({
name: 'ThemeEditor',
setup () {
const { NConfigProvider } = useConfig()
const theme = computed(() => {
const mergedUnstableTheme: GlobalTheme =
NConfigProvider?.mergedUnstableTheme || lightTheme
const common = mergedUnstableTheme.common || lightTheme.common
const overrides: GlobalThemeOverrides = {
common
}
for (const key of Object.keys(lightTheme) as Array<
keyof typeof lightTheme
>) {
if (key === 'common') continue
;(overrides as any)[key] = (mergedUnstableTheme[key]?.self?.(common) ||
lightTheme[key].self?.(common)) as any
// Here we must use as any, nor ts 2590 will be raised since the union is
// too complex
}
return overrides
})
const overridesRef = ref<any>({})
const tempOverridesRef = ref<any>({})
const varNamePatternRef = ref('')
const compNamePatternRef = ref('')
const tempVarNamePatternRef = ref('')
const tempCompNamePatternRef = ref('')
function applyTempOverrides (
compName: string,
varName: string,
value: string
): void {
if (value && (varName.includes('color') || varName.includes('Color'))) {
try {
rgba(value)
} catch {
setTempOverrides(compName, varName, '')
alert(
`${compName}.${varName} '${value}' is invalid Color. Only #000[0], #000000[00], rgb(0, 0, 0), rgba(0, 0, 0, 0) formatted color is supported.`
)
return
}
}
overridesRef.value = cloneDeep(toRaw(tempOverridesRef.value))
}
function setTempOverrides (
compName: string,
varName: string,
value: string
): void {
const { value: tempOverrides } = tempOverridesRef
if (!(compName in tempOverrides)) tempOverrides[compName] = {}
const compOverrides = tempOverrides[compName]
if (value) {
compOverrides[varName] = value
} else {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete compOverrides[varName]
}
}
function handleClearClick (): void {
tempOverridesRef.value = {}
overridesRef.value = {}
}
return {
theme,
showPanel: ref(false),
tempOverrides: tempOverridesRef,
overrides: overridesRef,
compNamePattern: compNamePatternRef,
tempCompNamePattern: tempCompNamePatternRef,
varNamePattern: varNamePatternRef,
tempVarNamePattern: tempVarNamePatternRef,
applyTempOverrides,
setTempOverrides,
handleClearClick
}
},
render () {
return (
<NConfigProvider themeOverrides={this.overrides}>
{{
default: () => [
<NPopover
trigger="manual"
show={this.showPanel}
displayDirective="show"
placement="top-end"
style={{
width: '288px',
height: 'calc(100vh - 200px)',
padding: 0
}}
>
{{
trigger: () => (
<NElement
style={{
position: 'fixed',
zIndex: 10,
bottom: '40px',
right: '40px',
width: '40px',
height: '40px',
fontSize: '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
backgroundColor: 'var(--popover-color)',
color: 'var(--text-color-2)',
transition: 'all .3s var(--cubic-bezier-ease-in-out)',
boxShadow: '0 2px 8px 0px rgba(0, 0, 0, .12)',
cursor: 'pointer'
}}
// @ts-expect-error
onClick={() => {
this.showPanel = !this.showPanel
}}
>
{{ default: () => ColorWandIcon }}
</NElement>
),
default: () => (
<NScrollbar contentStyle={{ padding: '8px 14px' }}>
{{
default: () => (
<>
<NSpace vertical>
{{
default: () => (
<>
Component Name
<NInput
onChange={() => {
this.compNamePattern = this.tempCompNamePattern
}}
onInput={(value: string) => {
this.tempCompNamePattern = value
}}
value={this.tempCompNamePattern}
/>
Variable Name
<NInput
onChange={(value: string) => {
this.varNamePattern = value
}}
onInput={(value: string) => {
this.tempVarNamePattern = value
}}
value={this.tempVarNamePattern}
/>
<NButton
size="small"
onClick={() => {
this.compNamePattern = ''
this.varNamePattern = ''
this.tempCompNamePattern = ''
this.tempVarNamePattern = ''
}}
block
>
{{ default: () => 'Clear Search' }}
</NButton>
<NButton
size="small"
onClick={this.handleClearClick}
block
>
{{ default: () => 'Clear All Variables' }}
</NButton>
</>
)
}}
</NSpace>
<NDivider />
<NCollapse displayDirective="show">
{{
default: () => {
const {
theme,
compNamePattern,
varNamePattern
} = this
const themeKeys = Object.keys(theme)
const compNamePatternLower = compNamePattern.toLowerCase()
const varNamePatternLower = varNamePattern.toLowerCase()
return themeKeys
.filter((themeKey) => {
return themeKey
.toLowerCase()
.includes(compNamePatternLower)
})
.map((themeKey) => {
return (
<NCollapseItem
title={themeKey}
name={themeKey}
>
{{
default: () => {
const componentTheme:
| Record<string, string>
| undefined = (theme as any)[
themeKey
]
if (componentTheme === undefined) {
return <NEmpty />
}
const varKeys = Object.keys(
componentTheme
).filter((key) => {
return (
key !== 'name' &&
key
.toLowerCase()
.includes(varNamePatternLower)
)
})
if (!varKeys.length) {
return <NEmpty />
}
return (
<NSpace vertical>
{{
default: () =>
varKeys.map((varKey) => {
return [
<div
key={`${varKey}Label`}
>
{varKey}
</div>,
<NInput
key={varKey}
onChange={(
value: string
) =>
this.applyTempOverrides(
themeKey,
varKey,
value
)
}
onUpdateValue={(
value: string
) => {
this.setTempOverrides(
themeKey,
varKey,
value
)
}}
value={
this
.tempOverrides?.[
themeKey
]?.[varKey] || ''
}
placeholder={
componentTheme[
varKey
]
}
/>
]
})
}}
</NSpace>
)
}
}}
</NCollapseItem>
)
})
}
}}
</NCollapse>
</>
)
}}
</NScrollbar>
)
}}
</NPopover>,
renderSlot(this.$slots, 'default')
]
}}
</NConfigProvider>
)
}
})

140
src/themes/light.ts Normal file
View File

@ -0,0 +1,140 @@
// The file is for internal usage, do not export it, since all the components
// have default light theme.
import { commonLight } from '../_styles/common'
import { alertLight } from '../alert/styles'
import { anchorLight } from '../anchor/styles'
import { autoCompleteLight } from '../auto-complete/styles'
import { avatarLight } from '../avatar/styles'
import { backTopLight } from '../back-top/styles'
import { badgeLight } from '../badge/styles'
import { breadcrumbLight } from '../breadcrumb/styles'
import { buttonLight } from '../button/styles'
import { cardLight } from '../card/styles'
import { cascaderLight } from '../cascader/styles'
import { checkboxLight } from '../checkbox/styles'
import { codeLight } from '../code/styles'
import { collapseLight } from '../collapse/styles'
import { dataTableLight } from '../data-table/styles'
import { datePickerLight } from '../date-picker/styles'
import { descriptionsLight } from '../descriptions/styles'
import { dialogLight } from '../dialog/styles'
import { dividerLight } from '../divider/styles'
import { drawerLight } from '../drawer/styles'
import { dropdownLight } from '../dropdown/styles'
import { dynamicInputLight } from '../dynamic-input/styles'
import { dynamicTagsLight } from '../dynamic-tags/styles'
import { elementLight } from '../element/styles'
import { ellipsisLight } from '../ellipsis/styles'
import { emptyLight } from '../empty/styles'
import { formLight } from '../form/styles'
import { gradientTextLight } from '../gradient-text/styles'
import { iconLight } from '../icon/styles'
import { inputLight } from '../input/styles'
import { inputNumberLight } from '../input-number/styles'
import { layoutLight } from '../layout/styles'
import { listLight } from '../list/styles'
import { loadingBarLight } from '../loading-bar/styles'
import { logLight } from '../log/styles'
import { menuLight } from '../menu/styles'
import { mentionLight } from '../mention/styles'
import { messageLight } from '../message/styles'
import { modalLight } from '../modal/styles'
import { notificationLight } from '../notification/styles'
import { paginationLight } from '../pagination/styles'
import { popconfirmLight } from '../popconfirm/styles'
import { popoverLight } from '../popover/styles'
import { popselectLight } from '../popselect/styles'
import { progressLight } from '../progress/styles'
import { radioLight } from '../radio/styles'
import { rateLight } from '../rate/styles'
import { resultLight } from '../result/styles'
import { scrollbarLight } from '../scrollbar/styles'
import { sliderLight } from '../slider/styles'
import { spaceLight } from '../space/styles'
import { spinLight } from '../spin/styles'
import { statisticLight } from '../statistic/styles'
import { stepsLight } from '../steps/styles'
import { switchLight } from '../switch/styles'
import { tableLight } from '../table/styles'
import { tabsLight } from '../tabs/styles'
import { tagLight } from '../tag/styles'
import { thingLight } from '../thing/styles'
import { timePickerLight } from '../time-picker/styles'
import { timelineLight } from '../timeline/styles'
import { tooltipLight } from '../tooltip/styles'
import { transferLight } from '../transfer/styles'
import { typographyLight } from '../typography/styles'
import { treeLight } from '../tree/styles'
import { uploadLight } from '../upload/styles'
import { selectLight } from '../select/styles'
import type { BuiltInGlobalTheme } from './interface'
export const lightTheme: BuiltInGlobalTheme = {
common: commonLight,
Alert: alertLight,
Anchor: anchorLight,
AutoComplete: autoCompleteLight,
Avatar: avatarLight,
BackTop: backTopLight,
Badge: badgeLight,
Breadcrumb: breadcrumbLight,
Button: buttonLight,
Card: cardLight,
Cascader: cascaderLight,
Checkbox: checkboxLight,
Code: codeLight,
Collapse: collapseLight,
DataTable: dataTableLight,
DatePicker: datePickerLight,
Descriptions: descriptionsLight,
Dialog: dialogLight,
Divider: dividerLight,
Drawer: drawerLight,
Dropdown: dropdownLight,
DynamicInput: dynamicInputLight,
DynamicTags: dynamicTagsLight,
Element: elementLight,
Empty: emptyLight,
Ellipsis: ellipsisLight,
Form: formLight,
GradientText: gradientTextLight,
Icon: iconLight,
Input: inputLight,
InputNumber: inputNumberLight,
Layout: layoutLight,
List: listLight,
LoadingBar: loadingBarLight,
Log: logLight,
Menu: menuLight,
Mention: mentionLight,
Message: messageLight,
Modal: modalLight,
Notification: notificationLight,
Pagination: paginationLight,
Popconfirm: popconfirmLight,
Popover: popoverLight,
Popselect: popselectLight,
Progress: progressLight,
Radio: radioLight,
Rate: rateLight,
Result: resultLight,
Scrollbar: scrollbarLight,
Select: selectLight,
Slider: sliderLight,
Space: spaceLight,
Spin: spinLight,
Statistic: statisticLight,
Steps: stepsLight,
Switch: switchLight,
Table: tableLight,
Tabs: tabsLight,
Tag: tagLight,
Thing: thingLight,
TimePicker: timePickerLight,
Timeline: timelineLight,
Tooltip: tooltipLight,
Transfer: transferLight,
Tree: treeLight,
Typography: typographyLight,
Upload: uploadLight
}

View File

@ -458,6 +458,7 @@
- [ ] mention mention pending option not correct, sometimes it is not the first option
- [x] code Deprecated as of 10.7.0. highlight(lang, code, ...args) has been deprecated.
- [x] vdirs zindexable https://github.com/vuejs/vue-next/issues/3497
- [ ] collapse-item overflow
## Build