Skip to content

Commit 17ea7ef

Browse files
committed
chore: externalize theme to type app config
1 parent f76ec5a commit 17ea7ef

File tree

11 files changed

+188
-84
lines changed

11 files changed

+188
-84
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
"eslint": "^8.57.0",
4141
"nuxt": "npm:nuxt-nightly@pr-26085",
4242
"ohash": "^1.1.3",
43+
"vue": "^3.4.21",
44+
"vue-router": "^4.3.0",
4345
"vue-tsc": "^2.0.5"
4446
}
4547
}

playground/app.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<template>
22
<div class="max-w-7xl mx-auto py-24">
3-
<UButton color="green" truncate icon="i-heroicons-rocket-launch">
4-
Click
5-
</UButton>
3+
<UButton color="green" icon="i-heroicons-rocket-launch" to="/" label="/" />
4+
<UButton color="red" icon="i-heroicons-rocket-launch" to="/about" label="/about" square />
5+
6+
<NuxtPage />
67
</div>
78
</template>

playground/pages/[...slug].vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<template>
2+
<div>{{ $route.path }}</div>
3+
</template>
4+
5+
<script setup lang="ts">
6+
</script>

pnpm-lock.yaml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/module.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
import { defu } from 'defu'
22
import { createResolver, defineNuxtModule, addComponentsDir, addImportsDir, installModule } from '@nuxt/kit'
3+
import type { DeepPartial } from './runtime/types'
4+
import * as theme from './runtime/theme'
5+
6+
type UI = {
7+
primary?: string
8+
gray?: string
9+
[key: string]: any
10+
} & DeepPartial<typeof theme>
11+
12+
declare module 'nuxt/schema' {
13+
interface AppConfigInput {
14+
ui?: UI
15+
}
16+
}
17+
declare module '@nuxt/schema' {
18+
interface AppConfigInput {
19+
ui?: UI
20+
}
21+
}
322

423
export default defineNuxtModule({
524
meta: {
@@ -12,6 +31,7 @@ export default defineNuxtModule({
1231
async setup (_, nuxt) {
1332
const resolver = createResolver(import.meta.url)
1433

34+
nuxt.options.alias['#ui'] = resolver.resolve('./runtime')
1535
nuxt.options.appConfig.ui = defu(nuxt.options.appConfig.ui || {}, {
1636
primary: 'green',
1737
gray: 'cool',
@@ -27,7 +47,8 @@ export default defineNuxtModule({
2747
darkMode: 'class',
2848
content: {
2949
files: [
30-
resolver.resolve('./runtime/components/**/*.{vue,mjs,ts}')
50+
resolver.resolve('./runtime/components/**/*.{vue,ts}'),
51+
resolver.resolve('./runtime/theme/**/*.ts')
3152
]
3253
}
3354
}

src/runtime/components/Button.vue

Lines changed: 26 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,13 @@
11
<script lang="ts">
22
import { tv, type VariantProps } from 'tailwind-variants'
3-
import type { LinkProps } from './Link.vue'
43
// import appConfig from '#build/app.config'
4+
import { getLinkProps, type LinkProps } from '#ui/components/Link.vue'
5+
import theme from '#ui/theme/button'
56
6-
export const theme = {
7-
slots: {
8-
base: 'inline-flex items-center focus:outline-none rounded-md font-medium',
9-
label: '',
10-
icon: 'flex-shrink-0'
11-
},
12-
variants: {
13-
color: {
14-
blue: 'bg-blue-500 hover:bg-blue-700',
15-
red: 'bg-red-500 hover:bg-red-700',
16-
green: 'bg-green-500 hover:bg-green-700'
17-
},
18-
size: {
19-
'2xs': {
20-
base: 'px-2 py-1 text-xs gap-x-1'
21-
},
22-
xs: {
23-
base: 'px-2.5 py-1.5 text-xs gap-x-1.5'
24-
},
25-
sm: {
26-
base: 'px-2.5 py-1.5 text-sm gap-x-1.5'
27-
},
28-
md: 'px-3 py-2 text-sm gap-x-2',
29-
lg: 'px-3.5 py-2.5 text-sm gap-x-2.5',
30-
xl: 'px-3.5 py-2.5 text-base gap-x-2.5'
31-
},
32-
truncate: {
33-
true: {
34-
label: 'text-left break-all line-clamp-1'
35-
}
36-
}
37-
},
38-
defaultVariants: {
39-
color: 'blue',
40-
size: 'md'
41-
}
42-
} as const
43-
44-
// export const button = tv({ extend: tv(theme), ...appConfig.ui.button })
45-
export const button = tv(theme)
7+
const appButton = tv(theme)
8+
// const appButton = tv({ extend: button, ...(appConfig.ui?.button || {}) })
469
47-
type ButtonVariants = VariantProps<typeof button>
48-
49-
export interface ButtonProps extends ButtonVariants, LinkProps {
10+
export interface ButtonProps extends VariantProps<typeof appButton>, LinkProps {
5011
label?: string
5112
icon?: string
5213
leading?: boolean
@@ -61,12 +22,12 @@ export interface ButtonProps extends ButtonVariants, LinkProps {
6122
padded?: boolean
6223
truncate?: boolean
6324
class?: any
64-
ui?: Partial<typeof button>
25+
ui?: Partial<typeof appButton>
6526
}
6627
</script>
6728

6829
<script setup lang="ts">
69-
import type { PropType } from 'vue'
30+
import { useSlots, computed, type PropType } from 'vue'
7031
import { linkProps } from './Link.vue'
7132
import UIcon from './Icon.vue'
7233
@@ -144,53 +105,56 @@ const props = defineProps({
144105
}
145106
})
146107
147-
148108
const slots = useSlots()
109+
const appConfig = useAppConfig()
149110
150111
// Computed
151112
152-
const isLeading = computed(() => (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon)
153-
154-
const isTrailing = computed(() => (props.icon && props.trailing) || (props.loading && props.trailing) || props.trailingIcon)
155-
156-
const ui = computed(() => tv({ extend: button, ...props.ui })({
113+
const ui = computed(() => tv({ extend: appButton, ...props.ui })({
157114
color: props.color,
158115
size: props.size,
159-
square: props.square || (!slots.default && !props.label),
160-
class: props.class
116+
loading: props.loading,
117+
truncate: props.truncate,
118+
block: props.block,
119+
padded: props.padded,
120+
square: props.square || (!slots.default && !props.label)
161121
}))
162122
123+
const isLeading = computed(() => (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon)
124+
125+
const isTrailing = computed(() => (props.icon && props.trailing) || (props.loading && props.trailing) || props.trailingIcon)
126+
163127
const leadingIconName = computed(() => {
164128
if (props.loading) {
165-
return props.loadingIcon
129+
return props.loadingIcon || appConfig.ui.icons.loading
166130
}
167131
168132
return props.leadingIcon || props.icon
169133
})
170134
171135
const trailingIconName = computed(() => {
172136
if (props.loading && !isLeading.value) {
173-
return props.loadingIcon
137+
return props.loadingIcon || appConfig.ui.icons.loading
174138
}
175139
176140
return props.trailingIcon || props.icon
177141
})
178142
</script>
179143

180144
<template>
181-
<ULink :type="type" :disabled="disabled || loading" :class="ui.base()" v-bind="$attrs">
145+
<ULink :type="type" :disabled="disabled || loading" :class="ui.base({ class: $props.class })" v-bind="{ ...getLinkProps($props), ...$attrs }">
182146
<slot name="leading" :disabled="disabled" :loading="loading">
183-
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.icon({ isLeading })" aria-hidden="true" />
147+
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.icon()" aria-hidden="true" />
184148
</slot>
185149

186-
<span v-if="label || $slots.default" :class="ui.label({ truncate })">
150+
<span v-if="label || $slots.default" :class="ui.label()">
187151
<slot>
188152
{{ label }}
189153
</slot>
190154
</span>
191155

192-
<!-- <slot name="trailing" :disabled="disabled" :loading="loading">
193-
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
194-
</slot> -->
156+
<slot name="trailing" :disabled="disabled" :loading="loading">
157+
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="ui.icon()" aria-hidden="true" />
158+
</slot>
195159
</ULink>
196160
</template>

src/runtime/components/Link.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ export const linkProps = {
126126
default: undefined
127127
}
128128
}
129+
130+
export const getLinkProps = (props: any) => {
131+
const keys = Object.keys(linkProps)
132+
133+
return keys.reduce((acc, key) => {
134+
if (props[key] !== undefined) {
135+
acc[key] = props[key]
136+
}
137+
return acc
138+
}, {} as Record<string, any>)
139+
}
129140
</script>
130141

131142
<script setup lang="ts">
@@ -156,6 +167,7 @@ function resolveLinkClass (route: RouteLocation, currentRoute: RouteLocation, {
156167
}
157168
</script>
158169

170+
<!-- eslint-disable vue/no-template-shadow -->
159171
<template>
160172
<component
161173
:is="as"

src/runtime/theme/button.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
export default {
2+
slots: {
3+
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',
4+
label: '',
5+
icon: 'flex-shrink-0'
6+
},
7+
variants: {
8+
color: {
9+
blue: 'bg-blue-500 hover:bg-blue-700',
10+
red: 'bg-red-500 hover:bg-red-700',
11+
green: 'bg-green-500 hover:bg-green-700'
12+
},
13+
size: {
14+
'2xs': {
15+
base: 'px-2 py-1 text-xs gap-x-1',
16+
icon: 'h-4 w-4'
17+
},
18+
xs: {
19+
base: 'px-2.5 py-1.5 text-xs gap-x-1.5',
20+
icon: 'h-4 w-4'
21+
},
22+
sm: {
23+
base: 'px-2.5 py-1.5 text-sm gap-x-1.5',
24+
icon: 'h-5 w-5'
25+
},
26+
md: {
27+
base: 'px-3 py-2 text-sm gap-x-2',
28+
icon: 'h-5 w-5'
29+
},
30+
lg: {
31+
base: 'px-3.5 py-2.5 text-sm gap-x-2.5',
32+
icon: 'h-6 w-6'
33+
},
34+
xl: {
35+
base: 'px-3.5 py-2.5 text-base gap-x-2.5',
36+
icon: 'h-6 w-6'
37+
}
38+
},
39+
truncate: {
40+
true: {
41+
label: 'truncate'
42+
}
43+
},
44+
loading: {
45+
true: {
46+
icon: 'animate-spin'
47+
}
48+
},
49+
block: {
50+
true: {
51+
base: 'w-full justify-center'
52+
}
53+
},
54+
square: {
55+
true: {
56+
base: ''
57+
}
58+
},
59+
padded: {
60+
true: {
61+
base: 'p-0'
62+
}
63+
}
64+
},
65+
compoundVariants: [{
66+
size: '2xs' as const,
67+
square: true,
68+
class: {
69+
base: 'p-1'
70+
}
71+
}, {
72+
size: 'xs' as const,
73+
square: true,
74+
class: {
75+
base: 'p-1'
76+
}
77+
}, {
78+
size: 'sm' as const,
79+
square: true,
80+
class: {
81+
base: 'p-1'
82+
}
83+
}, {
84+
size: 'md' as const,
85+
square: true,
86+
class: {
87+
base: 'p-2'
88+
}
89+
}, {
90+
size: 'lg' as const,
91+
square: true,
92+
class: {
93+
base: 'p-2'
94+
}
95+
}, {
96+
size: 'xl' as const,
97+
square: true,
98+
class: {
99+
base: 'p-2'
100+
}
101+
}],
102+
defaultVariants: {
103+
color: 'blue' as const,
104+
size: 'md' as const
105+
}
106+
}

src/runtime/theme/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as button } from './button'

src/runtime/types/index.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type DeepPartial<T> = Partial<{
2+
[P in keyof T]: DeepPartial<T[P]> | { [key: string]: string | object }
3+
}>

0 commit comments

Comments
 (0)