frappe-ui 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -76,13 +76,13 @@ export default {
76
76
  this.icon ? 'p-1.5' : 'px-3 py-1',
77
77
  {
78
78
  'opacity-50 cursor-not-allowed pointer-events-none': this.isDisabled,
79
- 'bg-gradient-blue hover:bg-gradient-none hover:bg-blue-500 text-white focus:shadow-outline-blue':
79
+ 'bg-gradient-blue hover:bg-gradient-none hover:bg-blue-500 text-white focus:ring-2 focus:ring-offset-2 focus:ring-blue-500':
80
80
  this.type === 'primary',
81
- 'bg-gray-100 hover:bg-gray-200 text-gray-900 focus:shadow-outline-gray':
81
+ 'bg-gray-100 hover:bg-gray-200 text-gray-900 focus:ring-2 focus:ring-offset-2 focus:ring-gray-500':
82
82
  this.type === 'secondary',
83
- 'bg-red-500 hover:bg-red-400 text-white focus:shadow-outline-red':
83
+ 'bg-red-500 hover:bg-red-400 text-white focus:ring-2 focus:ring-offset-2 focus:ring-red-500':
84
84
  this.type === 'danger',
85
- 'bg-white text-gray-900 shadow focus:ring focus:ring-gray-400':
85
+ 'bg-white text-gray-900 border hover:bg-gray-50 focus:ring-2 focus:ring-offset-2 focus:ring-gray-400':
86
86
  this.type === 'white',
87
87
  },
88
88
  ]
@@ -0,0 +1,220 @@
1
+ <template>
2
+ <div>
3
+ <input
4
+ ref="input"
5
+ type="file"
6
+ :accept="fileTypes"
7
+ class="hidden"
8
+ @change="onFileAdd"
9
+ />
10
+ <slot
11
+ v-bind="{
12
+ file,
13
+ uploading,
14
+ progress,
15
+ uploaded,
16
+ message,
17
+ error,
18
+ total,
19
+ success,
20
+ openFileSelector,
21
+ }"
22
+ />
23
+ </div>
24
+ </template>
25
+
26
+ <script>
27
+ class FileUploader {
28
+ constructor() {
29
+ this.listeners = {}
30
+ }
31
+
32
+ on(event, handler) {
33
+ this.listeners[event] = this.listeners[event] || []
34
+ this.listeners[event].push(handler)
35
+ }
36
+
37
+ trigger(event, data) {
38
+ let handlers = this.listeners[event] || []
39
+ handlers.forEach((handler) => {
40
+ handler.call(this, data)
41
+ })
42
+ }
43
+
44
+ upload(file, options) {
45
+ return new Promise((resolve, reject) => {
46
+ let xhr = new XMLHttpRequest()
47
+ xhr.upload.addEventListener('loadstart', () => {
48
+ this.trigger('start')
49
+ })
50
+ xhr.upload.addEventListener('progress', (e) => {
51
+ if (e.lengthComputable) {
52
+ this.trigger('progress', {
53
+ uploaded: e.loaded,
54
+ total: e.total,
55
+ })
56
+ }
57
+ })
58
+ xhr.upload.addEventListener('load', () => {
59
+ this.trigger('finish')
60
+ })
61
+ xhr.addEventListener('error', () => {
62
+ this.trigger('error')
63
+ reject()
64
+ })
65
+ xhr.onreadystatechange = () => {
66
+ if (xhr.readyState == XMLHttpRequest.DONE) {
67
+ let error
68
+ if (xhr.status === 200) {
69
+ let r = null
70
+ try {
71
+ r = JSON.parse(xhr.responseText)
72
+ } catch (e) {
73
+ r = xhr.responseText
74
+ }
75
+ let out = r.message || r
76
+ resolve(out)
77
+ } else if (xhr.status === 403) {
78
+ error = JSON.parse(xhr.responseText)
79
+ } else {
80
+ this.failed = true
81
+ try {
82
+ error = JSON.parse(xhr.responseText)
83
+ } catch (e) {
84
+ // pass
85
+ }
86
+ }
87
+ if (error && error.exc) {
88
+ console.error(JSON.parse(error.exc)[0])
89
+ }
90
+ reject(error)
91
+ }
92
+ }
93
+ xhr.open('POST', '/api/method/upload_file', true)
94
+ xhr.setRequestHeader('Accept', 'application/json')
95
+ if (window.csrf_token && window.csrf_token !== '{{ csrf_token }}') {
96
+ xhr.setRequestHeader('X-Frappe-CSRF-Token', window.csrf_token)
97
+ }
98
+
99
+ let form_data = new FormData()
100
+ if (file) {
101
+ form_data.append('file', file, file.name)
102
+ }
103
+ form_data.append('is_private', +(options.private || 0))
104
+ form_data.append('folder', options.folder || 'Home')
105
+
106
+ if (options.file_url) {
107
+ form_data.append('file_url', options.file_url)
108
+ }
109
+
110
+ if (options.doctype && options.docname) {
111
+ form_data.append('doctype', options.doctype)
112
+ form_data.append('docname', options.docname)
113
+ if (options.fieldname) {
114
+ form_data.append('fieldname', options.fieldname)
115
+ }
116
+ }
117
+
118
+ if (options.method) {
119
+ form_data.append('method', options.method)
120
+ }
121
+
122
+ if (options.type) {
123
+ form_data.append('type', options.type)
124
+ }
125
+
126
+ xhr.send(form_data)
127
+ })
128
+ }
129
+ }
130
+
131
+ export default {
132
+ name: 'FileUploader',
133
+ props: ['fileTypes', 'uploadArgs', 'type', 'validateFile'],
134
+ data() {
135
+ return {
136
+ uploader: null,
137
+ uploading: false,
138
+ uploaded: 0,
139
+ error: null,
140
+ message: '',
141
+ total: 0,
142
+ file: null,
143
+ finishedUploading: false,
144
+ }
145
+ },
146
+ computed: {
147
+ progress() {
148
+ let value = Math.floor((this.uploaded / this.total) * 100)
149
+ return isNaN(value) ? 0 : value
150
+ },
151
+ success() {
152
+ return this.finishedUploading && !this.error
153
+ },
154
+ },
155
+ methods: {
156
+ openFileSelector() {
157
+ this.$refs['input'].click()
158
+ },
159
+ async onFileAdd(e) {
160
+ this.error = null
161
+ this.file = e.target.files[0]
162
+
163
+ if (this.file && this.validateFile) {
164
+ try {
165
+ let message = await this.validateFile(this.file)
166
+ if (message) {
167
+ this.error = message
168
+ }
169
+ } catch (error) {
170
+ this.error = error
171
+ }
172
+ }
173
+
174
+ if (!this.error) {
175
+ this.uploadFile(this.file)
176
+ }
177
+ },
178
+ async uploadFile(file) {
179
+ this.error = null
180
+ this.uploaded = 0
181
+ this.total = 0
182
+
183
+ this.uploader = new FileUploader()
184
+ this.uploader.on('start', () => {
185
+ this.uploading = true
186
+ })
187
+ this.uploader.on('progress', (data) => {
188
+ this.uploaded = data.uploaded
189
+ this.total = data.total
190
+ })
191
+ this.uploader.on('error', () => {
192
+ this.uploading = false
193
+ this.error = 'Error Uploading File'
194
+ })
195
+ this.uploader.on('finish', () => {
196
+ this.uploading = false
197
+ this.finishedUploading = true
198
+ })
199
+ this.uploader
200
+ .upload(file, this.uploadArgs || {})
201
+ .then((data) => {
202
+ this.$emit('success', data)
203
+ })
204
+ .catch((error) => {
205
+ this.uploading = false
206
+ let errorMessage = 'Error Uploading File'
207
+ if (error._server_messages) {
208
+ errorMessage = JSON.parse(
209
+ JSON.parse(error._server_messages)[0]
210
+ ).message
211
+ } else if (error.exc) {
212
+ errorMessage = JSON.parse(error.exc)[0].split('\n').slice(-2, -1)[0]
213
+ }
214
+ this.error = errorMessage
215
+ this.$emit('failure', error)
216
+ })
217
+ },
218
+ },
219
+ }
220
+ </script>
@@ -0,0 +1,183 @@
1
+ <template>
2
+ <TransitionRoot as="template" :show="open">
3
+ <Dialog
4
+ as="div"
5
+ class="fixed inset-0 z-10 overflow-y-auto"
6
+ @close="open = false"
7
+ >
8
+ <div
9
+ class="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0"
10
+ >
11
+ <TransitionChild
12
+ as="template"
13
+ enter="ease-out duration-300"
14
+ enter-from="opacity-0"
15
+ enter-to="opacity-100"
16
+ leave="ease-in duration-200"
17
+ leave-from="opacity-100"
18
+ leave-to="opacity-0"
19
+ >
20
+ <DialogOverlay
21
+ class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75"
22
+ />
23
+ </TransitionChild>
24
+
25
+ <!-- This element is to trick the browser into centering the modal contents. -->
26
+ <span
27
+ class="hidden sm:inline-block sm:align-middle sm:h-screen"
28
+ aria-hidden="true"
29
+ >
30
+ &#8203;
31
+ </span>
32
+ <TransitionChild
33
+ as="template"
34
+ enter="ease-out duration-300"
35
+ enter-from="opacity-0 translate-y-4 sm:-translate-y-12 sm:scale-95"
36
+ enter-to="opacity-100 translate-y-0 sm:scale-100"
37
+ leave="ease-in duration-200"
38
+ leave-from="opacity-100 translate-y-0 sm:scale-100"
39
+ leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
40
+ >
41
+ <div
42
+ class="inline-block overflow-hidden text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
43
+ >
44
+ <slot name="dialog-body">
45
+ <slot name="dialog-main">
46
+ <div class="px-4 py-5 bg-white sm:p-6">
47
+ <div>
48
+ <div
49
+ v-if="icon"
50
+ class="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto mb-3 rounded-full sm:mx-0 sm:h-10 sm:w-10 sm:mb-0 sm:mr-4"
51
+ :class="{
52
+ 'bg-yellow-100': icon.type === 'warning',
53
+ 'bg-blue-100': icon.type === 'info',
54
+ 'bg-red-100': icon.type === 'danger',
55
+ 'bg-green-100': icon.type === 'success',
56
+ }"
57
+ >
58
+ <FeatherIcon
59
+ :name="icon.name"
60
+ class="w-6 h-6 text-red-600"
61
+ :class="{
62
+ 'text-yellow-600': icon.type === 'warning',
63
+ 'text-blue-600': icon.type === 'info',
64
+ 'text-red-600': icon.type === 'danger',
65
+ 'text-green-600': icon.type === 'success',
66
+ }"
67
+ aria-hidden="true"
68
+ />
69
+ </div>
70
+ <div class="text-center sm:text-left">
71
+ <DialogTitle
72
+ as="h3"
73
+ class="text-lg font-medium leading-6 text-gray-900"
74
+ >
75
+ {{ options.title || 'Untitled' }}
76
+ </DialogTitle>
77
+ <div class="mt-2">
78
+ <slot name="dialog-content">
79
+ <p
80
+ class="text-sm text-gray-500"
81
+ v-if="options.message"
82
+ >
83
+ {{ options.message }}
84
+ </p>
85
+ </slot>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </slot>
91
+ <div
92
+ class="px-4 py-3 space-y-2 sm:space-x-reverse sm:space-x-3 sm:space-y-0 bg-gray-50 sm:px-6 sm:flex sm:flex-row-reverse"
93
+ v-if="options?.actions || $slots['dialog-actions']"
94
+ >
95
+ <slot
96
+ name="dialog-actions"
97
+ v-bind="{ close: () => (open = false) }"
98
+ >
99
+ <button
100
+ v-for="action in options.actions"
101
+ :key="action.label"
102
+ type="button"
103
+ class="inline-flex justify-center w-full px-4 py-2 text-base font-medium border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:w-auto sm:text-sm"
104
+ :class="{
105
+ 'text-gray-700 bg-white border-gray-300 hover:bg-gray-50 focus:ring-primary-500':
106
+ !action.type || action.type === 'default',
107
+ 'text-white bg-yellow-600 border-transparent hover:bg-yellow-700 focus:ring-yellow-500':
108
+ action.type === 'warning',
109
+ 'text-white bg-blue-600 border-transparent hover:bg-blue-700 focus:ring-blue-500':
110
+ action.type === 'info',
111
+ 'text-white bg-red-600 border-transparent hover:bg-red-700 focus:ring-red-500':
112
+ action.type === 'danger',
113
+ 'text-white bg-green-600 border-transparent hover:bg-green-700 focus:ring-green-500':
114
+ action.type === 'success',
115
+ }"
116
+ @click="
117
+ () => {
118
+ if (action.action && action.action === 'cancel') {
119
+ open = false
120
+ } else {
121
+ action.action()
122
+ }
123
+ }
124
+ "
125
+ >
126
+ {{ action.label }}
127
+ </button>
128
+ </slot>
129
+ </div>
130
+ </slot>
131
+ </div>
132
+ </TransitionChild>
133
+ </div>
134
+ </Dialog>
135
+ </TransitionRoot>
136
+ </template>
137
+
138
+ <script>
139
+ import { ref, computed } from 'vue'
140
+ import {
141
+ Dialog,
142
+ DialogOverlay,
143
+ DialogTitle,
144
+ TransitionChild,
145
+ TransitionRoot,
146
+ } from '@headlessui/vue'
147
+
148
+ export default {
149
+ name: 'NewDialog',
150
+ props: ['modelValue', 'options'],
151
+ emits: ['update:modelValue'],
152
+ components: {
153
+ Dialog,
154
+ DialogOverlay,
155
+ DialogTitle,
156
+ TransitionChild,
157
+ TransitionRoot,
158
+ },
159
+ setup(props, { emit }) {
160
+ let open = computed({
161
+ get: () => props.modelValue,
162
+ set: (val) => emit('update:modelValue', val),
163
+ })
164
+ return {
165
+ open,
166
+ }
167
+ },
168
+ computed: {
169
+ icon() {
170
+ if (!this.options?.icon) return null
171
+
172
+ let icon = this.options.icon
173
+ if (typeof icon === 'string') {
174
+ icon = {
175
+ name: icon,
176
+ type: 'info',
177
+ }
178
+ }
179
+ return icon
180
+ },
181
+ },
182
+ }
183
+ </script>
package/src/index.js CHANGED
@@ -5,9 +5,11 @@ export { default as Badge } from './components/Badge.vue'
5
5
  export { default as Button } from './components/Button.vue'
6
6
  export { default as Card } from './components/Card.vue'
7
7
  export { default as Dialog } from './components/Dialog.vue'
8
+ export { default as NewDialog } from './components/NewDialog.vue'
8
9
  export { default as Dropdown } from './components/Dropdown.vue'
9
10
  export { default as ErrorMessage } from './components/ErrorMessage.vue'
10
11
  export { default as FeatherIcon } from './components/FeatherIcon.vue'
12
+ export { default as FileUploader } from './components/FileUploader.vue'
11
13
  export { default as GreenCheckIcon } from './components/GreenCheckIcon.vue'
12
14
  export { default as Input } from './components/Input.vue'
13
15
  export { default as Link } from './components/Link.vue'
@@ -23,7 +25,8 @@ export { default as SuccessMessage } from './components/SuccessMessage.vue'
23
25
  export { default as onOutsideClickDirective } from './directives/onOutsideClick.js'
24
26
 
25
27
  // utilities
26
- export { default as call } from './utils/call.js'
28
+ export { default as call, createCall } from './utils/call.js'
29
+ export { default as debounce } from './utils/debounce.js'
27
30
 
28
31
  // plugin
29
32
  export { default as FrappeUI } from './utils/plugin.js'
package/src/utils/call.js CHANGED
@@ -1,4 +1,4 @@
1
- export default async function call(method, args, _headers) {
1
+ export default async function call(method, args, options={}) {
2
2
  if (!args) {
3
3
  args = {}
4
4
  }
@@ -9,7 +9,7 @@ export default async function call(method, args, _headers) {
9
9
  'Content-Type': 'application/json; charset=utf-8',
10
10
  'X-Frappe-Site-Name': window.location.hostname,
11
11
  },
12
- _headers
12
+ options.headers || {}
13
13
  )
14
14
 
15
15
  if (window.csrf_token && window.csrf_token !== '{{ csrf_token }}') {
@@ -49,6 +49,7 @@ export default async function call(method, args, _headers) {
49
49
  let e = new Error(errorParts.join('\n'))
50
50
  e.exc_type = error.exc_type
51
51
  e.exc = exception
52
+ e.status = res.status
52
53
  e.messages = error._server_messages
53
54
  ? JSON.parse(error._server_messages)
54
55
  : []
@@ -67,6 +68,16 @@ export default async function call(method, args, _headers) {
67
68
  : ['Internal Server Error']
68
69
  }
69
70
 
71
+ if (options.onError) {
72
+ options.onError({ response: res, status: res.status, error: e })
73
+ }
74
+
70
75
  throw e
71
76
  }
72
77
  }
78
+
79
+ export function createCall(options) {
80
+ return function customCall(method, args) {
81
+ return call(method, args, options)
82
+ }
83
+ }
@@ -0,0 +1,15 @@
1
+ export default function debounce(func, wait, immediate) {
2
+ var timeout
3
+ return function () {
4
+ var context = this,
5
+ args = arguments
6
+ var later = function () {
7
+ timeout = null
8
+ if (!immediate) func.apply(context, args)
9
+ }
10
+ var callNow = immediate && !timeout
11
+ clearTimeout(timeout)
12
+ timeout = setTimeout(later, wait)
13
+ if (callNow) func.apply(context, args)
14
+ }
15
+ }
@@ -11,10 +11,11 @@ let defaultOptions = {
11
11
  export default {
12
12
  install(app, options = {}) {
13
13
  options = Object.assign({}, defaultOptions, options)
14
- options.resources && app.use(resources)
14
+ options.resources && app.use(resources, options.resources)
15
15
 
16
16
  if (options.call) {
17
- app.config.globalProperties.$call = call
17
+ let callFunction = typeof options.call == 'function' ? options.call : call
18
+ app.config.globalProperties.$call = callFunction
18
19
  }
19
20
  if (options.socketio) {
20
21
  app.config.globalProperties.$socket = socket
@@ -1,11 +1,19 @@
1
- import call from './call'
2
- import { onMounted, reactive, watchEffect } from 'vue'
1
+ import { call, debounce } from 'frappe-ui'
2
+ import { reactive, watch } from 'vue'
3
3
 
4
4
  let cached = {}
5
5
 
6
- function createResource(options, vm) {
7
- if (options.cache && cached[options.cache]) {
8
- return cached[options.cache]
6
+ function createResource(options, vm, getResource) {
7
+ let cacheKey = null
8
+ if (options.cache) {
9
+ cacheKey = options.cache
10
+ if (typeof cacheKey === 'string') {
11
+ cacheKey = [cacheKey]
12
+ }
13
+ cacheKey = JSON.stringify(cacheKey)
14
+ if (cached[cacheKey]) {
15
+ return cached[cacheKey]
16
+ }
9
17
  }
10
18
 
11
19
  if (typeof options == 'string') {
@@ -15,44 +23,40 @@ function createResource(options, vm) {
15
23
  }
16
24
  }
17
25
 
26
+ let resourceFetcher = getResource || call
27
+ let fetchFunction = options.debounce
28
+ ? debounce(fetch, options.debounce)
29
+ : fetch
30
+
18
31
  let out = reactive({
19
32
  data: options.initialData || null,
33
+ previousData: null,
20
34
  loading: false,
21
35
  fetched: false,
22
36
  error: null,
37
+ auto: options.auto,
23
38
  params: null,
24
- fetch,
25
- submit,
39
+ fetch: fetchFunction,
40
+ submit: fetchFunction,
41
+ update,
26
42
  })
27
43
 
28
- if (typeof options.params == 'function') {
29
- watchEffect(() => {
30
- out.params = options.params(vm)
31
- if (options.auto) {
32
- fetch()
33
- }
34
- })
35
- } else {
36
- out.params = options.params
37
- }
38
-
39
- onMounted(() => {
40
- if (options.auto) {
41
- fetch()
44
+ async function fetch(params) {
45
+ if (params instanceof Event) {
46
+ params = null
42
47
  }
43
- })
44
-
45
- async function fetch() {
48
+ out.params = params || options.params
46
49
  out.loading = true
47
- out.data = options.initialData || null
48
50
  try {
49
- let data = await call(options.method, out.params)
51
+ let data = await resourceFetcher(options.method, params || options.params)
52
+ out.previousData = out.data || null
50
53
  out.data = data
51
54
  out.fetched = true
52
55
  if (options.onSuccess) {
53
56
  options.onSuccess.call(vm, data)
54
57
  }
55
58
  } catch (error) {
59
+ console.error(error)
56
60
  out.error = error
57
61
  if (options.onError) {
58
62
  options.onError.call(vm, error)
@@ -61,33 +65,78 @@ function createResource(options, vm) {
61
65
  out.loading = false
62
66
  }
63
67
 
64
- function submit(params) {
65
- out.params = params
66
- return fetch()
68
+ function update({ method, params, auto }) {
69
+ if (method && method !== options.method) {
70
+ options.method = method
71
+ }
72
+ if (params && params !== options.params) {
73
+ options.params = params
74
+ }
75
+ if (auto !== undefined && auto !== out.auto) {
76
+ out.auto = auto
77
+ }
67
78
  }
68
79
 
69
- if (options.cache && !cached[options.cache]) {
70
- cached[options.cache] = out
80
+ if (cacheKey && !cached[cacheKey]) {
81
+ cached[cacheKey] = out
71
82
  }
72
83
 
73
84
  return out
74
85
  }
75
86
 
76
- let Resources = {
87
+ let createMixin = (mixinOptions) => ({
77
88
  created() {
78
89
  if (this.$options.resources) {
79
- this._resources = {}
90
+ this._resources = reactive({})
80
91
  for (let key in this.$options.resources) {
81
- this._resources[key] = createResource(
82
- this.$options.resources[key],
83
- this
84
- )
92
+ let options = this.$options.resources[key]
93
+
94
+ if (typeof options == 'function') {
95
+ watch(
96
+ () => options.call(this),
97
+ (updatedOptions, oldVal) => {
98
+ let changed =
99
+ !oldVal ||
100
+ JSON.stringify(updatedOptions) !== JSON.stringify(oldVal)
101
+
102
+ if (!changed) return
103
+
104
+ let resource = this._resources[key]
105
+ if (!resource) {
106
+ resource = createResource(
107
+ updatedOptions,
108
+ this,
109
+ mixinOptions.getResource
110
+ )
111
+ this._resources[key] = resource
112
+ } else {
113
+ resource.update(updatedOptions)
114
+ }
115
+ if (resource.auto) {
116
+ resource.fetch()
117
+ }
118
+ },
119
+ {
120
+ immediate: true,
121
+ }
122
+ )
123
+ } else {
124
+ let resource = createResource(options, this, mixinOptions.getResource)
125
+ this._resources[key] = resource
126
+ if (resource.auto) {
127
+ resource.fetch()
128
+ }
129
+ }
85
130
  }
86
131
  }
87
132
  },
88
133
  methods: {
89
- $resource(key) {
90
- return this._resources[key]
134
+ $refetchResource(cacheKey) {
135
+ let key = JSON.stringify(cacheKey)
136
+ if (cached[key]) {
137
+ let resource = cached[key]
138
+ resource.fetch()
139
+ }
91
140
  },
92
141
  },
93
142
  computed: {
@@ -95,10 +144,11 @@ let Resources = {
95
144
  return this._resources
96
145
  },
97
146
  },
98
- }
147
+ })
99
148
 
100
149
  export default {
101
150
  install(app, options) {
102
- app.mixin(Resources)
151
+ let resourceMixin = createMixin(options)
152
+ app.mixin(resourceMixin)
103
153
  },
104
154
  }
@@ -1,5 +1,3 @@
1
- const plugin = require('tailwindcss/plugin')
2
-
3
1
  module.exports = {
4
2
  theme: {
5
3
  extend: {
@@ -88,7 +86,7 @@ module.exports = {
88
86
  plugins: [
89
87
  require('@tailwindcss/forms'),
90
88
  require('@tailwindcss/typography'),
91
- plugin(function ({ addUtilities, theme }) {
89
+ require('tailwindcss/plugin')(function ({ addUtilities, theme }) {
92
90
  addUtilities({
93
91
  '.bg-gradient-blue': {
94
92
  'background-image': `linear-gradient(180deg,#2c9af1 0%, ${theme(