chore: externalize theme to type app config

This commit is contained in:
Benjamin Canac
2024-03-06 12:26:52 +01:00
parent f76ec5a376
commit 17ea7efd3b
11 changed files with 188 additions and 84 deletions

View File

@@ -40,6 +40,8 @@
"eslint": "^8.57.0",
"nuxt": "npm:nuxt-nightly@pr-26085",
"ohash": "^1.1.3",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"vue-tsc": "^2.0.5"
}
}

View File

@@ -1,7 +1,8 @@
<template>
<div class="max-w-7xl mx-auto py-24">
<UButton color="green" truncate icon="i-heroicons-rocket-launch">
Click
</UButton>
<UButton color="green" icon="i-heroicons-rocket-launch" to="/" label="/" />
<UButton color="red" icon="i-heroicons-rocket-launch" to="/about" label="/about" square />
<NuxtPage />
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div>{{ $route.path }}</div>
</template>
<script setup lang="ts">
</script>

6
pnpm-lock.yaml generated
View File

@@ -52,6 +52,12 @@ devDependencies:
ohash:
specifier: ^1.1.3
version: 1.1.3
vue:
specifier: ^3.4.21
version: 3.4.21(typescript@5.3.3)
vue-router:
specifier: ^4.3.0
version: 4.3.0(vue@3.4.21)
vue-tsc:
specifier: ^2.0.5
version: 2.0.5(typescript@5.3.3)

View File

@@ -1,5 +1,24 @@
import { defu } from 'defu'
import { createResolver, defineNuxtModule, addComponentsDir, addImportsDir, installModule } from '@nuxt/kit'
import type { DeepPartial } from './runtime/types'
import * as theme from './runtime/theme'
type UI = {
primary?: string
gray?: string
[key: string]: any
} & DeepPartial<typeof theme>
declare module 'nuxt/schema' {
interface AppConfigInput {
ui?: UI
}
}
declare module '@nuxt/schema' {
interface AppConfigInput {
ui?: UI
}
}
export default defineNuxtModule({
meta: {
@@ -12,6 +31,7 @@ export default defineNuxtModule({
async setup (_, nuxt) {
const resolver = createResolver(import.meta.url)
nuxt.options.alias['#ui'] = resolver.resolve('./runtime')
nuxt.options.appConfig.ui = defu(nuxt.options.appConfig.ui || {}, {
primary: 'green',
gray: 'cool',
@@ -27,7 +47,8 @@ export default defineNuxtModule({
darkMode: 'class',
content: {
files: [
resolver.resolve('./runtime/components/**/*.{vue,mjs,ts}')
resolver.resolve('./runtime/components/**/*.{vue,ts}'),
resolver.resolve('./runtime/theme/**/*.ts')
]
}
}

View File

@@ -1,52 +1,13 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { LinkProps } from './Link.vue'
// import appConfig from '#build/app.config'
import { getLinkProps, type LinkProps } from '#ui/components/Link.vue'
import theme from '#ui/theme/button'
export const theme = {
slots: {
base: 'inline-flex items-center focus:outline-none rounded-md font-medium',
label: '',
icon: 'flex-shrink-0'
},
variants: {
color: {
blue: 'bg-blue-500 hover:bg-blue-700',
red: 'bg-red-500 hover:bg-red-700',
green: 'bg-green-500 hover:bg-green-700'
},
size: {
'2xs': {
base: 'px-2 py-1 text-xs gap-x-1'
},
xs: {
base: 'px-2.5 py-1.5 text-xs gap-x-1.5'
},
sm: {
base: 'px-2.5 py-1.5 text-sm gap-x-1.5'
},
md: 'px-3 py-2 text-sm gap-x-2',
lg: 'px-3.5 py-2.5 text-sm gap-x-2.5',
xl: 'px-3.5 py-2.5 text-base gap-x-2.5'
},
truncate: {
true: {
label: 'text-left break-all line-clamp-1'
}
}
},
defaultVariants: {
color: 'blue',
size: 'md'
}
} as const
const appButton = tv(theme)
// const appButton = tv({ extend: button, ...(appConfig.ui?.button || {}) })
// export const button = tv({ extend: tv(theme), ...appConfig.ui.button })
export const button = tv(theme)
type ButtonVariants = VariantProps<typeof button>
export interface ButtonProps extends ButtonVariants, LinkProps {
export interface ButtonProps extends VariantProps<typeof appButton>, LinkProps {
label?: string
icon?: string
leading?: boolean
@@ -61,12 +22,12 @@ export interface ButtonProps extends ButtonVariants, LinkProps {
padded?: boolean
truncate?: boolean
class?: any
ui?: Partial<typeof button>
ui?: Partial<typeof appButton>
}
</script>
<script setup lang="ts">
import type { PropType } from 'vue'
import { useSlots, computed, type PropType } from 'vue'
import { linkProps } from './Link.vue'
import UIcon from './Icon.vue'
@@ -144,25 +105,28 @@ const props = defineProps({
}
})
const slots = useSlots()
const appConfig = useAppConfig()
// Computed
const ui = computed(() => tv({ extend: appButton, ...props.ui })({
color: props.color,
size: props.size,
loading: props.loading,
truncate: props.truncate,
block: props.block,
padded: props.padded,
square: props.square || (!slots.default && !props.label)
}))
const isLeading = computed(() => (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon)
const isTrailing = computed(() => (props.icon && props.trailing) || (props.loading && props.trailing) || props.trailingIcon)
const ui = computed(() => tv({ extend: button, ...props.ui })({
color: props.color,
size: props.size,
square: props.square || (!slots.default && !props.label),
class: props.class
}))
const leadingIconName = computed(() => {
if (props.loading) {
return props.loadingIcon
return props.loadingIcon || appConfig.ui.icons.loading
}
return props.leadingIcon || props.icon
@@ -170,7 +134,7 @@ const leadingIconName = computed(() => {
const trailingIconName = computed(() => {
if (props.loading && !isLeading.value) {
return props.loadingIcon
return props.loadingIcon || appConfig.ui.icons.loading
}
return props.trailingIcon || props.icon
@@ -178,19 +142,19 @@ const trailingIconName = computed(() => {
</script>
<template>
<ULink :type="type" :disabled="disabled || loading" :class="ui.base()" v-bind="$attrs">
<ULink :type="type" :disabled="disabled || loading" :class="ui.base({ class: $props.class })" v-bind="{ ...getLinkProps($props), ...$attrs }">
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.icon({ isLeading })" aria-hidden="true" />
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.icon()" aria-hidden="true" />
</slot>
<span v-if="label || $slots.default" :class="ui.label({ truncate })">
<span v-if="label || $slots.default" :class="ui.label()">
<slot>
{{ label }}
</slot>
</span>
<!-- <slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
</slot> -->
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="ui.icon()" aria-hidden="true" />
</slot>
</ULink>
</template>

View File

@@ -126,6 +126,17 @@ export const linkProps = {
default: undefined
}
}
export const getLinkProps = (props: any) => {
const keys = Object.keys(linkProps)
return keys.reduce((acc, key) => {
if (props[key] !== undefined) {
acc[key] = props[key]
}
return acc
}, {} as Record<string, any>)
}
</script>
<script setup lang="ts">
@@ -156,6 +167,7 @@ function resolveLinkClass (route: RouteLocation, currentRoute: RouteLocation, {
}
</script>
<!-- eslint-disable vue/no-template-shadow -->
<template>
<component
:is="as"

106
src/runtime/theme/button.ts Normal file
View File

@@ -0,0 +1,106 @@
export default {
slots: {
base: 'rounded-md font-medium inline-flex items-center focus:outline-none focus-visible:outline-0 disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0',
label: '',
icon: 'flex-shrink-0'
},
variants: {
color: {
blue: 'bg-blue-500 hover:bg-blue-700',
red: 'bg-red-500 hover:bg-red-700',
green: 'bg-green-500 hover:bg-green-700'
},
size: {
'2xs': {
base: 'px-2 py-1 text-xs gap-x-1',
icon: 'h-4 w-4'
},
xs: {
base: 'px-2.5 py-1.5 text-xs gap-x-1.5',
icon: 'h-4 w-4'
},
sm: {
base: 'px-2.5 py-1.5 text-sm gap-x-1.5',
icon: 'h-5 w-5'
},
md: {
base: 'px-3 py-2 text-sm gap-x-2',
icon: 'h-5 w-5'
},
lg: {
base: 'px-3.5 py-2.5 text-sm gap-x-2.5',
icon: 'h-6 w-6'
},
xl: {
base: 'px-3.5 py-2.5 text-base gap-x-2.5',
icon: 'h-6 w-6'
}
},
truncate: {
true: {
label: 'truncate'
}
},
loading: {
true: {
icon: 'animate-spin'
}
},
block: {
true: {
base: 'w-full justify-center'
}
},
square: {
true: {
base: ''
}
},
padded: {
true: {
base: 'p-0'
}
}
},
compoundVariants: [{
size: '2xs' as const,
square: true,
class: {
base: 'p-1'
}
}, {
size: 'xs' as const,
square: true,
class: {
base: 'p-1'
}
}, {
size: 'sm' as const,
square: true,
class: {
base: 'p-1'
}
}, {
size: 'md' as const,
square: true,
class: {
base: 'p-2'
}
}, {
size: 'lg' as const,
square: true,
class: {
base: 'p-2'
}
}, {
size: 'xl' as const,
square: true,
class: {
base: 'p-2'
}
}],
defaultVariants: {
color: 'blue' as const,
size: 'md' as const
}
}

View File

@@ -0,0 +1 @@
export { default as button } from './button'

3
src/runtime/types/index.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
export type DeepPartial<T> = Partial<{
[P in keyof T]: DeepPartial<T[P]> | { [key: string]: string | object }
}>

View File

@@ -1,18 +0,0 @@
import type { Config } from 'tailwindcss'
export default <Partial<Config>> {
content: [
'./components/**/*.{js,vue,ts}',
'./modules/**/*.{js,vue,ts}',
'./layouts/**/*.vue',
'./pages/**/*.vue',
'./plugins/**/*.{js,ts}',
'./app.vue',
'./app.config.ts',
'./error.vue'
],
theme: {
extend: {}
},
plugins: []
}