@ditojs/admin 3.2.6 → 3.3.0

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.
Files changed (112) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +6012 -5827
  3. package/dist/dito-admin.umd.js +3 -7
  4. package/package.json +18 -17
  5. package/src/DitoAdmin.js +44 -24
  6. package/src/DitoContext.js +28 -2
  7. package/src/DitoTypeComponent.js +1 -0
  8. package/src/appState.js +6 -3
  9. package/src/components/DitoAccount.vue +35 -27
  10. package/src/components/DitoAffixes.vue +9 -2
  11. package/src/components/DitoClipboard.vue +59 -93
  12. package/src/components/DitoConfirmMessage.vue +18 -0
  13. package/src/components/DitoContainer.vue +56 -18
  14. package/src/components/DitoCreateButton.vue +36 -84
  15. package/src/components/DitoDialog.vue +20 -15
  16. package/src/components/DitoDragHandle.vue +44 -0
  17. package/src/components/DitoDraggable.vue +43 -16
  18. package/src/components/DitoEditButtons.vue +48 -24
  19. package/src/components/DitoErrors.vue +9 -56
  20. package/src/components/DitoForm.vue +82 -42
  21. package/src/components/DitoInputAffixes.vue +50 -0
  22. package/src/components/DitoLabel.vue +1 -15
  23. package/src/components/DitoMenu.vue +45 -52
  24. package/src/components/DitoNotifications.vue +38 -14
  25. package/src/components/DitoOptionList.vue +81 -0
  26. package/src/components/DitoOptionsEditButtons.vue +57 -0
  27. package/src/components/DitoPagination.vue +1 -8
  28. package/src/components/DitoPane.vue +51 -39
  29. package/src/components/DitoPanel.vue +12 -26
  30. package/src/components/DitoPanels.vue +11 -5
  31. package/src/components/DitoResizeHandle.vue +47 -0
  32. package/src/components/DitoRoot.vue +113 -320
  33. package/src/components/DitoSchema.vue +72 -63
  34. package/src/components/DitoSchemaInlined.vue +8 -9
  35. package/src/components/DitoScopes.vue +14 -18
  36. package/src/components/DitoSidebar.vue +4 -29
  37. package/src/components/DitoSourceItem.vue +114 -0
  38. package/src/components/DitoTableCell.vue +4 -0
  39. package/src/components/DitoTableHead.vue +15 -17
  40. package/src/components/DitoTabs.vue +25 -0
  41. package/src/components/DitoTrail.vue +25 -25
  42. package/src/components/DitoTreeItem.vue +102 -141
  43. package/src/components/DitoUploadFile.vue +5 -2
  44. package/src/components/DitoView.vue +33 -24
  45. package/src/index.js +4 -1
  46. package/src/mixins/DitoMixin.js +77 -110
  47. package/src/mixins/DomMixin.js +12 -14
  48. package/src/mixins/EmitterMixin.js +3 -2
  49. package/src/mixins/ItemMixin.js +38 -94
  50. package/src/mixins/OptionsMixin.js +38 -25
  51. package/src/mixins/ResourceMixin.js +182 -111
  52. package/src/mixins/RouteMixin.js +61 -44
  53. package/src/mixins/SchemaParentMixin.js +4 -1
  54. package/src/mixins/SourceMixin.js +118 -100
  55. package/src/mixins/TypeMixin.js +42 -40
  56. package/src/mixins/ValidationMixin.js +16 -4
  57. package/src/mixins/ValidatorMixin.js +1 -1
  58. package/src/styles/_settings.scss +4 -0
  59. package/src/styles/_tippy.scss +0 -11
  60. package/src/styles/style.scss +0 -1
  61. package/src/types/DitoTypeButton.vue +11 -14
  62. package/src/types/DitoTypeCheckboxes.vue +35 -17
  63. package/src/types/DitoTypeCode.vue +19 -41
  64. package/src/types/DitoTypeColor.vue +67 -19
  65. package/src/types/DitoTypeDate.vue +11 -14
  66. package/src/types/DitoTypeList.vue +50 -62
  67. package/src/types/DitoTypeMarkup.vue +41 -54
  68. package/src/types/DitoTypeMultiselect.vue +82 -52
  69. package/src/types/DitoTypeNumber.vue +12 -15
  70. package/src/types/DitoTypeObject.vue +4 -38
  71. package/src/types/DitoTypeRadio.vue +19 -6
  72. package/src/types/DitoTypeSection.vue +23 -9
  73. package/src/types/DitoTypeSelect.vue +39 -23
  74. package/src/types/DitoTypeSlider.vue +9 -1
  75. package/src/types/DitoTypeSwitch.vue +1 -1
  76. package/src/types/DitoTypeText.vue +11 -14
  77. package/src/types/DitoTypeTreeList.vue +46 -8
  78. package/src/types/DitoTypeUpload.vue +190 -67
  79. package/src/utils/DataModel.js +368 -146
  80. package/src/utils/FileDropTargets.js +97 -0
  81. package/src/utils/ItemStores.js +31 -0
  82. package/src/utils/ListQuery.js +115 -0
  83. package/src/utils/LoadingTracker.js +76 -0
  84. package/src/utils/SchemaGraph.js +3 -2
  85. package/src/utils/Session.js +151 -0
  86. package/src/utils/ViewRegistry.js +85 -0
  87. package/src/utils/accessor.js +12 -4
  88. package/src/utils/affixes.js +33 -0
  89. package/src/utils/changeOnceEdited.js +33 -0
  90. package/src/utils/clipboard.js +55 -0
  91. package/src/utils/data.js +10 -5
  92. package/src/utils/dialogs.js +59 -0
  93. package/src/utils/dragResize.js +61 -0
  94. package/src/utils/filter.js +36 -2
  95. package/src/utils/html.js +9 -0
  96. package/src/utils/list.js +10 -0
  97. package/src/utils/options.js +4 -0
  98. package/src/utils/registration.js +18 -0
  99. package/src/utils/request.js +108 -13
  100. package/src/utils/route.js +6 -17
  101. package/src/utils/schema/data.js +29 -2
  102. package/src/utils/schema/events.js +19 -0
  103. package/src/utils/schema/item.js +114 -0
  104. package/src/utils/schema/lookup.js +39 -14
  105. package/src/utils/schema/setup.js +5 -5
  106. package/src/utils/schema/validation.js +16 -2
  107. package/src/utils/uid.js +105 -8
  108. package/src/validators/_decimals.js +16 -5
  109. package/types/index.d.ts +168 -41
  110. package/src/mixins/LoadingMixin.js +0 -23
  111. package/src/mixins/PulldownMixin.js +0 -72
  112. package/src/styles/_pulldown.scss +0 -38
@@ -6,7 +6,7 @@
6
6
  )
7
7
  Transition(name="dito-drag")
8
8
  .dito-drag-overlay(
9
- v-if="isDraggingFiles"
9
+ v-if="fileDropTargets.isDraggingFiles"
10
10
  )
11
11
  TransitionGroup(name="dito-dialog")
12
12
  DitoDialog(
@@ -20,10 +20,7 @@
20
20
  @remove="removeDialog(key)"
21
21
  )
22
22
  DitoNavigation
23
- main.dito-page.dito-scroll-parent(
24
- v-resize="onResizePage"
25
- :class="pageClasses"
26
- )
23
+ main.dito-page.dito-scroll-parent(:class="appState.pageClass")
27
24
  DitoHeader(
28
25
  :spinner="options.spinner"
29
26
  :isLoading="isLoading"
@@ -33,53 +30,59 @@
33
30
  DitoAccount(
34
31
  v-if="user"
35
32
  )
36
- a.dito-login(
37
- v-else-if="allowLogin"
38
- @click="rootComponent.login()"
33
+ button.dito-login(
34
+ v-else-if="hasSessionStarted"
35
+ type="button"
36
+ @click="session.login()"
39
37
  )
40
38
  span Login
41
39
  DitoNotifications(ref="notifications")
42
40
  </template>
43
41
 
44
42
  <script>
43
+ import { markRaw } from 'vue'
45
44
  import { delegate as tippyDelegate } from 'tippy.js'
46
- import { mapConcurrently } from '@ditojs/utils'
45
+ import { deprecate } from '@ditojs/utils'
47
46
  import DitoComponent from '../DitoComponent.js'
48
47
  import DomMixin from '../mixins/DomMixin.js'
49
- import DitoUser from '../DitoUser.js'
50
- import DitoView from '../components/DitoView.vue'
51
48
  import DitoDialog from './DitoDialog.vue'
52
- import {
53
- setupView,
54
- resolveViews,
55
- setupSchemaComponents
56
- } from '../utils/schema/setup.js'
49
+ import { setupSchemaComponents } from '../utils/schema/setup.js'
50
+ import { LoadingTracker } from '../utils/LoadingTracker.js'
51
+ import { FileDropTargets } from '../utils/FileDropTargets.js'
57
52
 
58
53
  // @vue/component
59
54
  export default DitoComponent.component('DitoRoot', {
60
55
  mixins: [DomMixin],
61
56
  components: { DitoDialog },
57
+ inject: ['viewRegistry'],
62
58
 
63
59
  provide() {
64
60
  return {
65
- $views: () => this.resolvedViews
61
+ $loadingTracker: () => this.loadingTracker,
62
+ $fileDropTargets: () => this.fileDropTargets
66
63
  }
67
64
  },
68
65
 
69
66
  props: {
70
- unresolvedViews: { type: [Object, Function, Promise], required: true },
71
67
  options: { type: Object, default: () => ({}) }
72
68
  },
73
69
 
74
70
  data() {
75
71
  return {
76
- resolvedViews: {},
77
- removeRoutes: null,
78
72
  dialogs: {},
79
- pageWidth: 0,
80
- loadingCount: 0,
81
- allowLogin: false,
82
- isDraggingFiles: false
73
+ // Tracks all pending requests of the admin, for the header's spinner:
74
+ loadingTracker: markRaw(new LoadingTracker()),
75
+ // The functions that end the loading operations begun by the deprecated
76
+ // `registerLoading(true)`, ended in reverse by `registerLoading(false)`:
77
+ registeredLoadingEnds: markRaw([]),
78
+ // Tracks the files dragged over the admin, for the uploads to drop them
79
+ // on, see `DitoTypeUpload`:
80
+ fileDropTargets: markRaw(new FileDropTargets()),
81
+ // Whether `session.start()` finished, after which the login link is
82
+ // shown without a user, e.g. once the login dialog was canceled:
83
+ hasSessionStarted: false,
84
+ // Detaches the login dialog and notifications from the session:
85
+ detachSessionUserInterface: null
83
86
  }
84
87
  },
85
88
 
@@ -89,20 +92,7 @@ export default DitoComponent.component('DitoRoot', {
89
92
  },
90
93
 
91
94
  isLoading() {
92
- return this.loadingCount > 0
93
- },
94
-
95
- pageClasses() {
96
- const prefix = 'dito-page'
97
- // NOTE: Keep synced with $content-width in SCSS:
98
- const contentWidth = 900
99
- return [
100
- this.appState.pageClass,
101
- {
102
- [`${prefix}--width-80`]: this.pageWidth <= contentWidth * 0.8,
103
- [`${prefix}--width-60`]: this.pageWidth <= contentWidth * 0.6
104
- }
105
- ]
95
+ return this.loadingTracker.isLoading
106
96
  }
107
97
  },
108
98
 
@@ -111,10 +101,19 @@ export default DitoComponent.component('DitoRoot', {
111
101
  // With hot-reloading, it looks like destroyed hooks aren't always called
112
102
  // for route components so reset the array of registered components instead.
113
103
  this.appState.routeComponents = []
104
+ this.detachSessionUserInterface = this.session.attachUserInterface({
105
+ requestLoginData: () => this.showLoginDialog(),
106
+ notify: options => this.notify(options)
107
+ })
108
+ },
109
+
110
+ unmounted() {
111
+ this.detachSessionUserInterface()
114
112
  },
115
113
 
116
114
  async mounted() {
117
- this.setupDragAndDrop()
115
+ // Only the overlay is rendered here, the uploads handle the dropped files.
116
+ this.domOn(document, this.fileDropTargets.getDocumentEventHandlers())
118
117
 
119
118
  tippyDelegate(this.$el, {
120
119
  target: '.dito-info',
@@ -127,164 +126,98 @@ export default DitoComponent.component('DitoRoot', {
127
126
  onShow: instance => instance.setContent(instance.reference.dataset.info)
128
127
  })
129
128
 
130
- // Clear the label marked as active on all mouse and keyboard events, except
131
- // the ones that DitoLabel itself intercepts.
132
- this.domOn(document, {
133
- click: event => {
134
- if (!event.target.closest('.dito-label')) {
135
- this.appState.activeLabel = null
136
- }
137
- },
138
-
139
- keyup: event => {
140
- if (event.code === 'Tab') {
141
- this.appState.activeLabel = null
129
+ // Warn before the page is reloaded or closed with unsaved changes. Route
130
+ // changes within the admin are guarded by `RouteMixin` instead.
131
+ this.domOn(window, {
132
+ beforeunload: event => {
133
+ if (this.hasUnsavedChanges()) {
134
+ event.preventDefault()
135
+ // Required by older browsers to show the warning:
136
+ event.returnValue = ''
142
137
  }
143
138
  }
144
139
  })
145
140
 
146
141
  try {
147
- this.allowLogin = false
148
- if (await this.fetchUser()) {
149
- await this.resolveViews()
150
- } else {
151
- await this.login()
152
- }
153
- } catch (err) {
154
- console.error(err)
142
+ await this.session.start()
143
+ } catch (error) {
144
+ console.error(error)
155
145
  }
156
- this.allowLogin = true
146
+ this.hasSessionStarted = true
157
147
  },
158
148
 
159
149
  methods: {
160
- setupDragAndDrop() {
161
- // This code only happens the visual effects around dragging and dropping
162
- // files into a `DitoTypeUpload` component. The actual uploading is
163
- // handled by the `DitoTypeUpload` component itself.
164
-
165
- let dragCount = 0
166
- let uploads = []
167
-
168
- const toggleDropTargetClass = enabled => {
169
- for (const upload of uploads) {
170
- upload
171
- .closest('.dito-container')
172
- .classList.toggle('dito-drop-target', enabled)
173
- }
174
- if (!enabled) {
175
- uploads = []
176
- }
177
- }
178
-
179
- const setDraggingFiles = enabled => {
180
- this.isDraggingFiles = enabled
181
- if (enabled) {
182
- toggleDropTargetClass(true)
183
- } else {
184
- setTimeout(() => toggleDropTargetClass(false), 150)
185
- }
150
+ /**
151
+ * @deprecated `isLoading` is tracked by `loadingTracker` now. Use
152
+ * `loadingTracker.begin()` and call the function that it returns, or
153
+ * `loadingTracker.track(callback)` instead.
154
+ */
155
+ registerLoading(isLoading) {
156
+ deprecate(
157
+ 'DitoRoot.registerLoading() is deprecated, use ' +
158
+ '`loadingTracker.begin()` or `loadingTracker.track()` instead.'
159
+ )
160
+ if (isLoading) {
161
+ this.registeredLoadingEnds.push(this.loadingTracker.begin())
162
+ } else {
163
+ this.registeredLoadingEnds.pop()?.()
186
164
  }
187
-
188
- this.domOn(document, {
189
- dragenter: event => {
190
- if (!dragCount && event.dataTransfer) {
191
- uploads = document.querySelectorAll('.dito-upload')
192
- const hasUploads = uploads.length > 0
193
- event.dataTransfer.effectAllowed = hasUploads ? 'copy' : 'none'
194
- if (hasUploads) {
195
- setDraggingFiles(true)
196
- } else {
197
- event.preventDefault()
198
- event.stopPropagation()
199
- return
200
- }
201
- }
202
- dragCount++
203
- },
204
-
205
- dragleave: event => {
206
- dragCount--
207
- if (!dragCount && event.dataTransfer) {
208
- setDraggingFiles(false)
209
- }
210
- },
211
-
212
- dragover: event => {
213
- if (event.dataTransfer) {
214
- const canDrop = event.target.closest(
215
- '.dito-container:has(.dito-upload)'
216
- )
217
- event.dataTransfer.dropEffect = canDrop ? 'copy' : 'none'
218
- if (!canDrop) {
219
- event.preventDefault()
220
- event.stopPropagation()
221
- }
222
- }
223
- },
224
-
225
- drop: event => {
226
- dragCount = 0
227
- if (event.dataTransfer) {
228
- setDraggingFiles(false)
229
- }
230
- }
231
- })
232
165
  },
233
166
 
234
- notify({ type = 'info', title, text, error, duration } = {}) {
235
- this.notifications.notify({ type, title, text, error, duration })
167
+ notify(options) {
168
+ this.notifications.notify(options)
236
169
  },
237
170
 
238
171
  closeNotifications() {
239
172
  this.notifications.destroyAll()
240
173
  },
241
174
 
242
- registerLoading(isLoading) {
243
- this.loadingCount += isLoading ? 1 : -1
175
+ // Returns true if any of the route components has unsaved changes, e.g. a
176
+ // form with a dirty data model or a view with edited components, see
177
+ // `RouteMixin`.
178
+ hasUnsavedChanges() {
179
+ return this.appState.routeComponents.some(
180
+ routeComponent => routeComponent.hasUnsavedChanges
181
+ )
244
182
  },
245
183
 
246
- showDialog({ components, buttons, data, settings }) {
184
+ async showDialog({ components, buttons, data, settings }) {
247
185
  // Shows a dito-dialog component and wraps it in a promise so that the
248
186
  // buttons in the dialog can use `dialog.resolve()` and `dialog.reject()`
249
187
  // to close the modal dialog and resolve / reject the promise at once.
250
- return new Promise(
251
- async (resolve, reject) => {
252
- // Process components to resolve async schemas.
253
- const routes = []
254
- await setupSchemaComponents(
255
- this.api,
256
- { type: 'dialog', components },
257
- routes,
258
- 0
259
- )
260
- if (routes.length > 0) {
261
- throw new Error(
262
- 'Dialogs do not support components that produce routes'
263
- )
264
- }
265
- const key = `dialog-${++dialogId}`
266
- this.dialogs[key] = {
267
- components,
268
- buttons,
269
- data,
270
- settings,
271
- promise: { resolve, reject }
272
- }
273
- }
188
+ // Process components to resolve async schemas first, so that errors
189
+ // reject the returned promise.
190
+ const routes = []
191
+ await setupSchemaComponents(
192
+ this.api,
193
+ { type: 'dialog', components },
194
+ routes,
195
+ 0
274
196
  )
197
+ if (routes.length > 0) {
198
+ throw new Error('Dialogs do not support components that produce routes')
199
+ }
200
+ return new Promise((resolve, reject) => {
201
+ const key = `dialog-${++dialogId}`
202
+ this.dialogs[key] = {
203
+ components,
204
+ buttons,
205
+ data,
206
+ settings,
207
+ promise: { resolve, reject }
208
+ }
209
+ })
275
210
  },
276
211
 
277
212
  removeDialog(key) {
278
213
  delete this.dialogs[key]
279
214
  },
280
215
 
281
- async login() {
282
- this.allowLogin = true
283
- const {
284
- additionalComponents,
285
- redirectAfterLogin
286
- } = this.options.login || {}
287
- const loginData = await this.showDialog({
216
+ // Shows the login dialog for `Session.login()`, and returns the entered
217
+ // credentials, or `null` if the user cancels.
218
+ showLoginDialog() {
219
+ const { additionalComponents } = this.options.login || {}
220
+ return this.showDialog({
288
221
  components: {
289
222
  username: {
290
223
  type: 'text',
@@ -311,141 +244,11 @@ export default DitoComponent.component('DitoRoot', {
311
244
  }
312
245
  }
313
246
  })
314
- if (loginData) {
315
- try {
316
- const response = await this.sendRequest({
317
- resource: this.api.users.login,
318
- data: loginData,
319
- internal: true
320
- })
321
- if (redirectAfterLogin) {
322
- location.replace(redirectAfterLogin)
323
- } else {
324
- this.setUser(response.data.user)
325
- await this.resolveViews()
326
- }
327
- } catch (err) {
328
- const error = err.response?.data?.error || err
329
- this.notify({
330
- type: 'error',
331
- error,
332
- title: 'Authentication Error',
333
- text: error
334
- })
335
- this.login()
336
- }
337
- }
338
- },
339
-
340
- navigateHome() {
341
- return this.navigate('/')
342
- },
343
-
344
- async logout() {
345
- try {
346
- const response = await this.sendRequest({
347
- resource: this.api.users.logout,
348
- internal: true
349
- })
350
- if (response.data.success) {
351
- this.setUser(null)
352
- this.navigateHome()
353
- }
354
- } catch (err) {
355
- console.error(err)
356
- }
357
- },
358
-
359
- async fetchUser() {
360
- let user = null
361
- try {
362
- const response = await this.sendRequest({
363
- resource: this.api.users.session,
364
- internal: true
365
- })
366
- user = response.data.user || null
367
- } catch (err) {
368
- const error = err.response?.data?.error || err
369
- this.notify({
370
- type: 'error',
371
- error,
372
- title: 'Authentication Error',
373
- text: error
374
- })
375
- }
376
- this.setUser(user)
377
- return user
378
- },
379
-
380
- setUser(user) {
381
- this.appState.user = (
382
- user &&
383
- Object.setPrototypeOf(user, DitoUser.prototype)
384
- )
385
- // Clear resolved views when user is logged out.
386
- if (!user) {
387
- this.resolvedViews = {}
388
- this.navigateHome()
389
- }
390
- },
391
-
392
- async ensureUser() {
393
- if (!(await this.fetchUser())) {
394
- await this.login()
395
- }
396
- },
397
-
398
- async resolveViews() {
399
- try {
400
- this.resolvedViews = await resolveViews(this.unresolvedViews)
401
- } catch (error) {
402
- if (!error.request) {
403
- console.error(error)
404
- }
405
- return this.login()
406
- }
407
- // Collect all routes from the root schema components
408
- const routes = await mapConcurrently(
409
- Object.entries(this.resolvedViews),
410
- ([name, schema]) => setupView(DitoView, this.api, schema, name)
411
- )
412
- // Now that the routes are loaded, replace all existing routes with the
413
- // new routes, and restore the current path.
414
- const { fullPath } = this.$route
415
- this.removeRoutes?.()
416
- this.removeRoutes = addRoutes(this.$router, [
417
- {
418
- name: 'root',
419
- path: '/',
420
- components: {}
421
- },
422
- ...routes.flat()
423
- ])
424
- this.$router.replace(fullPath)
425
- },
426
-
427
- onResizePage({ contentRect: { width } }) {
428
- this.pageWidth = width
429
247
  }
430
248
  }
431
249
  })
432
250
 
433
251
  let dialogId = 0
434
-
435
- function addRoutes(router, routes) {
436
- const removers = []
437
- for (const route of routes) {
438
- removers.push(
439
- router.addRoute(route)
440
- )
441
- }
442
-
443
- return () => {
444
- for (const remove of removers) {
445
- remove()
446
- }
447
- }
448
- }
449
252
  </script>
450
253
 
451
254
  <style lang="scss">
@@ -459,6 +262,9 @@ function addRoutes(router, routes) {
459
262
  }
460
263
 
461
264
  .dito-page {
265
+ // For the layout of `DitoContainer` in narrow pages:
266
+ container: dito-page / inline-size;
267
+
462
268
  --max-content-width: #{$content-width};
463
269
  --max-page-width: calc(var(--max-content-width) + 2 * #{$content-padding});
464
270
 
@@ -473,9 +279,15 @@ function addRoutes(router, routes) {
473
279
  }
474
280
  }
475
281
 
476
- .dito-account,
477
282
  .dito-login {
478
- cursor: pointer;
283
+ padding: 0;
284
+ border: 0;
285
+ color: inherit;
286
+ background: none;
287
+
288
+ &:focus-visible {
289
+ box-shadow: $shadow-focus;
290
+ }
479
291
  }
480
292
 
481
293
  .dito-drag-overlay {
@@ -490,35 +302,16 @@ function addRoutes(router, routes) {
490
302
  backdrop-filter: blur(8px);
491
303
  }
492
304
 
493
- .dito-drop-target {
494
- --shadow-alpha: 0.25;
495
-
496
- background: $content-color-background;
497
- border-radius: $border-radius;
498
- z-index: $z-index-drag-overlay + 1;
499
- filter: drop-shadow(0 4px 8px rgb(0, 0, 0, var(--shadow-alpha)));
500
- }
501
-
502
305
  .dito-drag-enter-active,
503
306
  .dito-drag-leave-active {
504
- $duration: 0.15s;
505
-
506
307
  transition:
507
- opacity $duration,
508
- backdrop-filter $duration;
509
-
510
- ~ * .dito-drop-target {
511
- transition: filter $duration;
512
- }
308
+ opacity $drag-overlay-duration,
309
+ backdrop-filter $drag-overlay-duration;
513
310
  }
514
311
 
515
312
  .dito-drag-enter-from,
516
313
  .dito-drag-leave-to {
517
314
  opacity: 0;
518
315
  backdrop-filter: blur(0);
519
-
520
- ~ * .dito-drop-target {
521
- --shadow-alpha: 0;
522
- }
523
316
  }
524
317
  </style>