@nixweb/nixloc-ui 1.0.0 → 1.2.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.
@@ -0,0 +1,103 @@
1
+ <template>
2
+ <div>
3
+ <div class="error-details-header">
4
+ <i class="fas fa-triangle-exclamation"></i>
5
+ <div>
6
+ <div class="error-details-title title">O que deu errado?</div>
7
+ <!-- <div class="error-details-sub">ID: {{ id }}</div> -->
8
+ </div>
9
+ </div>
10
+
11
+ <ScrollBar :minHeight="190" :maxHeight="190">
12
+ <ul class="error-list">
13
+ <li v-for="(msg, i) in safeMessages" :key="i" class="error-item">
14
+ <i class="fas fa-info-circle"></i>
15
+ <span>{{ msg }}</span>
16
+ </li>
17
+
18
+ <li v-if="safeMessages.length === 0" class="error-item muted">
19
+ <i class="fas fa-info-circle"></i>
20
+ <span>Algo deu errado, mas não recebemos detalhes do servidor.</span>
21
+ </li>
22
+ </ul>
23
+ </ScrollBar>
24
+
25
+ <div class="text-center">
26
+ <Button key="closeErrors" title="Fechar" color="white" backGroundColor="#6B7280" size="small"
27
+ :clicked="() => $emit('close')" />
28
+ </div>
29
+ </div>
30
+ </template>
31
+
32
+ <script>
33
+ import ScrollBar from "@nixweb/nixloc-ui/src/component/layout/ScrollBar.vue";
34
+ import Button from "@nixweb/nixloc-ui/src/component/forms/Button";
35
+
36
+ export default {
37
+ name: "ActionErrorContent",
38
+ components: { ScrollBar, Button },
39
+ props: {
40
+ id: { type: [String, Number], required: true },
41
+ index: { type: Number, default: -1 },
42
+ messages: { type: Array, default: () => [] },
43
+ },
44
+ computed: {
45
+ safeMessages() {
46
+ return (this.messages || []).filter(Boolean);
47
+ },
48
+ },
49
+ };
50
+ </script>
51
+
52
+ <style scoped>
53
+ .error-details-header {
54
+ display: flex;
55
+ gap: 10px;
56
+ align-items: center;
57
+ margin-bottom: 12px;
58
+ color: #dc2626;
59
+ }
60
+
61
+ .error-details-header i {
62
+ font-size: 18px;
63
+ }
64
+
65
+ .error-details-title {
66
+ font-weight: 600;
67
+ color: #111827;
68
+ }
69
+
70
+ .error-details-sub {
71
+ font-size: 12px;
72
+ color: #6b7280;
73
+ }
74
+
75
+ .error-list {
76
+ list-style: none;
77
+ margin: 0;
78
+ padding: 0;
79
+ }
80
+
81
+ .error-item {
82
+ display: flex;
83
+ gap: 8px;
84
+ align-items: flex-start;
85
+ padding: 8px 10px;
86
+ background: #fff7f7;
87
+ border: 1px solid #fde2e2;
88
+ border-radius: 8px;
89
+ margin-bottom: 8px;
90
+ font-size: 13px;
91
+ color: #dc2626;
92
+ }
93
+
94
+ .error-item i {
95
+ margin-top: 2px;
96
+ }
97
+
98
+ .error-item.muted {
99
+ background: #f9fafb;
100
+ border-color: #e5e7eb;
101
+ color: #6b7280;
102
+ }
103
+ </style>
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <div>
3
+ <div v-if="!isFinished" class="actions">
4
+ <div class="side-by-side">
5
+ <Button key="cancel" title="Cancelar" type="danger" size="small"
6
+ :disabled="processing || selected.length === 0" :clicked="hideModal" />
7
+ <Button key="ok" title="Processar" color="white" classIcon="fa-solid fa-play" backGroundColor="#007BFF" size="large"
8
+ :disabled="processing || selected.length === 0" :clicked="ok" />
9
+ </div>
10
+ </div>
11
+
12
+ <div v-else class="text-center">
13
+ <Button key="finish" title="Fechar" classIcon="fa-solid fa-xmark" color="white" backGroundColor="#6D757B" size="medium"
14
+ :clicked="finished" />
15
+ </div>
16
+ </div>
17
+ </template>
18
+
19
+ <script>
20
+ import Button from "@nixweb/nixloc-ui/src/component/forms/Button";
21
+
22
+ export default {
23
+ name: "ActionFooter",
24
+ components: { Button },
25
+ props: {
26
+ isFinished: { type: Boolean, default: false },
27
+ processing: { type: Boolean, default: false },
28
+ selected: { type: Array, default: () => [] },
29
+ },
30
+ emits: ["ok", "cancel", "finish"],
31
+ methods: {
32
+ ok() {
33
+ this.$emit("ok");
34
+ },
35
+ hideModal() {
36
+ this.$emit("cancel");
37
+ },
38
+ finished() {
39
+ this.$emit("finish");
40
+ },
41
+ },
42
+ };
43
+ </script>
44
+
45
+ <style scoped>
46
+ .actions {
47
+ text-align: center;
48
+ }
49
+
50
+ .side-by-side {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ gap: 8px;
54
+ }
55
+ </style>
@@ -0,0 +1,110 @@
1
+ <template>
2
+ <div>
3
+ <div class="action-banner">
4
+ <span class="action-accent"></span>
5
+ <span class="action-label">Ação</span>
6
+ <strong class="action-value title">{{ description }}</strong>
7
+ </div>
8
+
9
+ <div class="status-header">
10
+ <div class="status-chip">
11
+ <i class="fas fa-list"></i>
12
+ <span>Total {{ total }}</span>
13
+ </div>
14
+ <div class="status-chip success">
15
+ <i class="fas fa-check-circle"></i>
16
+ <span>Concluídos {{ done }}</span>
17
+ </div>
18
+ <div class="status-chip error-chip">
19
+ <i class="fas fa-times-circle"></i>
20
+ <span>Falhas {{ errors }}</span>
21
+ </div>
22
+ </div>
23
+ </div>
24
+ </template>
25
+
26
+ <script>
27
+ export default {
28
+ name: "ActionModalHeader",
29
+ props: {
30
+ description: { type: String, default: "" },
31
+ total: { type: Number, default: 0 },
32
+ done: { type: Number, default: 0 },
33
+ errors: { type: Number, default: 0 },
34
+ },
35
+ };
36
+ </script>
37
+
38
+ <style scoped>
39
+ .status-header {
40
+ display: flex;
41
+ align-items: center;
42
+ gap: 10px;
43
+ flex-wrap: wrap;
44
+ margin-bottom: 10px;
45
+ padding: 8px 0;
46
+ }
47
+
48
+ .status-chip {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: 6px;
52
+ background: #f3f4f6;
53
+ border: 1px solid #e5e7eb;
54
+ border-radius: 20px;
55
+ padding: 6px 10px;
56
+ font-size: 13px;
57
+ color: #374151;
58
+ }
59
+
60
+ .status-chip.success {
61
+ background: #ecfdf5;
62
+ border-color: #a7f3d0;
63
+ color: #065f46;
64
+ }
65
+
66
+ .status-chip.success i {
67
+ color: #16a34a;
68
+ }
69
+
70
+ .status-chip.error-chip {
71
+ background: #fef2f2;
72
+ border-color: #fecaca;
73
+ color: #b91c1c;
74
+ }
75
+
76
+ .status-chip.error-chip i {
77
+ color: #dc2626;
78
+ }
79
+
80
+ .action-banner {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 10px;
84
+ padding: 10px 12px;
85
+ margin-bottom: 12px;
86
+ background: #f8fafc;
87
+ border: 1px solid #e5e7eb;
88
+ border-radius: 12px;
89
+ }
90
+
91
+ .action-accent {
92
+ width: 4px;
93
+ align-self: stretch;
94
+ background: #3b82f6;
95
+ border-radius: 8px;
96
+ }
97
+
98
+ .action-label {
99
+ font-size: 12px;
100
+ text-transform: uppercase;
101
+ letter-spacing: 0.06em;
102
+ color: #64748b;
103
+ }
104
+
105
+ .action-value {
106
+ font-size: 14px;
107
+ color: #0f172a;
108
+ font-weight: 700;
109
+ }
110
+ </style>
@@ -0,0 +1,121 @@
1
+ <template>
2
+ <ScrollBar :minHeight="300" :maxHeight="300">
3
+ <div v-for="(id, index) in selected" :key="id" class="item-row">
4
+ <div class="item-left title">Item {{ index + 1 }}</div>
5
+ <div class="item-right">
6
+ <span v-if="status[id] === 'waiting'" class="waiting">
7
+ <i class="fas fa-hourglass-half"></i>
8
+ </span>
9
+
10
+ <span v-else-if="status[id] === 'running'" class="loading">
11
+ <span class="spinner"></span> Processando...
12
+ </span>
13
+
14
+ <span v-else-if="status[id] === 'done'" class="done">
15
+ <i class="fas fa-check-circle done-icon"></i> Sucesso!
16
+ </span>
17
+
18
+ <span v-else-if="status[id] === 'error'" class="error">
19
+ <i class="fas fa-times-circle"></i> Falhou!
20
+ <button class="error-link" @click="$emit('show-error', { id, index })"><span class="title">Ver
21
+ detalhes</span></button>
22
+ </span>
23
+ </div>
24
+ </div>
25
+ </ScrollBar>
26
+ </template>
27
+
28
+ <script>
29
+ import ScrollBar from "@nixweb/nixloc-ui/src/component/layout/ScrollBar.vue";
30
+
31
+ export default {
32
+ name: "ActionItemList",
33
+ components: { ScrollBar },
34
+ props: { selected: Array, status: Object, errorsMap: Object },
35
+ };
36
+ </script>
37
+
38
+ <style scoped>
39
+ .item-row {
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: space-between;
43
+ padding: 8px 10px;
44
+ border: 1px solid #eee;
45
+ border-radius: 8px;
46
+ background: #fafafa;
47
+ margin: 6px 0;
48
+ }
49
+
50
+ .item-left {
51
+ font-weight: 500;
52
+ color: #334155;
53
+ }
54
+
55
+ .item-right {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 8px;
59
+ font-size: 13px;
60
+ }
61
+
62
+ .waiting {
63
+ color: #6b7280;
64
+ }
65
+
66
+ .loading {
67
+ color: #007BFF;
68
+ }
69
+
70
+ .done {
71
+ color: #16a34a;
72
+ font-weight: 500;
73
+ }
74
+
75
+ .done-icon {
76
+ color: #22c55e;
77
+ font-size: 14px;
78
+ }
79
+
80
+ .error {
81
+ color: #dc2626;
82
+ font-weight: 500;
83
+ }
84
+
85
+ .error-link {
86
+ margin-left: 10px;
87
+ background: transparent;
88
+ border: none;
89
+ padding: 0;
90
+ font-size: 13px;
91
+ text-decoration: underline;
92
+ cursor: pointer;
93
+ color: #1f2937;
94
+ }
95
+
96
+ .error-link:hover {
97
+ opacity: 0.9;
98
+ }
99
+
100
+ .spinner {
101
+ display: inline-block;
102
+ width: 14px;
103
+ height: 14px;
104
+ border: 2px solid #007bff;
105
+ border-top: 2px solid transparent;
106
+ border-radius: 50%;
107
+ animation: spin 0.8s linear infinite;
108
+ vertical-align: middle;
109
+ margin-right: 6px;
110
+ }
111
+
112
+ @keyframes spin {
113
+ 0% {
114
+ transform: rotate(0deg);
115
+ }
116
+
117
+ 100% {
118
+ transform: rotate(360deg);
119
+ }
120
+ }
121
+ </style>
@@ -0,0 +1,159 @@
1
+ <template>
2
+ <div class="actions-options">
3
+ <div class="ao-body">
4
+ <ScrollBar :minHeight="minHeight" :maxHeight="maxHeight" class="ao-scroll">
5
+ <div v-if="actionSelected.type == 'options'" class="ao-radios" role="group"
6
+ :aria-labelledby="'ao-title'">
7
+ <RadioGroup :field="fieldName" :options="filteredOptions" v-model="optionSelected" />
8
+ </div>
9
+
10
+ <div v-else class="ao-empty" aria-live="polite">
11
+ <i class="fas fa-inbox"></i>
12
+ <span>Nenhuma opção encontrada.</span>
13
+ </div>
14
+ </ScrollBar>
15
+ </div>
16
+
17
+ <footer class="ao-footer">
18
+ <div class="ao-footer-left">
19
+ <span v-if="optionSelected" class="ao-helper title">
20
+ Selecionado: <strong>{{ labelSelected }}</strong>
21
+ </span>
22
+ </div>
23
+ <div class="ao-footer-right">
24
+ <Button key="select" :disabled="!isValid" :title="btnNextLabel" type="primary" size="medium"
25
+ :clicked="next" />
26
+ </div>
27
+ </footer>
28
+ </div>
29
+ </template>
30
+
31
+ <script>
32
+ import Button from "@nixweb/nixloc-ui/src/component/forms/Button";
33
+ import RadioGroup from "@nixweb/nixloc-ui/src/component/forms/RadioGroup";
34
+ import ScrollBar from "@nixweb/nixloc-ui/src/component/layout/ScrollBar.vue";
35
+ import { mapMutations } from "vuex";
36
+
37
+ export default {
38
+ name: "ActionsOptions",
39
+ components: { Button, RadioGroup, ScrollBar },
40
+ props: {
41
+ actionSelected: { type: Object, required: true },
42
+ minHeight: { type: Number, default: 120 },
43
+ maxHeight: { type: Number, default: 260 },
44
+ btnNextLabel: { type: String, default: "Avançar" },
45
+ fieldName: { type: String, default: "group" },
46
+ searchThreshold: { type: Number, default: 10 },
47
+ value: String
48
+ },
49
+ data() {
50
+ return {
51
+ optionSelected: null,
52
+ query: ""
53
+ };
54
+ },
55
+ computed: {
56
+ optionsRaw() {
57
+ const list = (this.actionSelected && Array.isArray(this.actionSelected.options))
58
+ ? this.actionSelected.options
59
+ : [];
60
+ return list.map(o => ({ text: o.text, value: o.value }));
61
+ },
62
+ filteredOptions() {
63
+ const q = (this.query || "").toLowerCase();
64
+ if (!q) return this.optionsRaw;
65
+ return this.optionsRaw.filter(opt =>
66
+ String(opt.text).toLowerCase().includes(q) ||
67
+ String(opt.value).toLowerCase().includes(q)
68
+ );
69
+ },
70
+ isValid() {
71
+ return this.optionSelected !== null && this.optionSelected !== undefined && this.optionSelected !== "";
72
+ },
73
+ labelSelected() {
74
+ const found = this.optionsRaw.find(o => o.value === this.optionSelected);
75
+ return found ? found.text : "";
76
+ }
77
+ },
78
+ watch: {
79
+ optionSelected: {
80
+ handler(value) {
81
+ this.$emit("input", value);
82
+ },
83
+ deep: true,
84
+ },
85
+ optionsRaw: {
86
+ immediate: true,
87
+ handler(list) {
88
+ if (Array.isArray(list) && list.length === 1) {
89
+ this.optionSelected = list[0].value;
90
+ }
91
+ }
92
+ }
93
+ },
94
+ methods: {
95
+ ...mapMutations("generic", ["openModal", "hideModal"]),
96
+ next() {
97
+ if (!this.isValid) return;
98
+ this.openModal("confirm");
99
+ },
100
+ enterToNext() {
101
+ if (this.isValid) this.next();
102
+ },
103
+ }
104
+ };
105
+ </script>
106
+
107
+ <style scoped>
108
+ .actions-options {
109
+ display: flex;
110
+ flex-direction: column;
111
+ gap: 12px;
112
+ max-width: 640px;
113
+ }
114
+
115
+ .ao-body {
116
+ display: flex;
117
+ flex-direction: column;
118
+ gap: 8px;
119
+ }
120
+
121
+ .ao-scroll {
122
+ border: 1px solid #e5e7eb;
123
+ border-radius: 10px;
124
+ padding: 8px;
125
+ background: #fff;
126
+ }
127
+
128
+ .ao-radios {
129
+ display: grid;
130
+ gap: 10px;
131
+ }
132
+
133
+ .ao-empty {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 8px;
137
+ color: #6b7280;
138
+ padding: 8px;
139
+ }
140
+
141
+ .ao-footer {
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: space-between;
145
+ gap: 12px;
146
+ padding-top: 8px;
147
+ position: sticky;
148
+ bottom: 0;
149
+ }
150
+
151
+ .ao-footer-right {
152
+ display: flex;
153
+ gap: 8px;
154
+ }
155
+
156
+ .ao-helper {
157
+ color: #374151;
158
+ }
159
+ </style>