v-sistec-features 1.0.0 → 1.1.1

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.
@@ -0,0 +1,721 @@
1
+ <template>
2
+ <div>
3
+ <div class="" :class="props.class_container">
4
+ <slot></slot>
5
+ <div class="" :class="props.class_content">
6
+ <div class="d-flex justify-content-between align-items-start mb-2">
7
+ <slot name="pageSize" :changePageSize="changePageSize" :page_size="page_size">
8
+ <div class="text-secondary">
9
+ {{ props.first_text_page_size }}
10
+ <div class="mx-2 d-inline-block">
11
+ <input class="form-control form-control-sm" @change="changePageSize" v-model="page_size" min="1" size="3"
12
+ aria-label="Número de nóticias por página" />
13
+ </div>
14
+ {{ props.second_text_page_size }}
15
+ </div>
16
+ </slot>
17
+
18
+ <slot name="fieldMiddle">
19
+
20
+ </slot>
21
+
22
+ <Search v-model:search="pagination.search" v-model:filter="pagination.filter" :list_filter="props.list_filter"
23
+ :item_use="item_use" @search="reSearch" />
24
+ </div>
25
+
26
+ <div v-if="props.use_checkbox && selected_items.length > 0"
27
+ class="alert alert-cyan d-flex justify-content-center align-items-center py-3" role="alert">
28
+ <h4 class="alert-title m-0"> <strong>Itens Selecionados:</strong> {{ selected_items.length }}</h4>
29
+ <button class="btn btn-outline-danger ms-3 bold " @click="selected_items = []">Limpar Seleção</button>
30
+ </div>
31
+ <template v-if="showLoadingState">
32
+ <template v-if="props.custom_loading">
33
+ <component :is="props.custom_loading" />
34
+ </template>
35
+ <template v-else>
36
+ <table class="table table-vcenter table-selectable" :class="props.class_table">
37
+ <thead>
38
+ <tr>
39
+ <th v-for="col in columns" :key="col.field || col.header" :class="col.class_column">
40
+ {{ col.header }}
41
+ </th>
42
+ </tr>
43
+ </thead>
44
+ <tbody>
45
+ <template v-if="props.type_loading === 'placeholder'">
46
+ <tr v-for="n in page_size" :key="'placeholder-' + n" class="placeholder-glow">
47
+ <td v-for="col in columns" :key="col.field || col.header" :class="col.class_row">
48
+ <span v-if="col.bodySlot" >
49
+ <span class="placeholder col-8"></span>
50
+ </span>
51
+ <span :class="col.class_item" v-else-if="col.type === 'text'">
52
+ <span class="placeholder col-8"></span>
53
+ </span>
54
+ <span v-else-if="col.type === 'date'">
55
+ <span class="placeholder col-9"></span>
56
+ </span>
57
+ <div :class="col.class_item" v-else-if="col.type === 'html'">
58
+ <div class="placeholder col-12"></div>
59
+ </div>
60
+
61
+ <div :class="col.class_item" v-else-if="col.type === 'img'">
62
+ <div class="placeholder placeholder-img"></div>
63
+ </div>
64
+
65
+ <span class="text-danger erro-custom-container" v-else>tipo <span
66
+ class="badge bg-orange text-white erro-custom-text">{{ col.type }}</span> não suportado
67
+ </span>
68
+ </td>
69
+ </tr>
70
+ </template>
71
+ <template v-else-if="props.type_loading === 'spiner-table'">
72
+ <tr v-for="n in page_size" :key="'placeholder-' + n">
73
+ <td v-for="col in columns" :key="col.field || col.header" :class="col.class_row">
74
+ <span v-if="col.bodySlot" >
75
+ <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
76
+ </span>
77
+ <span :class="col.class_item" v-else-if="col.type === 'text'">
78
+ <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
79
+ </span>
80
+ <span v-else-if="col.type === 'date'">
81
+ <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
82
+ </span>
83
+ <div :class="col.class_item" v-else-if="col.type === 'html'">
84
+ <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
85
+ </div>
86
+
87
+ <div class="" :class="col.class_item" v-else-if="col.type === 'img'">
88
+ <span class="placeholder-img d-flex justify-content-center align-items-center">
89
+ <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
90
+ </span>
91
+ </div>
92
+
93
+ <span class="text-danger erro-custom-container" v-else>tipo <span
94
+ class="badge bg-orange text-white erro-custom-text">{{ col.type }}</span> não suportado
95
+ </span>
96
+ </td>
97
+ </tr>
98
+
99
+ </template>
100
+ <template v-else-if="props.type_loading === 'spiner'">
101
+ <tr v-for="n in page_size" :key="n">
102
+ <td :colspan="columns.length" class="text-center p-0" style="border-bottom: none;">
103
+ <div v-if="n === Math.floor(page_size / 2) + 1"
104
+ class="d-flex flex-column justify-content-center align-items-center" style="height: 6rem;">
105
+ <div class="spinner-border" style="width: 3rem; height: 3rem;" role="status">
106
+ </div>
107
+ <span class="mt-2">Carregando...</span>
108
+ </div>
109
+ <div v-else style="height: 3rem;"></div>
110
+ </td>
111
+ </tr>
112
+ </template>
113
+
114
+ </tbody>
115
+ </table>
116
+ </template>
117
+
118
+ <div v-if="attempt && attempt.current > 1" class="p-3 text-center text-secondary">
119
+ A conexão falhou. Tentando novamente... (Tentativa {{ attempt.current }} de {{ attempt.total }})
120
+ </div>
121
+ </template>
122
+ <div v-else-if="error" class="feedback-container text-center">
123
+ <h4 class="text-danger">Ocorreu um Erro</h4>
124
+ <p class="text-secondary" v-if="attempt">
125
+ Não foi possível carregar os dados após {{ attempt.total }} tentativa(s).
126
+ </p>
127
+ <p class="text-secondary" v-else>
128
+ Não foi possível carregar os dados. Verifique sua conexão.
129
+ </p>
130
+ <button class="btn btn-primary mt-2" @click="fetchDataWithDelay">
131
+ Tentar Novamente
132
+ </button>
133
+ </div>
134
+ <div class="table-responsive" v-else-if="items">
135
+ <div v-if="items.length > 0">
136
+ <table class="table table-vcenter table-selectable" :class="props.class_table">
137
+ <thead>
138
+ <tr>
139
+ <th v-if="props.use_checkbox" class="w-1">
140
+ <input class="form-check-input m-0" type="checkbox" ref="selectAllCheckbox"
141
+ @change="toggleSelectAll" aria-label="Selecionar todos os itens na página" />
142
+ </th>
143
+ <th v-for="col in columns" :key="col.field || col.header" :class="col.class_column">
144
+ {{ col.header }}
145
+ </th>
146
+ </tr>
147
+ </thead>
148
+ <tbody>
149
+ <tr v-for="item in items" :key="item[props.item_key]">
150
+ <td v-if="props.use_checkbox" class="w-1">
151
+ <input class="form-check-input m-0" type="checkbox" :checked="isSelected(item)"
152
+ @change="toggleItemSelection(item)" aria-label="Selecionar este item" />
153
+ </td>
154
+ <td v-for="col in columns" :key="col.field || col.header" :class="col.class_row">
155
+ <component v-if="col.bodySlot" :is="col.bodySlot" :item="item" :is-selected="isSelected(item)" />
156
+ <span :class="col.class_item" v-else-if="col.type === 'text'">
157
+ {{
158
+ limiteText(getSubItem(col.field, item, col.transform_function), col.limite_text ?? null)
159
+ }}</span>
160
+
161
+ <span v-else-if="col.type === 'date'">
162
+ <span v-if="col.format === 'complete'">{{ new Date(getSubItem(col.field, item)).toLocaleString()
163
+ }}</span>
164
+ <span v-if="col.format === 'simple'"> {{ new Date(getSubItem(col.field,
165
+ item)).toLocaleDateString()
166
+ }} </span>
167
+ </span>
168
+ <div :class="col.class_item" v-else-if="col.type === 'html'" v-html="getSubItem(col.field, item)">
169
+ </div>
170
+
171
+ <div :class="col.class_item" v-else-if="col.type === 'img'">
172
+
173
+ <div v-if="getSubItem(col.field, item)" v-bind="col.deactivate_img_preview ? {
174
+ class: 'container-img'
175
+ } :
176
+ {
177
+ onMouseover: (event) => handleMouseOver(event, getSubItem(col.field, item)),
178
+ onMousemove: handleMouseMove,
179
+ onMouseleave: handleMouseLeave,
180
+ class: 'container-img container-img-preview'
181
+ }">
182
+
183
+ <img class="img-tamanho" :src="getSubItem(col.field, item)" />
184
+ <img class="img-tamanho-cover" :src="getSubItem(col.field, item)" />
185
+ <div class="bg-img"></div>
186
+ </div>
187
+
188
+ </div>
189
+ <span class="text-danger erro-custom-container" v-else>tipo <span
190
+ class="badge bg-orange text-white erro-custom-text">{{ col.type }}</span> não suportado</span>
191
+ </td>
192
+ </tr>
193
+ </tbody>
194
+ </table>
195
+ </div>
196
+ <div v-else class="text-center p-4 text-secondary">
197
+ <p class="m-0">Nenhum item encontrado.</p>
198
+ </div>
199
+ </div>
200
+
201
+ </div>
202
+
203
+
204
+ </div>
205
+ <slot name="pagination" :pagination="pagination" :tradePage="fetchDataWithDelay" :error="error">
206
+ <div v-if="!error && pagination.count > 0" class="mt-3 px-3" :class="props.class_pagination">
207
+ <PaginationDatatable :filtering="true" :pagination="pagination" @tradePage="fetchDataWithDelay" />
208
+ </div>
209
+ </slot>
210
+
211
+ <div v-if="isHovering" class="image-preview-container" :style="previewStyle">
212
+ <img :src="previewSrc" alt="Preview" class="image-preview-large" />
213
+ </div>
214
+ </div>
215
+
216
+ </template>
217
+
218
+ <script setup lang="ts" generic="T extends Record<string, any>" >
219
+ import { ref, provide, computed, watch, onMounted, nextTick, type Component, type Ref, type ComputedRef } from 'vue';
220
+
221
+ import PaginationDatatable from './PaginationDatatable.vue';
222
+ import Search from './SearchDatatable.vue';
223
+ import { useImagePreview } from '../composables/useImagePreview';
224
+ import { dataTableApiKey, type ColumnConfiguration, type PaginationObject } from '../keys';
225
+
226
+ const {
227
+ isHovering,
228
+ previewSrc,
229
+ previewStyle,
230
+ handleMouseOver,
231
+ handleMouseMove,
232
+ handleMouseLeave
233
+ } = useImagePreview();
234
+
235
+ interface VDataTableProps {
236
+ /* configuração do useApiFetch */
237
+ fetch: Function;
238
+ fetch_name?: string;
239
+ endpoint: string;
240
+ /* tipos de loading pré-definidos*/
241
+ type_loading?: 'placeholder' | 'spiner-table' | 'spiner';
242
+ /*recebe um component para loading*/
243
+ custom_loading?: Component | null;
244
+ /* retira os params default da requisição */
245
+ deactivate_default_params?: boolean;
246
+ /* nomes dos parâmetros para passar para o backend */
247
+ filter_param_name?: string;
248
+ search_param_name?: string;
249
+ page_param_name?: string;
250
+ page_size_param_name?: string;
251
+ add_params?: Object | Function;
252
+
253
+ /* usado para pegar os dados do useApiFetch */
254
+ data_key?: string;
255
+ total_key?: string;
256
+
257
+ /* filtros que irão ser usados */
258
+ list_filter?: any[];
259
+ /* mudar o que está escrito no select de mudança de items_per_page*/
260
+ first_text_page_size?: string;
261
+ second_text_page_size?: string;
262
+
263
+
264
+ /* props para estilizar o vdatatable */
265
+ class_table?: string;
266
+ class_content?: string;
267
+ class_container?: string;
268
+ class_pagination?: string;
269
+
270
+ /*
271
+ * tempo mínimo em ms para mostrar o loading para evitar telas piscando
272
+ */
273
+ min_loading_delay?: number;
274
+ /*
275
+ - Número de tentativas automáticas em caso de falha.
276
+ - 1 significa que a requisição será feita apenas uma vez, sem retentativas.
277
+ - Valor padrão é 3.
278
+ */
279
+ retry_attempts?: number;
280
+ // Atraso em milissegundos entre cada tentativa
281
+ retry_delay?: number;
282
+
283
+ // Ativa a funcionalidade de seleção com checkboxes
284
+ use_checkbox?: boolean;
285
+ // Define qual propriedade do item será usada como chave única para a seleção.
286
+ item_key?: string;
287
+ }
288
+
289
+ interface ExposedFunctions {
290
+ execute: () => void;
291
+ pagination: Ref<PaginationObject>;
292
+ default_params: Record<string, any>;
293
+ selected_items: Ref<T[]>;
294
+ atLeastOneSelected: ComputedRef<boolean>;
295
+ }
296
+
297
+ // =======================================================
298
+ // 1. DEFINIÇÃO DE PROPS COM VALORES PADRÃO
299
+ // =======================================================
300
+ const props = withDefaults(defineProps<VDataTableProps>(), {
301
+ fetch_name: '',
302
+ type_loading: 'placeholder',
303
+ custom_loading: null,
304
+ deactivate_default_params: false,
305
+ filter_param_name: 'filter',
306
+ search_param_name: 'search',
307
+ page_param_name: 'page',
308
+ page_size_param_name: 'page_size',
309
+ add_params: () => ({}),
310
+ data_key: 'results',
311
+ total_key: 'count',
312
+ list_filter: () => [],
313
+ class_table: '',
314
+ class_content: '',
315
+ class_container: '',
316
+ class_pagination: '',
317
+ min_loading_delay: 600,
318
+ retry_attempts: 3,
319
+ retry_delay: 2000,
320
+ use_checkbox: false,
321
+ item_key: 'id',
322
+ first_text_page_size: 'Mostrar',
323
+ second_text_page_size: 'registros',
324
+ });
325
+
326
+
327
+ // =======================================================
328
+ // 2. ESTADO REATIVO PRINCIPAL
329
+ // =======================================================
330
+
331
+ const page_size = ref<number>(5);
332
+ const columns = ref<ColumnConfiguration[]>([]);
333
+ const items = ref<T[]>([]) as Ref<T[]>;
334
+ const totalItems = ref<number>(0);
335
+ const selected_items = ref<T[]>([]) as Ref<T[]>;
336
+ const selectAllCheckbox = ref<HTMLInputElement | null>(null);
337
+ const isDelaying = ref<boolean>(false);
338
+ const delayTimer = ref<ReturnType<typeof setTimeout> | null>(null);
339
+
340
+
341
+ /*--------- definição de páginação ---------------*/
342
+ const pagination = ref<PaginationObject>({
343
+ current_page: 0, // pagina atual
344
+ count: 0, // total de itens
345
+ limit_per_page: 5, // limite de itens por página
346
+ search: '', // termo de busca
347
+ filter: '', // filtro selecionado
348
+ })
349
+
350
+ // =======================================================
351
+ // 3. LÓGICA DA API (useFetch)
352
+ // =======================================================
353
+ const { data: response, pending, error, execute, attempt } = props.fetch(props.endpoint, {
354
+ params: () => {
355
+
356
+ if (props.deactivate_default_params) {
357
+ if (props.add_params && typeof props.add_params === 'function') {
358
+ return props.add_params();
359
+ }
360
+ return {
361
+ ...props.add_params,
362
+ };
363
+ }
364
+ else if (props.add_params && typeof props.add_params === 'function') {
365
+ return {
366
+ ...default_params.value,
367
+ ...props.add_params(),
368
+ }
369
+ }
370
+ return {
371
+ ...default_params.value,
372
+ ...props.add_params,
373
+ };
374
+ },
375
+ retry: props.retry_attempts,
376
+ retryDelay: props.retry_delay,
377
+ paramsReactives: false,
378
+ immediate: false,
379
+ }, props.fetch_name);
380
+
381
+ // =======================================================
382
+ // 4. PROPRIEDADES COMPUTADAS
383
+ // =======================================================
384
+ const item_use = computed<number[]>(() => {
385
+ let use = [1]
386
+ if (props.list_filter.length > 0) {
387
+ use.push(2)
388
+ }
389
+ return use;
390
+ });
391
+
392
+ const default_params = computed<Record<string, any>>(() => ({
393
+ [props.page_param_name]: pagination.value.current_page + 1,
394
+ [props.page_size_param_name]: pagination.value.limit_per_page,
395
+ [props.search_param_name]: pagination.value.search || "",
396
+ [props.filter_param_name]: pagination.value.filter || "",
397
+ }));
398
+
399
+ // para controlar a exibição do loading
400
+ const showLoadingState = computed<boolean>(() => {
401
+ return (pending.value || isDelaying.value)
402
+ });
403
+
404
+
405
+ // Helper para verificar se um item está selecionado, comparando pela chave única
406
+ const isSelected = (item: T): boolean => {
407
+ const key = props.item_key;
408
+ return selected_items.value.some(selectedItem => selectedItem[key] === item[key]);
409
+ };
410
+
411
+ // Controla o estado do checkbox "selecionar todos"
412
+ const selectAllState = computed<boolean | 'indeterminate'>(() => {
413
+ if (!items.value.length) return false;
414
+ const selectedOnPageCount = items.value.filter(item => isSelected(item)).length;
415
+ if (selectedOnPageCount === 0) return false;
416
+ if (selectedOnPageCount === items.value.length) return true;
417
+ return 'indeterminate';
418
+ });
419
+
420
+ // computed que mostra se pelo menos um item está selecionado
421
+ const atLeastOneSelected = computed<boolean>(() => selected_items.value.length > 0);
422
+
423
+
424
+ // =======================================================
425
+ // 5. WATCHERS (Observadores)
426
+ // =======================================================
427
+
428
+ // observa o estado e atualiza a propriedade 'indeterminate'
429
+ watch([selectAllState, selectAllCheckbox], ([newState]) => {
430
+ if (selectAllCheckbox.value) {
431
+ if (newState === 'indeterminate') {
432
+ console.log("entrei no indeterminate")
433
+ // Se o estado for indeterminado:
434
+ selectAllCheckbox.value.checked = false; // Ele não está "marcado"
435
+ selectAllCheckbox.value.indeterminate = true; // Ele está com o "traço"
436
+ } else {
437
+ selectAllCheckbox.value.checked = newState; // Define o estado marcado/desmarcado
438
+ selectAllCheckbox.value.indeterminate = false; // Remove o "traço"
439
+ }
440
+ }
441
+ }, {
442
+ immediate: true,
443
+ flush: 'post'
444
+ });
445
+
446
+ watch(response, (newResponse: any) => {
447
+ if (newResponse) {
448
+ items.value = newResponse[props.data_key] || [];
449
+ totalItems.value = newResponse[props.total_key] || 0;
450
+ pagination.value.count = totalItems.value;
451
+ } else {
452
+ items.value = [];
453
+ totalItems.value = 0;
454
+ }
455
+ }, { immediate: true });
456
+
457
+
458
+ // =======================================================
459
+ // 6. MÉTODOS
460
+ // =======================================================
461
+
462
+ // Função para marcar ou desmarcar todos os itens da página atual
463
+ function toggleSelectAll(): void {
464
+ const pageItems = items.value;
465
+ if (!pageItems.length) return;
466
+
467
+ // Usa a propriedade computada para saber o estado atual
468
+ const currentState = selectAllState.value;
469
+
470
+ // Se TODOS ou ALGUNS estiverem selecionados, o clique irá LIMPAR a seleção da página.
471
+ if (currentState === true || currentState === 'indeterminate') {
472
+ const pageItemKeys = pageItems.map(item => item[props.item_key]);
473
+ selected_items.value = selected_items.value.filter(
474
+ selectedItem => !pageItemKeys.includes(selectedItem[props.item_key])
475
+ );
476
+ }
477
+ // Se NENHUM estiver selecionado, o clique irá SELECIONAR TODOS da página.
478
+ else { // currentState é false
479
+ pageItems.forEach(item => {
480
+ if (!isSelected(item)) {
481
+ selected_items.value.push(item);
482
+ }
483
+ });
484
+ }
485
+ }
486
+
487
+ // Função para marcar ou desmarcar um item individual
488
+ function toggleItemSelection(item: T): void {
489
+ const key = props.item_key;
490
+ const index = selected_items.value.findIndex(selectedItem => selectedItem[key] === item[key]);
491
+
492
+ if (index > -1) {
493
+ selected_items.value.splice(index, 1); // Remove se já existe
494
+ } else {
495
+ selected_items.value.push(item); // Adiciona se não existe
496
+ }
497
+ }
498
+
499
+ function addColumn(colConfig: ColumnConfiguration): void {
500
+ columns.value.push(colConfig);
501
+ }
502
+ provide(dataTableApiKey, { addColumn });
503
+
504
+ // Função que gerencia o delay e a chamada da API
505
+ function fetchDataWithDelay(): void {
506
+ // Limpa timer anterior, se houver
507
+ if (delayTimer.value) clearTimeout(delayTimer.value);
508
+
509
+ isDelaying.value = true;
510
+
511
+ delayTimer.value = setTimeout(() => {
512
+ isDelaying.value = false;
513
+ }, props.min_loading_delay);
514
+
515
+ execute(); // Executa a busca de dados original do useApiFetch
516
+ }
517
+
518
+ function reSearch(): void {
519
+ pagination.value.current_page = 0;
520
+ fetchDataWithDelay();
521
+ }
522
+
523
+ const changePageSize = (event: Event): void => {
524
+ const target = event.target as HTMLInputElement;
525
+ const newSize = parseInt(target.value, 10);
526
+ if (newSize > 0) {
527
+ page_size.value = newSize;
528
+ pagination.value.limit_per_page = newSize; // Atualiza o limite de itens por página
529
+ pagination.value.current_page = 0;
530
+ fetchDataWithDelay();
531
+ } else {
532
+ // toast.showToast("Erro", "Tamanho da página deve ser maior que 0", 2);
533
+ page_size.value = pagination.value.limit_per_page; // Reseta para o valor anterior
534
+ }
535
+ };
536
+
537
+ function getSubItem(field: string | null, item: T, transform_function: ((value: any) => any) | null = null): any {
538
+ if (!field) return item;
539
+ const parts = field.split('.');
540
+ let value_item = item;
541
+
542
+ for(const part of parts){
543
+ if (value_item && typeof value_item === 'object' && part in value_item){
544
+ value_item = value_item[part];
545
+ }
546
+ else{
547
+ console.error(`Caminho inválido ou valor nulo em: ${field} na parte ${part}`);
548
+ }
549
+ }
550
+
551
+ if (transform_function) {
552
+ value_item = transform_function(value_item);
553
+ }
554
+ return value_item;
555
+ }
556
+
557
+
558
+
559
+ function limiteText(text: string | null, limite: number | null): string | null {
560
+ if (limite && typeof limite === 'number' && limite > 0 && typeof text === 'string' && text.length > limite) {
561
+ return text.substring(0, limite) + '...';
562
+ }
563
+ return text;
564
+ }
565
+
566
+ // =======================================================
567
+ // 7. EXPOSE E CICLO DE VIDA
568
+ // =======================================================
569
+
570
+ defineExpose<
571
+ ExposedFunctions
572
+ >({
573
+ execute: fetchDataWithDelay,
574
+ pagination,
575
+ default_params,
576
+ selected_items,
577
+ atLeastOneSelected,
578
+ });
579
+
580
+ onMounted(() => {
581
+ nextTick(() => {
582
+
583
+ /*
584
+ * executar dentro do nextTick para garantir que o pai já tem acesso ao
585
+ * ref que foi exposto
586
+ */
587
+ fetchDataWithDelay();
588
+ })
589
+ });
590
+ </script>
591
+
592
+ <style lang="scss" scoped>
593
+ .table-responsive {
594
+ overflow-x: auto;
595
+ }
596
+
597
+ .state-feedback {
598
+ padding: 1rem;
599
+ text-align: center;
600
+ }
601
+
602
+ .state-feedback.error {
603
+ color: red;
604
+ }
605
+
606
+ .pagination-controls {
607
+ margin-top: 1rem;
608
+ display: flex;
609
+ justify-content: space-between;
610
+ }
611
+
612
+ $max-width-img: 40px;
613
+
614
+ .placeholder-img {
615
+ width: $max-width-img;
616
+ height: $max-width-img;
617
+ border-radius: 4px;
618
+ }
619
+
620
+ .container-img {
621
+ aspect-ratio: 1;
622
+ display: flex;
623
+ justify-content: center;
624
+ overflow: hidden;
625
+ position: relative;
626
+ max-width: $max-width-img;
627
+ min-width: $max-width-img;
628
+
629
+ .img-tamanho-cover {
630
+ position: absolute;
631
+ top: 0;
632
+ width: 100%;
633
+ height: 100%;
634
+ object-fit: cover;
635
+ z-index: 0;
636
+ opacity: 0.5;
637
+ filter: blur(4px);
638
+ }
639
+
640
+ .img-tamanho {
641
+ object-fit: contain;
642
+ width: 100%;
643
+ height: 100%;
644
+ z-index: 2;
645
+ }
646
+
647
+ &.container-img-preview {
648
+ cursor: pointer;
649
+
650
+ &:hover {
651
+ border-style: dashed;
652
+ border-color: var(--tblr-primary);
653
+ border-width: 2px;
654
+ transition: border-width 0.15s ease-in-out;
655
+
656
+ .img-tamanho {
657
+ opacity: 0.3;
658
+ }
659
+ }
660
+ }
661
+ }
662
+
663
+ .erro-custom-container {
664
+ display: inline-block;
665
+ padding: 0.2em 0.6em;
666
+ border-radius: 4px;
667
+ background-color: #ffffff;
668
+ font-weight: bold;
669
+ text-transform: uppercase;
670
+ }
671
+
672
+ .erro-custom-text {
673
+ font-size: 0.8em;
674
+ text-transform: uppercase;
675
+ font-weight: bold;
676
+ }
677
+
678
+ .bg-img {
679
+ background-color: #0000004d;
680
+ position: absolute;
681
+ top: 0;
682
+ width: 100%;
683
+ height: 100%;
684
+ z-index: 1;
685
+ }
686
+
687
+ $max-width-preview: 250px;
688
+
689
+ .image-preview-container {
690
+ position: fixed;
691
+
692
+ z-index: 9999;
693
+
694
+ background-color: #fff;
695
+ border-radius: 8px;
696
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
697
+ padding: 5px;
698
+
699
+ pointer-events: none;
700
+ transition: opacity 0.2s ease-in-out;
701
+
702
+ .image-preview-large {
703
+ display: block;
704
+ max-width: $max-width-preview;
705
+
706
+ max-height: $max-width-preview;
707
+ border-radius: 4px;
708
+ }
709
+ }
710
+
711
+ .form-check-input {
712
+ border-width: 1px !important;
713
+ border-color: rgba(0, 0, 0, 0.391) !important;
714
+ width: 17px;
715
+ height: 17px;
716
+ }
717
+
718
+ [data-bs-theme=dark] .form-check-input {
719
+ border-color: rgba(255, 255, 255, 0.374) !important;
720
+ }
721
+ </style>