@bildvitta/quasar-ui-asteroid 3.20.0-beta.14-alpha.3 → 3.20.0-beta.14

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 (27) hide show
  1. package/package.json +2 -2
  2. package/src/components/actions-menu/QasActionsMenu.vue +4 -4
  3. package/src/components/board-generator/QasBoardGenerator.vue +321 -100
  4. package/src/components/board-generator/QasBoardGenerator.yml +56 -2
  5. package/src/components/board-generator/private/PvBoardGeneratorCardsContainer.vue +0 -1
  6. package/src/components/box/QasBox.yml +5 -0
  7. package/src/components/card/QasCard.vue +1 -0
  8. package/src/components/date-time-input/QasDateTimeInput.vue +30 -6
  9. package/src/components/dialog/QasDialog.vue +1 -3
  10. package/src/components/dialog/QasDialog.yml +8 -0
  11. package/src/components/expansion-item/QasExpansionItem.yml +5 -0
  12. package/src/components/form-generator/QasFormGenerator.yml +5 -0
  13. package/src/components/header/QasHeader.yml +5 -0
  14. package/src/components/lazy-loading-components/QasLazyLoadingComponents.vue +0 -7
  15. package/src/components/lazy-loading-components/QasLazyLoadingComponents.yml +34 -9
  16. package/src/components/select-list-dialog/QasSelectListDialog.vue +1 -1
  17. package/src/components/stepper/QasStepper.vue +24 -2
  18. package/src/composables/use-screen.js +17 -1
  19. package/src/helpers/filters.js +1 -1
  20. package/src/helpers/set-scroll-gradient.js +5 -2
  21. package/src/mixins/search-filter.js +1 -1
  22. package/src/plugins/delete/Delete.yml +1 -1
  23. package/src/plugins/dialog/Dialog.yml +1 -1
  24. package/src/plugins/notify-error/NotifyError.yml +1 -1
  25. package/src/plugins/notify-success/NotifySuccess.yml +1 -1
  26. package/src/plugins/screen/Screen.js +17 -1
  27. package/src/plugins/screen/Screen.yml +5 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bildvitta/quasar-ui-asteroid",
3
3
  "description": "Asteroid",
4
- "version": "3.20.0-beta.14-alpha.3",
4
+ "version": "3.20.0-beta.14",
5
5
  "author": "Bild & Vitta <systemteam@bild.com.br>",
6
6
  "license": "MIT",
7
7
  "main": "./src/asteroid.js",
@@ -55,7 +55,7 @@
55
55
  "lodash-es": "^4.17.21",
56
56
  "pica": "^9.0.1",
57
57
  "signature_pad": "^4.1.5",
58
- "sortablejs": "^1.15.3"
58
+ "sortablejs": "1.15.7"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@quasar/extras": "^1.16.0",
@@ -6,11 +6,11 @@
6
6
  <q-item v-bind="getItemProps(item)" :key="key" active-class="primary" clickable data-cy="actions-menu-list-item" @click="setClickHandler(item)">
7
7
  <q-item-section avatar>
8
8
  <q-spinner v-if="item.loading" size="sm" />
9
- <q-icon v-else :class="getMagicAiClass(item)" :name="item.icon" />
9
+ <q-icon v-else :class="getMagicAiClasses(item)" :name="item.icon" />
10
10
  </q-item-section>
11
11
 
12
12
  <q-item-section>
13
- <q-item-label :class="getMagicAiClass(item)">
13
+ <q-item-label :class="getMagicAiClasses(item)">
14
14
  {{ item.label }}
15
15
  </q-item-label>
16
16
  </q-item-section>
@@ -286,8 +286,8 @@ function getItemProps (item) {
286
286
  }
287
287
  }
288
288
 
289
- function getMagicAiClass (item) {
290
- return { 'text-magic-ai': item.useMagicAiColor }
289
+ function getMagicAiClasses ({ useMagicAiColor }) {
290
+ return { 'text-magic-ai': useMagicAiColor }
291
291
  }
292
292
 
293
293
  function handleMenuModel (newValue, oldValue) {
@@ -3,8 +3,8 @@
3
3
  <qas-grabbable class="qas-board-generator" v-bind="grabbableProps">
4
4
  <div ref="columnsContainer" class="no-wrap q-gutter-md q-pb-xs q-px-lg row">
5
5
  <qas-lazy-loading-components :key="lazyLoadingKey" v-model:visible-items="visibleItems" direction="horizontal" placeholder-width="350px" :threshold="0">
6
- <qas-box v-for="(header, index) in normalizedHeaders" :key="index" :class="getColumnClass(header)" :style="containerStyle">
7
- <div class="ellipsis q-mb-md text-grey-10" v-bind="headerBoxProps">
6
+ <qas-box v-for="(header, index) in normalizedHeaders" :key="index" :class="getColumnClasses(header)" :style="containerStyle">
7
+ <div class="ellipsis q-mb-md text-grey-10">
8
8
  <qas-skeleton v-if="props.skeleton" type="text" use-contrast width="80%" />
9
9
 
10
10
  <slot v-else :fields="getFieldsByHeader(header)" :header="header" :index="index" name="header-column" />
@@ -22,24 +22,19 @@
22
22
  </div>
23
23
 
24
24
  <div class="text-center">
25
- <qas-btn class="q-mt-md" icon="sym_r_refresh" label="Tentar novamente" :loading="columnsLoading[getKeyByHeader(header)]" @click="fetchColumn(header, true)" />
25
+ <qas-btn class="q-mt-md" icon="sym_r_refresh" label="Tentar novamente" :loading="columnsLoading[getKeyByHeader(header)]" @click="handleFetchColumnClick(header)" />
26
26
  </div>
27
27
  </div>
28
28
 
29
29
  <template v-else>
30
30
  <qas-lazy-loading-components :threshold="0">
31
31
  <div v-for="(item) in getItemsByHeader(header)" :id="item[props.itemIdKey]" :key="item[props.itemIdKey]" class="qas-board-generator__item" :data-disable-drag="updatingPositionItemKeys.has(item[props.itemIdKey])">
32
- <!-- <slot v-if="!props.skeleton" :column-index="index" :fields="getFieldsByHeader(header)" :header="header" :item="item" name="column-item" /> -->
33
- <!-- <qas-card v-if="updatingPositionItemKey === item[props.itemIdKey]" v-bind="skeletonCards.at(0)" :key="item[props.itemIdKey]" class="q-mb-sm" :column-index="index">
34
- <template #default />
35
- </qas-card> -->
36
-
37
32
  <slot v-if="!props.skeleton" :column-index="index" :fields="getFieldsByHeader(header)" :header="header" :is-updating-position="updatingPositionItemKeys.has(item[props.itemIdKey])" :item="item" name="column-item" />
38
33
  </div>
39
34
  </qas-lazy-loading-components>
40
35
 
41
36
  <div class="full-width justify-center row">
42
- <qas-btn v-if="hasSeeMore(header)" icon="sym_r_add" :label="props.seeMoreButtonLabel" :loading="columnsLoading[getKeyByHeader(header)]" :use-label-on-small-screen="false" variant="tertiary" @click="fetchColumn(header, true)" />
37
+ <qas-btn v-if="hasSeeMore(header)" icon="sym_r_add" :label="props.seeMoreButtonLabel" :loading="columnsLoading[getKeyByHeader(header)]" :use-label-on-small-screen="false" variant="tertiary" @click="handleFetchColumnClick(header)" />
43
38
 
44
39
  <template v-if="hasSkeletonByHeader(header)">
45
40
  <div class="q-col-gutter-y-sm row">
@@ -117,11 +112,6 @@ const props = defineProps({
117
112
  default: () => ({})
118
113
  },
119
114
 
120
- headerBoxProps: {
121
- type: Object,
122
- default: () => ({})
123
- },
124
-
125
115
  columnIdKey: {
126
116
  type: String,
127
117
  required: true
@@ -182,13 +172,7 @@ const props = defineProps({
182
172
  },
183
173
 
184
174
  skeleton: {
185
- type: Boolean,
186
- default: true
187
- },
188
-
189
- useMarkRaw: {
190
- type: Boolean,
191
- default: true
175
+ type: Boolean
192
176
  },
193
177
 
194
178
  useDragAndDropX: {
@@ -247,8 +231,13 @@ const columnsLoading = ref({})
247
231
  const columnsFieldsModel = ref({})
248
232
  const showConfirmDialog = ref(false)
249
233
  const isLoadingUpdatePosition = ref(false)
250
- const isLoadingFromSeeMore = ref(false)
234
+ const hideSkeleton = ref(false)
251
235
  const columnsWithError = ref({})
236
+
237
+ /**
238
+ * Set de IDs dos itens com update de posição em andamento.
239
+ * Usamos Set para evitar duplicatas e ter O(1) no has().
240
+ */
252
241
  const updatingPositionItemKeys = ref(new Set())
253
242
 
254
243
  /**
@@ -307,6 +296,33 @@ let columnAbortControllers = {}
307
296
  */
308
297
  let currentFetchColumnsSession = 0
309
298
 
299
+ /**
300
+ * Estado do smooth scroll horizontal.
301
+ * Acumula o target e interpola suavemente via requestAnimationFrame (rAF).
302
+ *
303
+ * rAF (requestAnimationFrame) sincroniza a animação com o refresh da tela (~60fps).
304
+ * Isso evita "saltos" de scroll, tornando o movimento suave e fluido.
305
+ */
306
+ const SMOOTH_SCROLL_LERP = 0.2
307
+
308
+ /**
309
+ * Velocidade de scroll vertical (dentro da coluna), independente do scrollSpeed horizontal.
310
+ * O offsetY recebido no scrollFn é proporcional ao scrollSpeed (60), então normalizamos
311
+ * para que o scroll vertical se comporte como se scrollSpeed fosse 10 (padrão do SortableJS).
312
+ */
313
+ const HORIZONTAL_SCROLL_SPEED = 60
314
+ const VERTICAL_SCROLL_SPEED = 20
315
+
316
+ /**
317
+ * Sensibilidade para iniciar o scroll automático ao arrastar próximo às bordas do container.
318
+ */
319
+ const HORIZONTAL_SCROLL_SENSITIVITY = 150
320
+ const VERTICAL_SCROLL_SENSITIVITY = 80
321
+
322
+ let smoothScrollTarget = 0
323
+ let smoothScrollRafId = null
324
+ let smoothScrollEl = null
325
+
310
326
  /**
311
327
  * Geração por coluna para invalidar callbacks onLoading de requests canceladas.
312
328
  * Evita que o onLoading(false) de uma request cancelada sobrescreva o estado da nova request.
@@ -328,13 +344,15 @@ const grabbableProps = {
328
344
  * Gera cards de skeleton para exibir enquanto carrega os itens da coluna, o mesmo é usado para preencher a coluna
329
345
  * quando a prop "skeleton" for true, indicando que é para simular o carregamento.
330
346
  */
331
- const skeletonCards = Array.from({ length: 6 }).map(() => ({
332
- skeleton: true,
333
- title: '-',
334
- expansionProps: { label: '-' },
335
- actionsMenuProps: { list: {} },
336
- useSelection: true
337
- }))
347
+ const skeletonCards = Array.from({ length: 6 }).map(() => {
348
+ return {
349
+ skeleton: true,
350
+ title: '-',
351
+ expansionProps: { label: '-' },
352
+ actionsMenuProps: { list: {} },
353
+ useSelection: true
354
+ }
355
+ })
338
356
 
339
357
  // computeds
340
358
  const columnContainerElements = computed(() => {
@@ -354,38 +372,6 @@ const normalizedHeaders = computed(() => {
354
372
  return props.headers
355
373
  })
356
374
 
357
- // watchers
358
- watch(
359
- () => normalizedHeaders.value,
360
- value => {
361
- if (isLoadingUpdatePosition.value || !value?.length) return
362
-
363
- fetchColumnsValues()
364
- }
365
- )
366
-
367
- watch(() => columnContainerElements.value, () => {
368
- setColumnHeightContainer()
369
- handleElementsList()
370
- })
371
-
372
- // hooks
373
- onMounted(() => {
374
- if (normalizedHeaders.value.length) {
375
- fetchColumnsValues()
376
- }
377
-
378
- window.addEventListener('resize', setColumnHeightContainer)
379
- })
380
-
381
- onBeforeUnmount(() => {
382
- abortAllColumnRequests()
383
- destroySortable()
384
-
385
- window.removeEventListener('resize', setColumnHeightContainer)
386
- })
387
-
388
- // Computeds
389
375
  const columnsResultsModel = computed({
390
376
  get () {
391
377
  return props.results
@@ -398,8 +384,6 @@ const columnsResultsModel = computed({
398
384
 
399
385
  const isDragging = computed(() => draggingCount.value > 0)
400
386
 
401
- provide('isDragging', isDragging)
402
-
403
387
  const hasColumnsLength = computed(() => !!Object.keys(columnsResultsModel.value).length)
404
388
 
405
389
  const containerStyle = computed(() => `width: ${props.columnWidth};`)
@@ -425,12 +409,54 @@ const defaultConfirmDialogProps = computed(() => {
425
409
  }
426
410
  })
427
411
 
412
+ // provide
413
+ provide('isDragging', isDragging)
414
+
415
+ // watchers
416
+ watch(
417
+ () => normalizedHeaders.value,
418
+ value => {
419
+ if (isLoadingUpdatePosition.value || !value?.length) return
420
+
421
+ fetchColumnsValues()
422
+ }
423
+ )
424
+
425
+ watch(() => columnContainerElements.value, () => {
426
+ setColumnHeightContainer()
427
+ handleElementsList()
428
+ })
429
+
430
+ // hooks
431
+ onMounted(() => {
432
+ if (normalizedHeaders.value.length) {
433
+ fetchColumnsValues()
434
+ }
435
+
436
+ window.addEventListener('resize', setColumnHeightContainer)
437
+ })
438
+
439
+ onBeforeUnmount(() => {
440
+ abortAllColumnRequests()
441
+ destroySortable()
442
+
443
+ window.removeEventListener('resize', setColumnHeightContainer)
444
+ })
445
+
428
446
  // functions
429
447
  /*
430
448
  * Setar o tamanho do container do board, onde deverá ser a altura passada via prop, ou o default será ocupar o maximo
431
449
  * de espaço que ele conseguir considerando a altura do container em relação ao topo.
432
450
  */
433
451
  function setColumnHeightContainer () {
452
+ if (props.height) {
453
+ columnContainerElements.value.forEach(columnElement => {
454
+ columnElement.style.height = props.height
455
+ })
456
+
457
+ return
458
+ }
459
+
434
460
  // Primeira etapa: calcula e aplica a altura inicial de cada coluna
435
461
  columnContainerElements.value.forEach(columnElement => {
436
462
  // Pega a posição atual da coluna em relação ao topo da viewport
@@ -490,7 +516,7 @@ async function fetchColumns () {
490
516
  const mySession = ++currentFetchColumnsSession
491
517
 
492
518
  // Mapeia os índices visíveis para os IDs de coluna correspondentes
493
- const visibleKeys = new Set(visibleItems.value.map(i => getKeyByHeader(normalizedHeaders.value[i])))
519
+ const visibleKeys = new Set(visibleItems.value.map(item => getKeyByHeader(normalizedHeaders.value[item])))
494
520
 
495
521
  const visibleHeaders = visibleKeys.size
496
522
  ? normalizedHeaders.value.filter(header => visibleKeys.has(getKeyByHeader(header)))
@@ -524,6 +550,7 @@ async function fetchColumns () {
524
550
  if (currentFetchColumnsSession !== mySession) return
525
551
 
526
552
  const allPromises = [...visibleColumns, ...hiddenColumns]
553
+ console.log('🚀 ~ fetchColumns ~ allPromises:', allPromises)
527
554
 
528
555
  const hasAllPromisesSucceeded = allPromises.every(promise => promise.status === 'fulfilled')
529
556
  const hasAllPromisesFailed = allPromises.length > 0 && allPromises.every(promise => promise.status === 'rejected')
@@ -544,18 +571,36 @@ async function fetchColumns () {
544
571
  }
545
572
 
546
573
  /*
547
- * Busca a coluna com base no header recebido.
574
+ * Wrapper para chamadas de fetchColumn originadas de eventos de clique do usuário.
575
+ * fetchColumn relança o erro para que Promise.allSettled em fetchColumns consiga
576
+ * detectar colunas com falha; aqui o erro já foi tratado internamente, portanto
577
+ * é suprimido para evitar o warning "Unhandled error during execution of component
578
+ * event handler" do Vue.
548
579
  */
549
- async function fetchColumn (header, fromSeeMore, setEr) {
580
+ function handleFetchColumnClick (header) {
581
+ fetchColumn(header, true)
582
+ }
583
+
584
+ /**
585
+ * Busca a coluna com base no header recebido.
586
+ *
587
+ * @param header - payload do header da coluna a ser buscada, exemplo: { date: '2024-02-12', ... }
588
+ * @param shouldHideSkeleton - flag para controlar se o skeleton de carregamento deve ser ocultado durante a busca
589
+ */
590
+ async function fetchColumn (header, shouldHideSkeleton) {
550
591
  const headerKey = getKeyByHeader(header)
551
592
 
552
- // Cancela qualquer request anterior para esta mesma coluna antes de iniciar a nova.
553
- // Garante que nunca haja duas requests paralelas para a mesma coluna.
593
+ /**
594
+ * Cancela qualquer request anterior para esta mesma coluna antes de iniciar a nova.
595
+ * Garante que nunca haja duas requests paralelas para a mesma coluna.
596
+ */
554
597
  abortColumnRequest(headerKey)
555
598
 
556
- // Incrementa a geração desta coluna para invalidar callbacks onLoading da request cancelada,
557
- // evitando que o onLoading(false) antigo sobrescreva o estado da nova request.
558
- // Usa ?? 0 pois ++undefined retorna NaN, e NaN === NaN é sempre false, travando o skeleton.
599
+ /**
600
+ * Incrementa a geração desta coluna para invalidar callbacks onLoading da request cancelada,
601
+ * evitando que o onLoading(false) antigo sobrescreva o estado da nova request.
602
+ * Usa ?? 0 pois ++undefined retorna NaN, e NaN === NaN é sempre false, travando o skeleton.
603
+ */
559
604
  columnFetchGenerations[headerKey] = (columnFetchGenerations[headerKey] ?? 0) + 1
560
605
  const generation = columnFetchGenerations[headerKey]
561
606
 
@@ -564,10 +609,10 @@ async function fetchColumn (header, fromSeeMore, setEr) {
564
609
 
565
610
  const { limit, offset } = columnsPagination.value[headerKey] || {}
566
611
 
567
- isLoadingFromSeeMore.value = fromSeeMore
612
+ hideSkeleton.value = shouldHideSkeleton
568
613
 
569
614
  const { data: response, error } = await promiseHandler(
570
- axios.get(`${props.columnUrl}/${headerKey}/${setEr ? 'setError' : ''}`, {
615
+ axios.get(`${props.columnUrl}/${headerKey}`, {
571
616
  signal: abortController.signal,
572
617
  params: {
573
618
  ...props.columnParams,
@@ -577,8 +622,10 @@ async function fetchColumn (header, fromSeeMore, setEr) {
577
622
  }),
578
623
  {
579
624
  onLoading: value => {
580
- // Só atualiza o loading se ainda for a request atual desta coluna,
581
- // evitando que o onLoading(false) de uma request cancelada sobrescreva o da nova.
625
+ /**
626
+ * atualiza o loading se ainda for a request atual desta coluna,
627
+ * evitando que o onLoading(false) de uma request cancelada sobrescreva o da nova.
628
+ */
582
629
  if (columnFetchGenerations[headerKey] === generation) {
583
630
  columnsLoading.value[headerKey] = value
584
631
  }
@@ -589,7 +636,7 @@ async function fetchColumn (header, fromSeeMore, setEr) {
589
636
 
590
637
  delete columnAbortControllers[headerKey]
591
638
 
592
- isLoadingFromSeeMore.value = false
639
+ hideSkeleton.value = false
593
640
 
594
641
  if (error) {
595
642
  // Request cancelada intencionalmente (novo fetchColumnsValues chamado): ignora silenciosamente.
@@ -623,7 +670,7 @@ async function fetchColumn (header, fromSeeMore, setEr) {
623
670
  * onde cada item do objeto é uma coluna no board. O mesmo vale para "columnsFieldsModel", "columnsLoading" e
624
671
  * "columnPagination", organizando os fields, loadings e o controle de paginação por chave identificadora do header.
625
672
  */
626
- columnsResultsModel.value[headerKey] = props.useMarkRaw ? markRaw(newColumnValues) : newColumnValues
673
+ columnsResultsModel.value[headerKey] = hasDragAndDrop ? newColumnValues : markRaw(newColumnValues)
627
674
 
628
675
  /*
629
676
  * Pode acontecer das options nos fields da segunda página serem diferentes da primeira página,
@@ -731,15 +778,19 @@ function setColumnsPagination () {
731
778
 
732
779
  columnsPagination.value[headerKey] = { limit: props.limitPerColumn, offset: 0 }
733
780
 
734
- // Inicia como true para exibir skeleton imediatamente, evitando flash de tela vazia
735
- // entre o reset e o início das novas requests.
781
+ /**
782
+ * Inicia como true para exibir skeleton imediatamente, evitando flash de tela vazia
783
+ * entre o reset e o início das novas requests.
784
+ */
736
785
  columnsLoading.value[headerKey] = true
737
786
  })
738
787
  }
739
788
 
740
789
  function fetchColumnsValues () {
741
- // Cancela todas as requests em andamento antes de iniciar novas,
742
- // evitando resposta de requests antigas sobrescrevendo dados da nova sessão.
790
+ /**
791
+ * Cancela todas as requests em andamento antes de iniciar novas, evitando resposta de requests
792
+ * antigas sobrescrevendo dados da nova sessão.
793
+ */
743
794
  abortAllColumnRequests()
744
795
 
745
796
  lazyLoadingKey.value++
@@ -753,10 +804,10 @@ function fetchColumnsValues () {
753
804
  /**
754
805
  * Descrição:
755
806
  * Exibe o texto quando:
756
- * - Nao esta carregando a coluna
757
- * - Nao tem itens na coluna
758
- * - Nao estou fazendo o drag and drop
759
- * - Nao esta exibindo o dialog de confirmação
807
+ * - Não está carregando a coluna
808
+ * - Não tem itens na coluna
809
+ * - Não estou fazendo o drag and drop
810
+ * - Não está exibindo o dialog de confirmação
760
811
  *
761
812
  * @param {Object} header
762
813
  */
@@ -811,19 +862,23 @@ function handleElementsList () {
811
862
  * Seta a instancia do sortable, no qual varia de acordo com as props passadas.
812
863
  *
813
864
  * @param {HTMLElement} element
814
- * @param {Number} index
865
+ * @param {number} index
815
866
  */
816
867
  function setSortable (element, index) {
817
868
  const defaultSortableConfig = {
818
- animation: 500,
869
+ animation: 150,
819
870
  group: 'shared',
820
- ghostClass: 'ghost',
821
- sort: false,
871
+ ghostClass: 'qas-board-generator__ghost',
822
872
  swapThreshold: 1,
823
- delay: 50,
873
+ delay: 0,
824
874
  delayOnTouchOnly: true,
825
875
  emptyInsertThreshold: 0,
826
- filter: '[data-disable-drag="true"]'
876
+ filter: '[data-disable-drag="true"]',
877
+ scrollSensitivity: HORIZONTAL_SCROLL_SENSITIVITY,
878
+ scrollSpeed: HORIZONTAL_SCROLL_SPEED,
879
+ bubbleScroll: true,
880
+ forceAutoScrollFallback: true,
881
+ scrollFn: handleSortableScroll
827
882
  }
828
883
 
829
884
  /**
@@ -839,15 +894,26 @@ function setSortable (element, index) {
839
894
  let dropHandled = false
840
895
 
841
896
  const sortable = new Sortable(element, {
842
- sort: props.useDragAndDropY,
843
-
844
897
  ...defaultSortableConfig,
845
898
 
846
899
  ...props.sortableConfig,
847
900
 
901
+ /**
902
+ * `sort` deve vir APÓS os spreads para não ser sobrescrito pelo defaultSortableConfig.
903
+ * Quando useDragAndDropY é true, o sort precisa estar habilitado para que o SortableJS
904
+ * rastreie ativamente a posição do item dentro da coluna durante o drag. Sem isso,
905
+ * o item oscila entre colunas porque o Sortable do destino não "assume" o controle do item.
906
+ */
907
+ sort: props.useDragAndDropY,
908
+
848
909
  group: useOnlyDragAndDropY ? `column-${index}` : 'shared',
849
910
 
850
- direction: useOnlyDragAndDropY ? 'vertical' : 'horizontal',
911
+ /**
912
+ * invertSwap muda o algoritmo de swap: em vez de trocar quando o centro do item arrastado
913
+ * cruza o centro do alvo (instável, causa oscilação), troca apenas quando cruza a BORDA
914
+ * superior/inferior do alvo. Isso cria uma "zona morta" que elimina o jitter.
915
+ */
916
+ invertSwap: true,
851
917
 
852
918
  onStart: () => {
853
919
  dropHandled = false
@@ -871,6 +937,18 @@ function setSortable (element, index) {
871
937
  ...(props.useDragAndDropY && {
872
938
  onSort: event => {
873
939
  dropHandled = true
940
+
941
+ /**
942
+ * O SortableJS dispara onSort em AMBAS as listas envolvidas quando um item
943
+ * é arrastado entre colunas (cross-column): na lista de origem porque perdeu
944
+ * um item e na lista de destino porque ganhou um.
945
+ * O onAdd já trata o cross-column na lista de destino, então ao ignorar o
946
+ * onSort para eventos cross-column evitamos processar o mesmo drop duas vezes,
947
+ * o que causava corrupção do model (item duplicado no destino e remoção
948
+ * incorreta do último item da origem via splice(-1, 1)).
949
+ */
950
+ if (event.from !== event.to) return
951
+
874
952
  onDropCard(event)
875
953
  }
876
954
  })
@@ -885,6 +963,110 @@ function startDragging () {
885
963
 
886
964
  function stopDragging () {
887
965
  draggingCount.value = Math.max(0, draggingCount.value - 1)
966
+ resetSmoothScroll()
967
+ }
968
+
969
+ /**
970
+ * Intercepta cada chamada de scroll do SortableJS.
971
+ * - Scroll vertical (colunas): retorna 'continue' para manter o comportamento nativo.
972
+ * - Scroll horizontal (container do board): aplica smooth scroll interpolado via rAF.
973
+ *
974
+ * rAF (requestAnimationFrame) sincroniza com o refresh da tela (~60fps),
975
+ * tornando o scroll suave. Sem rAF, seria um "salto" abrupto.
976
+ */
977
+ function handleSortableScroll (offsetX, offsetY, evt, _touchEvt, scrollEl) {
978
+ // Obtém o container com scroll horizontal (`.qas-grabbable__container`).
979
+ const horizontalContainer = columnContainerElements.value[0]?.closest('.qas-grabbable__container')
980
+
981
+ // Acumula o offset no target e inicia a interpolação via rAF.
982
+ if (scrollEl === horizontalContainer && offsetX) {
983
+ if (smoothScrollEl !== scrollEl) {
984
+ smoothScrollTarget = scrollEl.scrollLeft
985
+ smoothScrollEl = scrollEl
986
+ }
987
+
988
+ const maxScroll = scrollEl.scrollWidth - scrollEl.clientWidth
989
+
990
+ smoothScrollTarget = Math.max(0, Math.min(smoothScrollTarget + offsetX, maxScroll))
991
+
992
+ // Se não há animação em andamento, inicia uma.
993
+ // rAF (requestAnimationFrame) agenda a função smoothScrollStep para o próximo frame (~16ms a 60fps).
994
+
995
+ /**
996
+ * Se não há animação em andamento, inicia uma. rAF (requestAnimationFrame) agenda a função smoothScrollStep
997
+ * para o próximo frame (~16ms a 60fps).
998
+ */
999
+ if (!smoothScrollRafId) {
1000
+ smoothScrollRafId = requestAnimationFrame(smoothScrollStep)
1001
+ }
1002
+
1003
+ return
1004
+ }
1005
+
1006
+ /**
1007
+ * SortableJS chama essa função com offsets de scroll.
1008
+ * Para scroll vertical: retorna 'continue' para deixar o comportamento nativo.
1009
+ * Para scroll horizontal: aplica smooth scroll customizado via rAF.
1010
+ */
1011
+ if (offsetY) {
1012
+ const clientY = evt?.clientY ?? evt?.touches?.[0]?.clientY
1013
+
1014
+ if (clientY !== undefined) {
1015
+ const rect = scrollEl.getBoundingClientRect()
1016
+ const distFromEdge = Math.min(clientY - rect.top, rect.bottom - clientY)
1017
+
1018
+ if (distFromEdge > VERTICAL_SCROLL_SENSITIVITY) return
1019
+ }
1020
+
1021
+ scrollEl.scrollTop += offsetY * (VERTICAL_SCROLL_SPEED / HORIZONTAL_SCROLL_SPEED)
1022
+ return
1023
+ }
1024
+
1025
+ return 'continue'
1026
+ }
1027
+
1028
+ /**
1029
+ * Loop de animação que interpola o scrollLeft em direção ao target
1030
+ * usando fator de lerp (move 20% da distância restante por frame).
1031
+ *
1032
+ * Funciona assim:
1033
+ * 1. Calcula a diferença entre a posição atual e a posição desejada (target).
1034
+ * 2. Se ainda há diferença significativa, move 20% da distância restante.
1035
+ * 3. Agenda o próximo frame via rAF para continuar a interpolação.
1036
+ * 4. Quando chega perto o suficiente (~0.5px), para a animação.
1037
+ *
1038
+ * Resultado: scroll suave e progressivo, não um "salto" abrupto.
1039
+ */
1040
+ function smoothScrollStep () {
1041
+ smoothScrollRafId = null
1042
+
1043
+ if (!smoothScrollEl) return
1044
+
1045
+ const current = smoothScrollEl.scrollLeft
1046
+ const diff = smoothScrollTarget - current
1047
+
1048
+ // Se chegou bem perto do target, para na posição exata.
1049
+ if (Math.abs(diff) < 0.5) {
1050
+ smoothScrollEl.scrollLeft = smoothScrollTarget
1051
+ return
1052
+ }
1053
+
1054
+ // Move 20% da distância restante (interpolação suave).
1055
+ smoothScrollEl.scrollLeft = current + diff * SMOOTH_SCROLL_LERP
1056
+
1057
+ // Agenda o próximo frame para continuar a animação (chamada recursiva via rAF).
1058
+ smoothScrollRafId = requestAnimationFrame(smoothScrollStep)
1059
+ }
1060
+
1061
+ function resetSmoothScroll () {
1062
+ // Cancela a animação agendada (rAF) para evitar que continue executando após o drag terminar.
1063
+ if (smoothScrollRafId) {
1064
+ cancelAnimationFrame(smoothScrollRafId)
1065
+ smoothScrollRafId = null
1066
+ }
1067
+
1068
+ smoothScrollEl = null
1069
+ smoothScrollTarget = 0
888
1070
  }
889
1071
 
890
1072
  /**
@@ -919,7 +1101,7 @@ function addItemToList ({ headerKey, item, index }) {
919
1101
 
920
1102
  updatedList.splice(insertIndex, 0, item)
921
1103
 
922
- columnsResultsModel.value[headerKey] = props.useMarkRaw ? markRaw(updatedList) : updatedList
1104
+ columnsResultsModel.value[headerKey] = updatedList
923
1105
  columnsPagination.value[headerKey].count += 1
924
1106
  }
925
1107
 
@@ -1040,7 +1222,7 @@ function removeItemFromList ({ headerKey, itemId }) {
1040
1222
  const updatedList = [...columnItemList]
1041
1223
  updatedList.splice(itemIndex, 1)
1042
1224
 
1043
- columnsResultsModel.value[headerKey] = props.useMarkRaw ? markRaw(updatedList) : updatedList
1225
+ columnsResultsModel.value[headerKey] = updatedList
1044
1226
 
1045
1227
  /**
1046
1228
  * Remove o item do count da coluna para não mostrar o botão de "Ver mais¨.
@@ -1048,13 +1230,29 @@ function removeItemFromList ({ headerKey, itemId }) {
1048
1230
  columnsPagination.value[headerKey].count -= 1
1049
1231
  }
1050
1232
 
1233
+ /**
1234
+ * Substitui um item na lista de uma coluna pelos dados atualizados do servidor.
1235
+ *
1236
+ * 1. Localiza a lista de itens da coluna
1237
+ * 2. Encontra o índice do item usando o itemIdKey como identificador
1238
+ * 3. Se encontrado, altera o item localmente (sem criar nova referência de array)
1239
+ *
1240
+ * @param {Object} params
1241
+ * @param {string} params.headerKey - ID da coluna
1242
+ * @param {string|number} params.itemId - ID do item a atualizar (deve corresponder a props.itemIdKey)
1243
+ * @param {Object} params.updatedItem - Novo objeto do item com dados atualizados
1244
+ *
1245
+ */
1051
1246
  function updateItemInList ({ headerKey, itemId, updatedItem }) {
1052
1247
  const columnItemList = columnsResultsModel.value[headerKey]
1053
1248
 
1249
+ // Encontra o índice do item na lista usando o identificador (itemIdKey)
1054
1250
  const itemIndex = columnItemList.findIndex(itemContent => itemContent[props.itemIdKey] === itemId)
1055
1251
 
1252
+ // Se não encontrar (itemIndex === -1), retorna sem fazer nada
1056
1253
  if (!~itemIndex) return
1057
1254
 
1255
+ // Substitui o item antigo pelo item atualizado na mesma posição
1058
1256
  columnItemList.splice(itemIndex, 1, updatedItem)
1059
1257
  }
1060
1258
 
@@ -1077,7 +1275,7 @@ async function updatePosition ({ newHeaderKey, oldHeaderKey, itemId, event, opti
1077
1275
 
1078
1276
  const isFnUpdatePositionUrl = typeof props.updatePositionUrl === 'function'
1079
1277
 
1080
- isLoadingFromSeeMore.value = true
1278
+ hideSkeleton.value = true
1081
1279
 
1082
1280
  const url = isFnUpdatePositionUrl
1083
1281
  ? props.updatePositionUrl({ newHeaderKey, oldHeaderKey, itemId })
@@ -1182,7 +1380,7 @@ function isCancelledError (error) {
1182
1380
  * @param {string|number} params.itemId - ID do item a ser movido (valor correspondente à prop `itemIdKey`).
1183
1381
  * @param {string|number} params.fromColumnId - ID da coluna de origem (valor correspondente à prop `columnIdKey`).
1184
1382
  * @param {string|number} params.toColumnId - ID da coluna de destino (valor correspondente à prop `columnIdKey`).
1185
- * @param {Object} [params.updatedItem] - Dados atualizados do item. Quando informado, sobrescreve o item original na coluna de destino.
1383
+ * @param {Object} [params.updatedItem] - Dados atualizados do item. Quando informado, sobrescreve o item original na coluna de destino.
1186
1384
  * @returns {void}
1187
1385
  */
1188
1386
  function transferItemToColumn ({ itemId, fromColumnId, toColumnId, updatedItem }) {
@@ -1202,10 +1400,10 @@ function transferItemToColumn ({ itemId, fromColumnId, toColumnId, updatedItem }
1202
1400
  function hasSkeletonByHeader (header) {
1203
1401
  const headerKey = getKeyByHeader(header)
1204
1402
 
1205
- return (props.skeleton || columnsLoading.value[headerKey]) && !isLoadingFromSeeMore.value
1403
+ return (props.skeleton || columnsLoading.value[headerKey]) && !hideSkeleton.value
1206
1404
  }
1207
1405
 
1208
- function getColumnClass (header) {
1406
+ function getColumnClasses (header) {
1209
1407
  const headerKey = getKeyByHeader(header)
1210
1408
 
1211
1409
  return {
@@ -1252,5 +1450,28 @@ function getCardsContainerProps (header) {
1252
1450
  &__column-error {
1253
1451
  border: 1px solid $negative;
1254
1452
  }
1453
+
1454
+ &__ghost {
1455
+ border: 1px solid $grey-4 !important;
1456
+ overflow: hidden;
1457
+ border-radius: $generic-border-radius;
1458
+ position: relative;
1459
+ margin-bottom: var(--qas-spacing-sm);
1460
+
1461
+ &::after {
1462
+ align-items: center;
1463
+ background-color: $grey-3;
1464
+ border-radius: inherit;
1465
+ color: $grey-8;
1466
+ content: 'place_item';
1467
+ display: flex;
1468
+ font-family: 'Material Symbols Rounded';
1469
+ font-size: 40px;
1470
+ inset: 0;
1471
+ justify-content: center;
1472
+ position: absolute;
1473
+ z-index: 999;
1474
+ }
1475
+ }
1255
1476
  }
1256
1477
  </style>
@@ -97,8 +97,8 @@ props:
97
97
  type: Number
98
98
  default: 12
99
99
 
100
- use-mark-raw:
101
- desc: Define se os valores dos itens das colunas irão ser atribuídos utilizando o "markRaw", onde somente a primeira camada do model será reativa. (https://vuejs.org/api/reactivity-advanced.html#markraw)
100
+ skeleton:
101
+ desc: Exibe o estado de esqueleto de carregamento das colunas. Enquanto `true`, cada coluna renderiza cards fictícios no lugar dos itens reais.
102
102
  type: Boolean
103
103
  default: true
104
104
 
@@ -150,11 +150,26 @@ slots:
150
150
  column-item:
151
151
  desc: Slot para acessar o item da coluna.
152
152
  scope:
153
+ column-index:
154
+ desc: Índice da coluna atual.
155
+ type: Number
156
+ default: 0
157
+
153
158
  fields:
154
159
  desc: Fields referente à coluna atual do template.
155
160
  type: Object
156
161
  default: {}
157
162
 
163
+ header:
164
+ desc: Informações do header da coluna atual.
165
+ type: Object
166
+ default: {}
167
+
168
+ is-updating-position:
169
+ desc: Indica se o item está sendo atualizado via drag-and-drop (aguardando retorno da API de update).
170
+ type: Boolean
171
+ default: false
172
+
158
173
  item:
159
174
  desc: Informações de cada item da coluna que foi buscado através da API.
160
175
  type: Object
@@ -220,6 +235,45 @@ methods:
220
235
  'fetchColumn: (header) => void':
221
236
  desc: Busca uma coluna específica com base no header fornecido.
222
237
 
238
+ 'refreshColumn: (header) => void':
239
+ desc: Reinicia a paginação da coluna e refaz a busca, substituindo completamente os itens existentes.
240
+ params:
241
+ header:
242
+ desc: Header da coluna a ser recarregada (mesmo formato da prop `headers`).
243
+ type: Object
244
+ required: true
245
+
246
+ 'removeItemFromList: ({ headerKey, itemId }) => void':
247
+ desc: Remove um item da lista de uma coluna localmente, sem realizar nenhuma requisição. Também decrementa o contador de paginação.
248
+ params:
249
+ headerKey:
250
+ desc: Chave identificadora da coluna de origem (valor correspondente à prop `column-id-key`).
251
+ type: String | Number
252
+ required: true
253
+ itemId:
254
+ desc: ID do item a ser removido (valor correspondente à prop `item-id-key`).
255
+ type: String | Number
256
+ required: true
257
+
258
+ 'updateItemInList: ({ headerKey, itemId, updatedItem }) => void':
259
+ desc: Substitui os dados de um item existente em uma coluna localmente, sem realizar nenhuma requisição.
260
+ params:
261
+ headerKey:
262
+ desc: Chave identificadora da coluna (valor correspondente à prop `column-id-key`).
263
+ type: String | Number
264
+ required: true
265
+ itemId:
266
+ desc: ID do item a ser atualizado (valor correspondente à prop `item-id-key`).
267
+ type: String | Number
268
+ required: true
269
+ updatedItem:
270
+ desc: Dados atualizados que substituirão o item original.
271
+ type: Object
272
+ required: true
273
+
274
+ 'refetchColumns: () => void':
275
+ desc: Alias de `fetchColumns`. Busca novamente todas as colunas com base nos headers fornecidos.
276
+
223
277
  'transferItemToColumn: ({ itemId, fromColumnId, toColumnId, updatedItem? }) => void':
224
278
  desc: |
225
279
  Transfere um item de uma coluna para outra localmente, sem realizar nenhuma requisição.
@@ -5,7 +5,6 @@
5
5
  </template>
6
6
 
7
7
  <script setup>
8
-
9
8
  import setScrollGradient from '../../../helpers/set-scroll-gradient'
10
9
 
11
10
  import { ref, onMounted, onBeforeUnmount } from 'vue'
@@ -32,3 +32,8 @@ props:
32
32
  use-spacing:
33
33
  desc: Controla espaçamento do componente.
34
34
  type: Boolean
35
+
36
+ provide:
37
+ is-box:
38
+ desc: Identificador booleano que indica quando um componente está dentro do contexto de um QasBox.
39
+ type: Boolean
@@ -34,6 +34,7 @@
34
34
 
35
35
  <div class="qas-card__content relative-position" :class="contentClasses">
36
36
  <qas-skeleton v-if="props.skeleton" height="100px" />
37
+
37
38
  <slot v-else name="default" />
38
39
  </div>
39
40
 
@@ -165,6 +165,12 @@ const hasDatePicker = computed(() => !props.useTimeOnly && !props.readonly)
165
165
  const hasTimePicker = computed(() => !props.useDateOnly && !props.readonly)
166
166
 
167
167
  watch(() => props.modelValue, (current, original) => {
168
+ /**
169
+ * No caso de ser adicionado uma data incompleta ou inválida, vamos limpar o model,
170
+ * mas não vamos limpar o "currentValue", para não limpar o campo, somente o model.
171
+ */
172
+ if (!current && original && error.value) return
173
+
168
174
  if (!current || props.useTimeOnly) {
169
175
  currentValue.value = current
170
176
  return
@@ -217,6 +223,9 @@ function updateModelValue (value) {
217
223
  if (error.value) {
218
224
  hasInvalidDate.value = true
219
225
  errorMessage.value = 'Data inválida.'
226
+
227
+ emit('update:modelValue', '')
228
+
220
229
  return
221
230
  }
222
231
 
@@ -262,18 +271,33 @@ function validateDateAndTime (value) {
262
271
  function validateDateTimeOnBlur () {
263
272
  const valueLength = currentValue.value?.replace?.(/_/g, '')?.length
264
273
 
274
+ // Caso for datetime
275
+ if (!props.useDateOnly && !props.useTimeOnly) {
276
+ const [date, time] = (currentValue.value || '').split(' ') || []
277
+ const isValidDate = date?.replace?.(/_/g, '')?.length === props.dateMask.length
278
+
279
+ /**
280
+ * Caso tenha preenchido a data, a data seja válida e não tenha preenchido o horário,
281
+ * preenche o horário com 00:00, ou de acordo com a mask.
282
+ */
283
+ if (isValidDate && !validateDateOnly(date) && !time?.length) {
284
+ // Horario setado deve seguir a mascara.
285
+ const defaultTimeByMask = props.timeMask.replace(/[HhMmSs]/g, '0')
286
+ const newValue = `${date} ${defaultTimeByMask}`
287
+
288
+ currentValue.value = newValue
289
+ updateModelValue(newValue)
290
+
291
+ return
292
+ }
293
+ }
294
+
265
295
  // valida se o tamanho digitado é o tamanho que a mascara espera receber
266
296
  error.value = !!((valueLength < mask.value.length || error.value) && valueLength)
267
297
 
268
298
  if (error.value && !hasInvalidDate.value) {
269
299
  errorMessage.value = 'Data incompleta.'
270
- }
271
-
272
- if (hasInvalidDate.value) {
273
- currentValue.value = ''
274
- }
275
300
 
276
- if (error.value || hasInvalidDate.value) {
277
301
  emit('update:modelValue', '')
278
302
  }
279
303
  }
@@ -5,10 +5,8 @@
5
5
  <slot name="header">
6
6
  <div class="items-center justify-between row">
7
7
  <qas-label data-cy="dialog-title" :label="props.card.title" margin="none">
8
- <slot name="title"></slot>
8
+ <slot name="title" />
9
9
  </qas-label>
10
- <!-- <slot name="title">
11
- </slot> -->
12
10
 
13
11
  <qas-btn v-if="isInfoDialog" v-close-popup color="grey-10" data-cy="dialog-close-btn" icon="sym_r_close" variant="tertiary" />
14
12
  </div>
@@ -69,6 +69,11 @@ slots:
69
69
  header:
70
70
  desc: Slot para o título.
71
71
 
72
+ provide:
73
+ is-dialog:
74
+ desc: Identificador booleano que indica quando um componente está dentro do contexto de um QasDialog.
75
+ type: Boolean
76
+
72
77
  events:
73
78
  '@update:model-value -> function (value)':
74
79
  desc: Dispara toda vez que o model é atualizado, também utilizado para v-model.
@@ -111,6 +116,9 @@ selectors:
111
116
  desc: Seletor do botão de confirmar do componente.
112
117
  examples: ['data-cy="dialog-ok-btn"']
113
118
 
119
+ title:
120
+ desc: Slot para inserir conteúdo adicional ao lado do título do card do dialog.
121
+
114
122
  dialog-title:
115
123
  desc: Seletor do título do componente.
116
124
  examples: ['data-cy="dialog-title"']
@@ -62,6 +62,11 @@ slots:
62
62
  content:
63
63
  desc: Slot para substituir o conteúdo principal do card.
64
64
 
65
+ provide:
66
+ is-expansion-item:
67
+ desc: Identificador booleano que indica quando um componente está dentro do contexto de um QasExpansionItem.
68
+ type: Boolean
69
+
65
70
  events:
66
71
  '@update:model-value -> function(value)':
67
72
  desc: Dispara quando o model-value altera, também usado para v-model.
@@ -120,6 +120,11 @@ slots:
120
120
  'legend-bottom[nome-da-chave-do-fieldset]-[nome-da-chave-do-subset]':
121
121
  desc: Acessa o slot da seção abaixo do conteúdo do form de um subset específico.
122
122
 
123
+ provide:
124
+ is-form-generator:
125
+ desc: Identificador booleano que indica quando um componente está dentro do contexto de um QasFormGenerator.
126
+ type: Boolean
127
+
123
128
  events:
124
129
  '@update:model-value -> function(value)':
125
130
  desc: Dispara quando o model-value altera, também usado para v-model.
@@ -54,6 +54,11 @@ props:
54
54
  type: Boolean
55
55
  default: false
56
56
 
57
+ provide:
58
+ is-header:
59
+ desc: Identificador booleano que indica quando um componente está dentro do contexto de um QasHeader.
60
+ type: Boolean
61
+
57
62
  slots:
58
63
  actions:
59
64
  desc: slot para acessar seção da direita (ações).
@@ -30,35 +30,28 @@ import { ref, onMounted, onBeforeUnmount, useSlots, nextTick, watch } from 'vue'
30
30
 
31
31
  defineOptions({ name: 'QasLazyLoadingComponents' })
32
32
 
33
- // const emit = defineEmits(['update:visibleItems'])
34
-
35
33
  const props = defineProps({
36
- // Porcentagem de visibilidade necessária para ativar (0.0 a 1.0)
37
34
  threshold: {
38
35
  type: Number,
39
36
  default: 0.1 // 10% visível
40
37
  },
41
38
 
42
- // Margem extra ao redor do viewport para pré-carregamento
43
39
  rootMargin: {
44
40
  type: String,
45
41
  default: '0px'
46
42
  },
47
43
 
48
- // Direção do scroll: 'vertical' (padrão) ou 'horizontal'
49
44
  direction: {
50
45
  type: String,
51
46
  default: 'vertical',
52
47
  validator: value => ['vertical', 'horizontal'].includes(value)
53
48
  },
54
49
 
55
- // Altura dos placeholders antes de carregar (usado em direction='vertical')
56
50
  placeholderHeight: {
57
51
  type: String,
58
52
  default: '500px'
59
53
  },
60
54
 
61
- // Largura dos placeholders antes de carregar (usado em direction='horizontal')
62
55
  placeholderWidth: {
63
56
  type: String,
64
57
  default: '300px'
@@ -4,21 +4,46 @@ meta:
4
4
  desc: Componente para carregar elementos do slot somente quando ficam visíveis na viewport, otimizando performance da página.
5
5
 
6
6
  props:
7
- threshold:
8
- desc: Threshold do IntersectionObserver (0 = aparece um pouco, 1 = totalmente visível)
9
- default: 0.1
10
- type: Number
11
-
12
- root-margin:
13
- desc: Margem extra ao redor do viewport para pré-carregamento
14
- default: '0px'
7
+ direction:
8
+ desc: Direção do scroll e do lazy loading. Use `vertical` (padrão) para listas de rolagem vertical e `horizontal` para listas de rolagem horizontal.
15
9
  type: String
10
+ default: vertical
11
+ values: vertical | horizontal
16
12
 
17
13
  placeholder-height:
18
- desc: Altura do placeholder exibido enquanto o elemento não está visível
14
+ desc: Altura do placeholder exibido enquanto o elemento não está visível (usado quando `direction` é `vertical`).
19
15
  default: '500px'
20
16
  type: String
21
17
 
18
+ placeholder-width:
19
+ desc: Largura do placeholder exibido enquanto o elemento não está visível (usado quando `direction` é `horizontal`).
20
+ default: '300px'
21
+ type: String
22
+
23
+ root-margin:
24
+ desc: Margem extra ao redor do viewport para pré-carregamento
25
+ default: '0px'
26
+ type: String
27
+
28
+ threshold:
29
+ desc: Threshold do IntersectionObserver (0 = aparece um pouco, 1 = totalmente visível)
30
+ default: 0.1
31
+ type: Number
32
+
33
+ visible-items:
34
+ desc: Expõe os índices dos itens atualmente visíveis na viewport. Útil para otimizar renders e priorizar carregamento de dados.
35
+ type: Array
36
+ default: []
37
+ model: true
38
+
22
39
  slots:
23
40
  default:
24
41
  desc: Elementos/componentes que serão carregados conforme ficam visíveis, onde cada elemento irmão dentro do slot será tratado individualmente para o lazy loading.
42
+
43
+ events:
44
+ '@update:visible-items -> function(value)':
45
+ desc: Dispara toda vez que os índices dos itens visíveis são atualizados, também utilizado para v-model:visible-items.
46
+ params:
47
+ value:
48
+ desc: Array de índices dos itens atualmente visíveis na viewport.
49
+ type: Array
@@ -229,7 +229,7 @@ function getDialogSlot (name) {
229
229
 
230
230
  // ------------------------- composable functions ------------------------------
231
231
  function useList () {
232
- const filteredOptions = ref(props.options)
232
+ const filteredOptions = ref([...props.options])
233
233
 
234
234
  const selectedOptions = computed(() => {
235
235
  const options = []
@@ -206,6 +206,27 @@ function previous () {
206
206
  border-radius: var(--qas-generic-border-radius);
207
207
  }
208
208
 
209
+ .q-stepper__tab {
210
+ /*
211
+ * Por padrão o quasar sempre deixa as demais linhas com o mesmo tamanho, exceto a primeira e a última, que são
212
+ * maiores devido terem alinhamento inicial e final respectivamente, enquanto as demais possuem alinhamento
213
+ * central. Para equalizar visualmente os tamanhos, é necessário setar uma proporção de flex diferente para a
214
+ * primeira/última tab e para as tabs do meio.
215
+ *
216
+ * Proporção 2:3 para equalizar visualmente os tamanhos:
217
+ * - Primeira/última tab: flex 2 (possuem apenas meia linha)
218
+ * - Tabs do meio: flex 3 (possuem linha completa dos dois lados)
219
+ */
220
+ &:first-child,
221
+ &:last-child {
222
+ flex: 2;
223
+ }
224
+
225
+ &:not(:first-child):not(:last-child) {
226
+ flex: 3;
227
+ }
228
+ }
229
+
209
230
  .q-stepper {
210
231
  &__tab {
211
232
  &--done {
@@ -223,9 +244,10 @@ function previous () {
223
244
  }
224
245
  }
225
246
 
226
- // Seta a cor do after da linha quando a step está finalizada, é a primeira step ou a próxima é ativa.
247
+ // Seta a cor do after da linha quando a step está finalizada, é a primeira step ou a próxima é ativa ou finalizada.
227
248
  &:first-child,
228
- &:has(+ .q-stepper__tab.q-stepper__tab--active) {
249
+ &:has(+ .q-stepper__tab.q-stepper__tab--active),
250
+ &:has(+ .q-stepper__tab.q-stepper__tab--done) {
229
251
  .q-stepper__line::after {
230
252
  background-color: var(--q-primary);
231
253
  }
@@ -7,8 +7,12 @@ import { computed, reactive } from 'vue'
7
7
  * isSmall: boolean,
8
8
  * isMedium: boolean,
9
9
  * isLarge: boolean,
10
+ * isXLarge: boolean,
11
+ * is2XLarge: boolean,
10
12
  * untilMedium: boolean,
11
13
  * untilLarge: boolean,
14
+ * untilXLarge: boolean,
15
+ * until2XLarge: boolean,
12
16
  * isMobile: boolean
13
17
  * }}
14
18
  *
@@ -25,15 +29,27 @@ export default function () {
25
29
  // de 600 até 1023px
26
30
  isMedium: computed(() => Screen.sm),
27
31
 
28
- // de 600 até 1023px
32
+ // Maior que 1023px
29
33
  isLarge: computed(() => Screen.gt.sm),
30
34
 
35
+ // Maior que 1439px
36
+ isXLarge: computed(() => Screen.gt.md),
37
+
38
+ // Maior que 1919px
39
+ is2XLarge: computed(() => Screen.gt.lg),
40
+
31
41
  // de 0 até 599px
32
42
  untilMedium: computed(() => Screen.lt.sm),
33
43
 
34
44
  // de 0 ate 1023px
35
45
  untilLarge: computed(() => Screen.lt.md),
36
46
 
47
+ // de 0 até 1439px
48
+ untilXLarge: computed(() => Screen.lt.lg),
49
+
50
+ // de 0 até 1919px
51
+ until2XLarge: computed(() => Screen.lt.xl),
52
+
37
53
  // Plataforma
38
54
  isMobile: computed(() => Platform.is.mobile || false)
39
55
  })
@@ -160,7 +160,7 @@ function parseValue (value) {
160
160
  try { return JSON.parse(value) } catch { return value }
161
161
  }
162
162
 
163
- function booleanLabel (value, trueLabel = 'sim', falseLabel = 'não') {
163
+ function booleanLabel (value, trueLabel = 'Sim', falseLabel = 'Não') {
164
164
  try { return JSON.parse(value) ? trueLabel : falseLabel } catch { return value }
165
165
  }
166
166
 
@@ -174,13 +174,16 @@ export default function setScrollGradient (config = {}) {
174
174
  const elementRect = element.getBoundingClientRect()
175
175
  const parentRect = element.parentElement.getBoundingClientRect()
176
176
 
177
+ // pequena margem para garantir que o gradiente fique grudado ao elemento.
178
+ const safeArea = 1
179
+
177
180
  /**
178
181
  * diferença entre o bottom do pai e o bottom do filho, valor positivo significa
179
182
  * que há espaço livre abaixo do filho.
180
183
  */
181
184
  const distance = {
182
- end: isVertical ? (parentRect.bottom - elementRect.bottom) : (parentRect.right - elementRect.right),
183
- start: isVertical ? (elementRect.top - parentRect.top) : (elementRect.left - parentRect.left)
185
+ end: isVertical ? (parentRect.bottom - elementRect.bottom - safeArea) : (parentRect.right - elementRect.right - safeArea),
186
+ start: isVertical ? (elementRect.top - parentRect.top) - safeArea : (elementRect.left - parentRect.left) - safeArea
184
187
  }
185
188
 
186
189
  span.style[getDirection(direction)] = `${distance[direction]}px`
@@ -98,7 +98,7 @@ export default {
98
98
 
99
99
  this.mx_cachedOptions = []
100
100
 
101
- this.mx_filterOptionsByStore('')
101
+ if (!this.disable) this.mx_filterOptionsByStore('')
102
102
 
103
103
  setTimeout(() => this.$emit('update:modelValue', undefined))
104
104
  }
@@ -3,7 +3,7 @@ type: component
3
3
  meta:
4
4
  desc: Plugin que implementa a action `destroy` do `StoreModule` adicionando comportamento de confirmação antes de excluir, este mesmo plugin é utilizado no componente `QasDelete`.
5
5
 
6
- inject:
6
+ provide:
7
7
  'this.$qas.delete(config)':
8
8
  params:
9
9
  config:
@@ -3,7 +3,7 @@ type: component
3
3
  meta:
4
4
  desc: Plugin que implementa o `QasDialog`.
5
5
 
6
- inject:
6
+ provide:
7
7
  'this.$qas.dialog(config)':
8
8
  params:
9
9
  config:
@@ -3,7 +3,7 @@ type: component
3
3
  meta:
4
4
  desc: Plugin que implementa o "QNotify" do quasar.
5
5
 
6
- inject:
6
+ provide:
7
7
  'this.$qas.error(msg)':
8
8
  params:
9
9
  msg:
@@ -3,7 +3,7 @@ type: component
3
3
  meta:
4
4
  desc: Plugin que implementa o "QNotify" do quasar para notificações de sucesso.
5
5
 
6
- inject:
6
+ provide:
7
7
  'this.$qas.success(msg)':
8
8
  params:
9
9
  msg:
@@ -4,15 +4,31 @@ export default () => {
4
4
  const screensModel = {
5
5
  // até 599px
6
6
  isSmall: () => Screen.xs,
7
+
7
8
  // de 600 até 1023px
8
9
  isMedium: () => Screen.sm,
9
- // de 600 até 1023px
10
+
11
+ // Maior que 1023px
10
12
  isLarge: () => Screen.gt.sm,
13
+
14
+ // Maior que 1439px
15
+ isXLarge: () => Screen.gt.md,
16
+
17
+ // Maior que 1919px
18
+ is2XLarge: () => Screen.gt.lg,
19
+
11
20
  // de 0 até 599px
12
21
  untilMedium: () => Screen.lt.sm,
22
+
13
23
  // de 0 ate 1023px
14
24
  untilLarge: () => Screen.lt.md,
15
25
 
26
+ // de 0 até 1439px
27
+ untilXLarge: () => Screen.lt.lg,
28
+
29
+ // de 0 até 1919px
30
+ until2XLarge: () => Screen.lt.xl,
31
+
16
32
  // Plataforma
17
33
  isMobile: () => Platform.is.mobile || false
18
34
  }
@@ -3,7 +3,7 @@ type: component
3
3
  meta:
4
4
  desc: Plugin que implementa o "Screen" do quasar.
5
5
 
6
- inject:
6
+ provide:
7
7
  'this.$qas.screen':
8
8
  type: Object
9
9
  debugger: true
@@ -11,6 +11,10 @@ inject:
11
11
  isSmall: false
12
12
  isMedium: false
13
13
  isLarge: false
14
+ isXLarge: false
15
+ is2XLarge: false
14
16
  untilMedium: false
15
17
  untilLarge: false
18
+ untilXLarge: false
19
+ until2XLarge: false
16
20
  isMobile: false