PluginProbe
The Innovative Form Builder – IvyForms / 0.2
The Innovative Form Builder – IvyForms v0.2
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / views / _components / tabs / IvyTabs.vue

IvyTabs.vue in The Innovative Form Builder – IvyForms 0.2, at frontend/src/views/_components/tabs/IvyTabs.vue

434 lines 10.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <div :class="['ivyforms-tabs-wrapper', { 'bg-transparent': !background }]">
3 <ElTabs
4 v-model="activeTab"
5 type="card"
6 :tab-position="position"
7 :addable="addTab"
8 :editable="addTab"
9 :stretch="stretch"
10 :class="[
11 `size-${size}`,
12 'ivyforms-tabs-group',
13 `priority-${priority}`,
14 `type-${type}`,
15 { 'bg-transparent': !background },
16 ]"
17 @tab-add="onTabAdd"
18 @tab-remove="onTabRemove"
19 >
20 <ElTabPane
21 v-for="tab in tabs"
22 :id="`tab-panel-${tab.name}`"
23 :key="tab.name"
24 :name="tab.name"
25 :aria-labelledby="`tab-${tab.name}`"
26 role="tabpanel"
27 aria-live="polite"
28 :disabled="tab.disabled"
29 :type="{ minWidth: props.minWidth }"
30 @click="!tab.disabled && (activeTab = tab.name)"
31 >
32 <template #label>
33 <div class="ivyforms-tab-label-wrapper">
34 <IvyIcon v-if="tab.iconConfig?.name" v-bind="tab.iconConfig" />
35 <span
36 :id="`tab-${tab.name}`"
37 role="tab"
38 :aria-selected="activeTab === tab.name"
39 :aria-controls="`tab-panel-${tab.name}`"
40 tabindex="0"
41 >
42 {{ tab.label }}
43 </span>
44 </div>
45 </template>
46 <slot :name="tab.name"></slot>
47 </ElTabPane>
48 </ElTabs>
49 <ComingSoonBadge v-if="props.comingSoon" />
50 </div>
51 </template>
52
53 <script setup lang="ts">
54 import { nanoid } from 'nanoid'
55 import { ref, computed, onMounted, onUnmounted } from 'vue'
56 import IvyIcon from '@/views/_components/icon/IvyIcon.vue'
57 import type { IconType } from '@/types/icons/icon-type'
58 import type { IconCategory } from '@/types/icons/icon-category'
59
60 interface Tab {
61 name: string
62 label: string
63 path?: string
64 disabled?: boolean
65 iconConfig?: {
66 name: string
67 type?: IconType
68 category?: IconCategory
69 size?: 'l' | 'd' | 's' | 'xs'
70 class?: string
71 }
72 }
73
74 interface Props {
75 background?: boolean
76 comingSoon?: boolean
77 priority?: 'primary' | 'secondary' | 'tertiary' | 'amber'
78 size?: 's' | 'd' | 'l'
79 type?: 'filled' | 'tonal' | 'fab'
80 modelValue: string
81 position?: 'left' | 'right' | 'top' | 'bottom'
82 addTab?: boolean
83 stretch?: boolean
84 minWidth?: string
85 tabs: Tab[]
86 }
87
88 const props = withDefaults(defineProps<Props>(), {
89 background: false,
90 priority: 'primary',
91 size: 'd',
92 type: 'filled',
93 position: 'top',
94 addTab: false,
95 stretch: true,
96 minWidth: '50px',
97 })
98
99 // Reactive state for tabs and activeTab
100 const tabs = ref([...props.tabs])
101
102 const emit = defineEmits(['update:modelValue'])
103
104 const activeTab = computed({
105 get: () => props.modelValue,
106 set: (val) => emit('update:modelValue', val),
107 })
108
109 // Add a new tab
110 const onTabAdd = () => {
111 const newTabName = `tab-${nanoid(6)}`
112 tabs.value.push({ name: newTabName, label: `Page ${tabs.value.length + 1}` })
113 activeTab.value = newTabName
114 }
115
116 // Remove a tab
117 const onTabRemove = () => {
118 tabs.value = tabs.value.filter((tab) => tab.name !== activeTab.value)
119 if (tabs.value.length > 0) {
120 activeTab.value = tabs.value[0]?.name || ''
121 }
122 }
123
124 // Keyboard navigation
125 onMounted(() => {
126 document.addEventListener('keydown', handleKeyDown)
127 })
128
129 onUnmounted(() => {
130 document.removeEventListener('keydown', handleKeyDown)
131 })
132
133 const handleKeyDown = (event: KeyboardEvent) => {
134 if (!tabs.value.length) return
135 const currentIndex = tabs.value.findIndex((tab) => tab.name === activeTab.value)
136 if (currentIndex === -1) return
137
138 if (event.key === 'ArrowRight') {
139 // Find the next available tab that's not disabled
140 let nextIndex = (currentIndex + 1) % tabs.value.length
141 while (tabs.value[nextIndex].disabled && nextIndex !== currentIndex) {
142 nextIndex = (nextIndex + 1) % tabs.value.length
143 }
144 if (nextIndex !== currentIndex) {
145 activeTab.value = tabs.value[nextIndex].name
146 }
147 } else if (event.key === 'ArrowLeft') {
148 // Find the previous available tab that's not disabled
149 let prevIndex = (currentIndex - 1 + tabs.value.length) % tabs.value.length
150 while (tabs.value[prevIndex].disabled && prevIndex !== currentIndex) {
151 prevIndex = (prevIndex - 1 + tabs.value.length) % tabs.value.length
152 }
153 if (prevIndex !== currentIndex) {
154 activeTab.value = tabs.value[prevIndex].name
155 }
156 }
157 }
158 </script>
159
160 <style lang="scss">
161 @use 'sass:list' as *;
162 @use 'sass:meta' as *;
163
164 .ivyforms-tabs-wrapper {
165 position: relative;
166 display: flex;
167 box-sizing: border-box;
168 height: fit-content;
169 border-radius: var(--radius-l, 8px);
170 background: var(--map-base-dusk-o10);
171
172 &.bg-transparent {
173 background-color: transparent;
174 }
175
176 .ivyforms-tabs-group {
177 display: inline-flex;
178 justify-content: center;
179 align-items: center;
180 overflow-x: auto;
181 overflow-y: auto;
182 white-space: nowrap;
183 width: 100%;
184 box-sizing: border-box;
185 padding: var(--spacing-01, 4px);
186
187 // Left and right position
188 &.el-tabs--left.el-tabs--card {
189 .el-tabs__item.is-left,
190 .el-tabs__item.is-left:first-child {
191 border: none;
192 }
193 }
194
195 &.el-tabs--right.el-tabs--card {
196 .el-tabs__item.is-right,
197 .el-tabs__item.is-right:first-child {
198 border: none;
199 }
200 }
201
202 // Define priorities and their styles
203 $priorities: (
204 'primary': (
205 transparent,
206 var(--map-base-text-0),
207 var(--map-base-dusk-o10),
208 var(--map-base-brand-fill-0),
209 $primitive-white,
210 var(--map-base-brand-o05),
211 var(--map-base-brand-symbol-0),
212 ),
213 'secondary': (
214 transparent,
215 var(--map-base-text-0),
216 var(--map-base-dusk-o10),
217 var(--map-base-purple-fill-0),
218 $primitive-white,
219 var(--map-base-purple-o05),
220 var(--map-base-purple-symbol-0),
221 ),
222 'tertiary': (
223 transparent,
224 var(--map-base-text-0),
225 var(--map-base-dusk-o10),
226 var(--map-base-dusk-o10),
227 var(--map-base-text-0),
228 var(--map-ground-level-3),
229 var(--map-base-text-0),
230 ),
231 'amber': (
232 transparent,
233 var(--map-base-text-0),
234 var(--map-base-dusk-o10),
235 var(--map-accent-amber-fill-0),
236 $primitive-white,
237 var(--map-accent-amber-o05),
238 var(--map-accent-amber-symbol-0),
239 ),
240 );
241
242 // Mixin for priority styles
243 @mixin tabStyles($priority, $colors: ()) {
244 $default-bg: nth($colors, 1);
245 $default-text: nth($colors, 2);
246 $hover-bg: nth($colors, 3);
247 $active-bg: nth($colors, 4);
248 $active-text: nth($colors, 5);
249 $tonal-bg: nth($colors, 6);
250 $tonal-text: nth($colors, 7);
251
252 &.type-tonal.priority-#{$priority} .el-tabs__item {
253 &.is-active {
254 background-color: $tonal-bg;
255 color: $tonal-text;
256
257 .ivyforms-icon {
258 svg {
259 fill: $tonal-text;
260 }
261 }
262 }
263 }
264 &.type-fab.priority-#{$priority} .el-tabs__item {
265 &.is-active {
266 background: var(--map-ground-level-3);
267 color: $tonal-text;
268 box-shadow: 0 4px 15px 0 rgba(0, 0, 0, 0.05);
269
270 .ivyforms-icon {
271 svg {
272 fill: $tonal-text;
273 }
274 }
275 }
276 }
277
278 &.type-tonal.priority-tertiary .el-tabs__item {
279 &.is-active {
280 box-shadow: 0 4px 15px 0 rgba(0, 0, 0, 0.05);
281 }
282 }
283
284 // Add tab button
285 .el-tabs__new-tab {
286 border: none;
287 background-color: $default-bg;
288 color: $default-text;
289
290 &:hover {
291 color: $default-text;
292 }
293 }
294
295 &.priority-#{$priority} .el-tabs__item {
296 background-color: $default-bg;
297 color: $default-text;
298 transition: background-color 0.3s;
299 border-radius: 8px;
300 display: inline-flex;
301 flex-shrink: 0;
302 min-width: 44px;
303 padding: 10px var(--spacing-04, 16px) !important;
304 justify-content: center;
305 align-items: center;
306
307 &:hover {
308 background-color: $hover-bg;
309 }
310
311 &.is-active {
312 background-color: $active-bg;
313 color: $active-text;
314 transition: background-color 0.3s;
315
316 .ivyforms-icon {
317 svg {
318 fill: $active-text;
319 }
320 }
321 }
322
323 .ivyforms-icon {
324 svg {
325 fill: $default-text;
326 transform: scale(0.67);
327 }
328 }
329 }
330
331 .el-tab-pane {
332 &.priority-#{$priority} {
333 background-color: $default-bg;
334 color: $default-text;
335
336 &.is-active {
337 background-color: $active-bg;
338 }
339 }
340 }
341 }
342
343 // Generate styles for all priorities
344 @each $priority, $colors in $priorities {
345 @include tabStyles($priority, $colors);
346 }
347
348 // Card style adjustments
349 &.el-tabs--card {
350 .el-tabs__header {
351 border: none;
352 min-width: 100%;
353 margin: 0;
354 height: auto;
355
356 .el-tabs__nav,
357 .el-tabs__item {
358 border: none;
359 gap: var(--spacing-01, 4px);
360 flex: 1;
361 text-align: center;
362 min-width: var(--tab-min-width, 50px);
363 }
364
365 // Disabled tab styles
366 .el-tabs__item.is-disabled {
367 cursor: not-allowed;
368 opacity: 0.5;
369
370 &:hover {
371 background-color: transparent;
372 }
373 }
374
375 .el-tabs__nav {
376 display: flex;
377 min-width: 100%;
378 }
379 }
380 }
381
382 // Transparent background for tabs
383 &.bg-transparent {
384 .el-tabs__header {
385 background-color: transparent;
386 .el-tabs__nav-wrap {
387 margin-bottom: 0;
388 }
389 }
390 }
391
392 // Size-specific styles
393 &.size-s {
394 height: 32px;
395 .el-tabs__header {
396 height: 32px;
397 .el-tabs__item {
398 height: 24px;
399 .ivyforms-tab-label-wrapper {
400 margin-top: 1px;
401 }
402 }
403 }
404 }
405
406 &.size-d {
407 height: 40px;
408 .el-tabs__header {
409 height: 40px;
410 .el-tabs__item {
411 height: 32px;
412 }
413 }
414 }
415
416 &.size-l {
417 height: 48px;
418 .el-tabs__header {
419 height: 48px;
420 .el-tabs__item {
421 height: 40px;
422 }
423 }
424 }
425 }
426
427 .ivyforms-coming-soon-badge {
428 position: absolute;
429 top: -9px;
430 right: -8px;
431 }
432 }
433 </style>
434