@weni/unnnic-system 2.6.1 → 2.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weni/unnnic-system",
3
- "version": "2.6.1",
3
+ "version": "2.7.0",
4
4
  "type": "commonjs",
5
5
  "files": [
6
6
  "dist",
@@ -13,6 +13,14 @@ import data from '@emoji-mart/data/sets/14/apple.json';
13
13
  import { Picker } from 'emoji-mart/';
14
14
 
15
15
  export default {
16
+ name: 'UnnnicEmojiPicker',
17
+ props: {
18
+ returnName: {
19
+ type: Boolean,
20
+ default: false,
21
+ },
22
+ },
23
+ emits: ['close', 'emojiSelected'],
16
24
  computed: {
17
25
  emojiPickerPreferences() {
18
26
  return {
@@ -42,7 +50,7 @@ export default {
42
50
  });
43
51
  },
44
52
  onEmojiSelect(emoji) {
45
- this.$emit('emojiSelected', emoji.native);
53
+ this.$emit('emojiSelected', this.returnName ? emoji.id : emoji.native);
46
54
  },
47
55
  },
48
56
  };
@@ -0,0 +1,129 @@
1
+ <template>
2
+ <TourMask
3
+ v-if="isTourActive"
4
+ :maskRect="maskRect"
5
+ />
6
+
7
+ <TourPopover
8
+ v-if="isTourActive"
9
+ :step="currentStepOptions"
10
+ :stepsLength="steps.length"
11
+ :currentStep="currentStep"
12
+ :attachedElement="currentStepOptions.attachedElement"
13
+ @end="end"
14
+ @next-step="nextStep"
15
+ />
16
+ </template>
17
+
18
+ <script>
19
+ import TourMask from './TourMask.vue';
20
+ import TourPopover from './TourPopover.vue';
21
+
22
+ import { validateSteps } from './propsValidation';
23
+
24
+ export default {
25
+ name: 'UnnnicTour',
26
+
27
+ components: {
28
+ TourMask,
29
+ TourPopover,
30
+ },
31
+
32
+ props: {
33
+ steps: {
34
+ required: true,
35
+ type: Array,
36
+ validator: validateSteps,
37
+ },
38
+ },
39
+
40
+ data() {
41
+ return {
42
+ isTourActive: false,
43
+ currentStep: 1,
44
+ maskRect: {
45
+ x: 0,
46
+ y: 0,
47
+ width: 0,
48
+ height: 0,
49
+ },
50
+ };
51
+ },
52
+
53
+ computed: {
54
+ currentStepOptions() {
55
+ return this.steps[this.currentStep - 1];
56
+ },
57
+ stepStyle() {
58
+ const attachedElement = this.currentStepOptions.attachedElement;
59
+
60
+ if (!this.isTourActive || !attachedElement) {
61
+ return {};
62
+ }
63
+
64
+ const { top, left, width, height } =
65
+ attachedElement.getBoundingClientRect();
66
+ return {
67
+ top: `${top}px`,
68
+ left: `${left}px`,
69
+ width: `${width}px`,
70
+ height: `${height}px`,
71
+ };
72
+ },
73
+ },
74
+
75
+ watch: {
76
+ currentStep() {
77
+ this.updateMaskRect();
78
+ },
79
+ isTourActive(isTourActive) {
80
+ if (isTourActive) {
81
+ this.updateMaskRect();
82
+ }
83
+ },
84
+ },
85
+
86
+ methods: {
87
+ start() {
88
+ this.isTourActive = true;
89
+ this.currentStep = 1;
90
+ },
91
+ end() {
92
+ this.isTourActive = false;
93
+ this.currentStep = 1;
94
+ },
95
+ handleStep(step) {
96
+ if (this.currentStep < this.steps.length) {
97
+ this.currentStep = step;
98
+ }
99
+ },
100
+ nextStep() {
101
+ const { handleStep, steps, currentStep, end } = this;
102
+
103
+ if (currentStep === steps.length) {
104
+ end();
105
+ return;
106
+ }
107
+
108
+ handleStep(currentStep + 1);
109
+ },
110
+ updateMaskRect() {
111
+ const attachedElement = this.currentStepOptions?.attachedElement;
112
+ const { padding } = this.currentStepOptions;
113
+
114
+ if (!attachedElement) {
115
+ return;
116
+ }
117
+
118
+ const { top, left, width, height } =
119
+ attachedElement.getBoundingClientRect();
120
+ this.maskRect = {
121
+ x: left - (padding?.horizontal || 0) / 2,
122
+ y: top - (padding?.vertical || 0) / 2,
123
+ width: width + (padding?.horizontal || 0),
124
+ height: height + (padding?.vertical || 0),
125
+ };
126
+ },
127
+ },
128
+ };
129
+ </script>
@@ -0,0 +1,135 @@
1
+ <template>
2
+ <section class="unnnic-tour__mask">
3
+ <svg
4
+ class="mask__svg"
5
+ width="100%"
6
+ height="100%"
7
+ >
8
+ <defs>
9
+ <mask
10
+ id="svgTourMask"
11
+ x="0"
12
+ y="0"
13
+ width="100%"
14
+ height="100%"
15
+ >
16
+ <rect
17
+ x="0"
18
+ y="0"
19
+ width="100vw"
20
+ height="100vh"
21
+ fill="white"
22
+ />
23
+ <rect
24
+ :x="maskRect.x"
25
+ :y="maskRect.y"
26
+ :width="maskRect.width"
27
+ :height="maskRect.height"
28
+ rx="4"
29
+ fill="black"
30
+ class="unnnic-tour__step"
31
+ />
32
+ </mask>
33
+ </defs>
34
+ <rect
35
+ x="0"
36
+ y="0"
37
+ width="100%"
38
+ height="100%"
39
+ mask="url(#svgTourMask)"
40
+ class="unnnic-tour__overlay"
41
+ />
42
+ <rect
43
+ v-for="(rect, index) of clickBlockerRects"
44
+ :key="index"
45
+ :x="rect.x"
46
+ :y="rect.y"
47
+ :width="rect.width"
48
+ :height="rect.height"
49
+ :style="rect.style"
50
+ fill="transparent"
51
+ pointer-events="auto"
52
+ />
53
+ </svg>
54
+ </section>
55
+ </template>
56
+
57
+ <script>
58
+ export default {
59
+ name: 'TourMask',
60
+
61
+ props: {
62
+ maskRect: {
63
+ type: Object,
64
+ required: true,
65
+ },
66
+ },
67
+
68
+ computed: {
69
+ clickBlockerRects() {
70
+ const { maskRect } = this;
71
+ return [
72
+ {
73
+ x: '0',
74
+ y: '0',
75
+ width: '100%',
76
+ height: maskRect.y,
77
+ },
78
+ {
79
+ x: '0',
80
+ y: '0',
81
+ width: maskRect.x,
82
+ height: '100%',
83
+ },
84
+ {
85
+ x: '0',
86
+ y: maskRect.y + maskRect.height,
87
+ style: { height: `calc(100vh - ${maskRect.y}px)` },
88
+ width: '100%',
89
+ },
90
+ {
91
+ x: maskRect.x + maskRect.width,
92
+ y: '0',
93
+ style: { width: `calc(100vw - ${maskRect.x}px)` },
94
+ height: '100%',
95
+ },
96
+ ];
97
+ },
98
+ },
99
+ };
100
+ </script>
101
+
102
+ <style lang="scss" scoped>
103
+ @import '../../assets/scss/unnnic.scss';
104
+
105
+ .unnnic-tour {
106
+ &__mask {
107
+ position: fixed;
108
+ z-index: 1000;
109
+
110
+ inset: 0;
111
+
112
+ pointer-events: none;
113
+
114
+ width: 100%;
115
+ height: 100%;
116
+
117
+ &__svg {
118
+ position: absolute;
119
+ top: 0;
120
+ left: 0;
121
+
122
+ width: 100%;
123
+ height: 100%;
124
+ }
125
+ }
126
+
127
+ &__step {
128
+ transition: all 0.2s ease;
129
+ }
130
+
131
+ &__overlay {
132
+ fill: rgba($unnnic-color-neutral-black, $unnnic-opacity-level-overlay);
133
+ }
134
+ }
135
+ </style>
@@ -0,0 +1,229 @@
1
+ <template>
2
+ <section
3
+ ref="popover"
4
+ :class="['unnnic-tour__popover', step.popoverPosition]"
5
+ :style="style"
6
+ >
7
+ <header class="popover__header">
8
+ <h1 class="header__title">{{ step.title }}</h1>
9
+ <p
10
+ class="header__close-tour"
11
+ @click="$emit('end')"
12
+ >
13
+ {{ i18n('close_tour') }}
14
+ </p>
15
+ </header>
16
+ <p class="popover__description">{{ step.description }}</p>
17
+ <UnnnicButton
18
+ type="primary"
19
+ @click="$emit('nextStep')"
20
+ >
21
+ {{ i18n('understood') }} {{ currentStep }}/{{ stepsLength }}
22
+ </UnnnicButton>
23
+ </section>
24
+ </template>
25
+
26
+ <script>
27
+ import UnnnicI18n from '@/mixins/i18n';
28
+
29
+ import UnnnicButton from '@/components/Button/Button.vue';
30
+
31
+ export default {
32
+ name: 'UnnnicTourPopover',
33
+
34
+ components: {
35
+ UnnnicButton,
36
+ },
37
+
38
+ mixins: [UnnnicI18n],
39
+
40
+ props: {
41
+ step: {
42
+ type: Object,
43
+ required: true,
44
+ },
45
+ currentStep: {
46
+ type: Number,
47
+ required: true,
48
+ },
49
+ stepsLength: {
50
+ type: Number,
51
+ required: true,
52
+ },
53
+ attachedElement: {
54
+ type: Element,
55
+ required: true,
56
+ },
57
+ },
58
+
59
+ emits: ['end', 'nextStep'],
60
+
61
+ data() {
62
+ return {
63
+ style: {},
64
+ defaultTranslations: {
65
+ close_tour: {
66
+ 'pt-br': 'Fechar tour',
67
+ en: 'Close tour',
68
+ es: 'Cerrar recorrido',
69
+ },
70
+ understood: {
71
+ 'pt-br': 'Entendi',
72
+ en: 'I understood',
73
+ es: 'Entendí',
74
+ },
75
+ },
76
+ };
77
+ },
78
+
79
+ watch: {
80
+ attachedElement: {
81
+ immediate: true,
82
+ deep: true,
83
+ handler() {
84
+ this.$nextTick(() => {
85
+ this.updatePopoverStyle();
86
+ });
87
+ },
88
+ },
89
+ },
90
+
91
+ methods: {
92
+ updatePopoverStyle() {
93
+ const { attachedElement, step } = this;
94
+
95
+ const popoverElement = this.$refs.popover;
96
+
97
+ if (!attachedElement || !popoverElement) {
98
+ return;
99
+ }
100
+
101
+ const {
102
+ top: attachedTop,
103
+ left: attachedLeft,
104
+ width: attachedWidth,
105
+ height: attachedHeight,
106
+ } = attachedElement.getBoundingClientRect();
107
+ const popoverArrowSize = 12;
108
+ const popoverArrowMargin = 2;
109
+ const popoverArrowSpacing = popoverArrowSize + popoverArrowMargin;
110
+ const popoverWidth = popoverElement?.offsetWidth;
111
+ const popoverHeight = popoverElement?.offsetHeight;
112
+
113
+ const translateXMap = {
114
+ top: attachedLeft - (popoverWidth - attachedWidth) / 2,
115
+ bottom: attachedLeft - (popoverWidth - attachedWidth) / 2,
116
+ right: attachedLeft + attachedWidth + popoverArrowSpacing,
117
+ left: attachedLeft - popoverWidth - popoverArrowSpacing,
118
+ };
119
+ const translateYMap = {
120
+ top: attachedTop - popoverHeight - popoverArrowSpacing,
121
+ bottom: attachedTop + attachedHeight + popoverArrowSpacing,
122
+ right: attachedTop - popoverHeight / 2 + attachedHeight / 2,
123
+ left: attachedTop - popoverHeight / 2 + attachedHeight / 2,
124
+ };
125
+
126
+ let style = {
127
+ transform: `translate(${translateXMap[step.popoverPosition]}px, ${translateYMap[step.popoverPosition]}px)`,
128
+ transition: 'transform .3s ease',
129
+ };
130
+
131
+ this.style = style;
132
+ },
133
+ },
134
+ };
135
+ </script>
136
+ <style lang="scss" scoped>
137
+ @import '../../assets/scss/unnnic.scss';
138
+
139
+ * {
140
+ margin: 0;
141
+ padding: 0;
142
+ box-sizing: border-box;
143
+ }
144
+
145
+ .unnnic-tour__popover {
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: $unnnic-spacing-ant;
149
+ position: fixed;
150
+ top: 0;
151
+ left: 0;
152
+
153
+ z-index: 1001;
154
+
155
+ border-radius: $unnnic-border-radius-sm;
156
+ box-shadow: $unnnic-shadow-level-near;
157
+ background: $unnnic-color-background-white;
158
+ padding: $unnnic-spacing-ant;
159
+
160
+ width: max-content;
161
+ max-width: 30vw;
162
+
163
+ color: $unnnic-color-neutral-darkest;
164
+ font-family: $unnnic-font-family-secondary;
165
+ font-size: $unnnic-font-size-body-gt;
166
+ font-weight: $unnnic-font-weight-regular;
167
+
168
+ $arrowSize: $unnnic-icon-size-xs;
169
+ $arrowHalfSize: calc($arrowSize / 2);
170
+
171
+ &::before {
172
+ content: '';
173
+ position: absolute;
174
+
175
+ width: $unnnic-icon-size-xs;
176
+ height: $unnnic-icon-size-xs;
177
+
178
+ border-radius: calc($unnnic-border-radius-sm / 2);
179
+
180
+ background: linear-gradient(
181
+ -45deg,
182
+ $unnnic-color-background-white 50%,
183
+ transparent 50%
184
+ );
185
+ }
186
+
187
+ &.top::before {
188
+ left: 50%;
189
+ bottom: -$arrowHalfSize;
190
+ transform: translateX(-50%) rotate(45deg);
191
+ }
192
+
193
+ &.bottom::before {
194
+ top: -$arrowHalfSize;
195
+ left: 50%;
196
+ transform: translateX(-50%) rotate(225deg);
197
+ }
198
+
199
+ &.left::before {
200
+ right: -$arrowHalfSize;
201
+ top: 50%;
202
+ transform: translateY(-50%) rotate(315deg);
203
+ }
204
+
205
+ &.right::before {
206
+ left: -$arrowHalfSize;
207
+ top: 50%;
208
+ transform: translateY(-50%) rotate(135deg);
209
+ }
210
+
211
+ .popover__header {
212
+ display: flex;
213
+ justify-content: space-between;
214
+ align-items: center;
215
+ gap: $unnnic-spacing-nano;
216
+
217
+ .header__title {
218
+ font-weight: $unnnic-font-weight-bold;
219
+ font-size: $unnnic-font-size-body-gt;
220
+ }
221
+ .header__close-tour {
222
+ cursor: pointer;
223
+
224
+ font-size: $unnnic-font-size-body-md;
225
+ text-decoration-line: underline;
226
+ }
227
+ }
228
+ }
229
+ </style>
@@ -0,0 +1,82 @@
1
+ const validateStep = (step) => {
2
+ const { title, description, attachedElement, popoverPosition } = step;
3
+ const hasObrigatoryProps =
4
+ 'title' in step &&
5
+ 'description' in step &&
6
+ 'attachedElement' in step &&
7
+ 'popoverPosition' in step;
8
+
9
+ if (!hasObrigatoryProps) {
10
+ throw new Error(
11
+ 'Each item in "steps" must have "title", "description", "attachedElement" and "popoverPosition".',
12
+ );
13
+ }
14
+
15
+ const isTitleInvalid = typeof title !== 'string' || !title.trim();
16
+ if (isTitleInvalid) {
17
+ throw new Error('"title" must be a padded string');
18
+ }
19
+
20
+ const isDescriptionInvalid =
21
+ typeof description !== 'string' || !description.trim();
22
+ if (isDescriptionInvalid) {
23
+ throw new Error('"description" must be a padded string');
24
+ }
25
+
26
+ const isAttachedElementInvalid = !(attachedElement instanceof HTMLElement);
27
+ if (isAttachedElementInvalid) {
28
+ throw new Error('"attachedElement" must be an HTML element.');
29
+ }
30
+ const validPopoverPositions = ['top', 'bottom', 'left', 'right'];
31
+ const isPopoverPositionInvalid =
32
+ !validPopoverPositions.includes(popoverPosition);
33
+ if (isPopoverPositionInvalid) {
34
+ throw new Error(
35
+ '"popoverPosition" must be one of "top", "bottom", "left", or "right".',
36
+ );
37
+ }
38
+
39
+ if ('padding' in step) {
40
+ const isPaddingInvalidType =
41
+ typeof step.padding !== 'object' || step.padding === null;
42
+ if (isPaddingInvalidType) {
43
+ throw new Error(
44
+ 'Each item in "steps" that contains "padding" must assign it as an object.',
45
+ );
46
+ }
47
+
48
+ const isPaddingEmpty = !(step.padding.vertical || step.padding.horizontal);
49
+ if (isPaddingEmpty) {
50
+ throw new Error(
51
+ 'Each item in "steps" that contains "padding" must assign it as an object with "vertical" and/or "horizontal" keys.',
52
+ );
53
+ }
54
+
55
+ const isPaddingVerticalInvalid =
56
+ 'vertical' in step.padding && typeof step.padding.vertical !== 'number';
57
+ if (isPaddingVerticalInvalid) {
58
+ throw new Error('"vertical" in "padding" must be a number.');
59
+ }
60
+
61
+ const isPaddingHorizontalInvalid =
62
+ 'horizontal' in step.padding &&
63
+ typeof step.padding.horizontal !== 'number';
64
+ if (isPaddingHorizontalInvalid) {
65
+ throw new Error('"horizontal" in "padding" must be a number.');
66
+ }
67
+ }
68
+ };
69
+
70
+ export const validateSteps = (steps) => {
71
+ if (!Array.isArray(steps)) {
72
+ throw new Error('Property steps needs to be a padded array.');
73
+ }
74
+
75
+ if (!steps?.[0]) {
76
+ throw new Error('Steps property cannot be an empty array.');
77
+ }
78
+
79
+ steps.forEach(validateStep);
80
+
81
+ return true;
82
+ };
@@ -82,6 +82,7 @@ import Drawer from './Drawer/Drawer.vue';
82
82
  import TableNext from './TableNext/TableNext.vue';
83
83
  import ModalNext from './ModalNext/ModalNext.vue';
84
84
  import ModalDialog from './ModalDialog/ModalDialog.vue';
85
+ import Tour from './Tour/Tour.vue';
85
86
 
86
87
  export const components = {
87
88
  unnnicFormElement: formElement,
@@ -169,6 +170,7 @@ export const components = {
169
170
  unnnicDisclaimer: Disclaimer,
170
171
  unnnicDrawer: Drawer,
171
172
  unnnicTableNext: TableNext,
173
+ unnnicTour: Tour,
172
174
  };
173
175
 
174
176
  export const unnnicFontSize = fontSize;
@@ -257,3 +259,4 @@ export const unnnicChartFunnel = ChartFunnel;
257
259
  export const unnnicDisclaimer = Disclaimer;
258
260
  export const unnnicDrawer = Drawer;
259
261
  export const unnnicTableNext = TableNext;
262
+ export const unnnicTour = Tour;
@@ -1,5 +1,5 @@
1
- import unnnicButton from '../components/Button/Button.vue';
2
- import unnnicEmojiPicker from '../components/EmojiPicker/EmojiPicker.vue';
1
+ import UnnnicButton from '../components/Button/Button.vue';
2
+ import UnnnicEmojiPicker from '../components/EmojiPicker/EmojiPicker.vue';
3
3
 
4
4
  export default {
5
5
  title: 'Form/EmojiPicker',
@@ -12,11 +12,11 @@ export default {
12
12
  };
13
13
 
14
14
  export const Default = () => ({
15
- components: { unnnicEmojiPicker, unnnicButton },
15
+ components: { UnnnicEmojiPicker, UnnnicButton },
16
16
  template: `
17
17
  <section style="position: relative;">
18
- <unnnicButton @click.stop="handleEmojiPicker" type="secondary" iconCenter="emoji" size="small"/>
19
- <unnnicEmojiPicker
18
+ <UnnnicButton @click.stop="handleEmojiPicker" type="secondary" iconCenter="emoji" size="small"/>
19
+ <UnnnicEmojiPicker
20
20
  v-show="isEmojiPickerOpen"
21
21
  @emojiSelected="handleInput"
22
22
  @close="closeEmojiPicker"
@@ -57,3 +57,51 @@ export const Default = () => ({
57
57
  },
58
58
  },
59
59
  });
60
+
61
+ export const EmojiName = () => ({
62
+ components: { UnnnicEmojiPicker, UnnnicButton },
63
+ template: `
64
+ <section style="position: relative;">
65
+ <UnnnicButton @click.stop="handleEmojiPicker" type="secondary" iconCenter="emoji" size="small"/>
66
+ <UnnnicEmojiPicker
67
+ v-show="isEmojiPickerOpen"
68
+ returnName
69
+ @emojiSelected="handleInput"
70
+ @close="closeEmojiPicker"
71
+ />
72
+ <input
73
+ placeholder="..."
74
+ :value="inputValue"
75
+ @input="handleInput"
76
+ />
77
+ </section>
78
+ `,
79
+ data() {
80
+ return {
81
+ isEmojiPickerOpen: false,
82
+ inputValue: '',
83
+ };
84
+ },
85
+ methods: {
86
+ openEmojiPicker() {
87
+ this.isEmojiPickerOpen = true;
88
+ },
89
+ closeEmojiPicker() {
90
+ this.isEmojiPickerOpen = false;
91
+ },
92
+ handleEmojiPicker() {
93
+ if (this.isEmojiPickerOpen) {
94
+ this.closeEmojiPicker();
95
+ } else {
96
+ this.openEmojiPicker();
97
+ }
98
+ },
99
+ handleInput(event) {
100
+ if (typeof event === 'string') {
101
+ this.inputValue = event;
102
+ } else {
103
+ this.inputValue = event.target.value;
104
+ }
105
+ },
106
+ },
107
+ });