@bagelink/vue 0.0.322 → 0.0.335

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 (72) hide show
  1. package/dist/components/ComboBox.vue.d.ts.map +1 -1
  2. package/dist/components/MaterialIcon.vue.d.ts +4 -2
  3. package/dist/components/MaterialIcon.vue.d.ts.map +1 -1
  4. package/dist/components/Modal.vue.d.ts +0 -1
  5. package/dist/components/ModalBglForm.vue.d.ts.map +1 -1
  6. package/dist/components/Popover.vue.d.ts +10 -0
  7. package/dist/components/Popover.vue.d.ts.map +1 -0
  8. package/dist/components/form/BglForm.vue.d.ts +1 -9
  9. package/dist/components/form/BglForm.vue.d.ts.map +1 -1
  10. package/dist/components/form/inputs/DateInput.vue.d.ts +0 -1
  11. package/dist/components/form/inputs/SelectInput.vue.d.ts +22 -16
  12. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  13. package/dist/components/form/inputs/TextArea.vue.d.ts +3 -1
  14. package/dist/components/form/inputs/TextArea.vue.d.ts.map +1 -1
  15. package/dist/components/form/inputs/TextInput.vue.d.ts.map +1 -1
  16. package/dist/components/formkit/index.d.ts +1 -14
  17. package/dist/components/formkit/index.d.ts.map +1 -1
  18. package/dist/components/index.d.ts +0 -2
  19. package/dist/components/index.d.ts.map +1 -1
  20. package/dist/components/layout/Layout.vue.d.ts +8 -0
  21. package/dist/components/layout/Layout.vue.d.ts.map +1 -1
  22. package/dist/components/layout/SidebarMenu.vue.d.ts.map +1 -1
  23. package/dist/components/layout/TabbedLayout.vue.d.ts +38 -0
  24. package/dist/components/layout/TabbedLayout.vue.d.ts.map +1 -0
  25. package/dist/components/layout/Tabs.vue.d.ts +24 -0
  26. package/dist/components/layout/Tabs.vue.d.ts.map +1 -0
  27. package/dist/components/layout/TabsBody.vue.d.ts +21 -0
  28. package/dist/components/layout/TabsBody.vue.d.ts.map +1 -0
  29. package/dist/components/layout/TabsNav.vue.d.ts +25 -0
  30. package/dist/components/layout/TabsNav.vue.d.ts.map +1 -0
  31. package/dist/components/layout/index.d.ts +4 -0
  32. package/dist/components/layout/index.d.ts.map +1 -1
  33. package/dist/components/layout/tabsManager.d.ts +4 -0
  34. package/dist/components/layout/tabsManager.d.ts.map +1 -0
  35. package/dist/index.cjs +783 -2035
  36. package/dist/index.d.ts +0 -1
  37. package/dist/index.mjs +785 -2037
  38. package/dist/style.css +2129 -695
  39. package/dist/types/index.d.ts +7 -1
  40. package/dist/types/index.d.ts.map +1 -1
  41. package/dist/types/materialIcon.d.ts +2 -0
  42. package/dist/types/materialIcon.d.ts.map +1 -0
  43. package/dist/types/materialIcons.d.ts +1 -1
  44. package/dist/types/materialIcons.d.ts.map +1 -1
  45. package/dist/utils/index.d.ts.map +1 -1
  46. package/package.json +1 -1
  47. package/src/components/MaterialIcon.vue +3 -2
  48. package/src/components/PageTitle.vue +10 -20
  49. package/src/components/form/BglField.vue +2 -2
  50. package/src/components/form/BglForm.vue +49 -53
  51. package/src/components/form/inputs/SelectInput.vue +128 -484
  52. package/src/components/form/inputs/TextInput.vue +158 -112
  53. package/src/components/index.ts +0 -2
  54. package/src/components/layout/Layout.vue +34 -13
  55. package/src/components/layout/SidebarMenu.vue +22 -22
  56. package/src/components/layout/TabbedLayout.vue +79 -0
  57. package/src/components/layout/Tabs.vue +19 -0
  58. package/src/components/layout/TabsBody.vue +15 -0
  59. package/src/components/layout/TabsNav.vue +48 -0
  60. package/src/components/layout/index.ts +4 -0
  61. package/src/components/layout/tabsManager.ts +18 -0
  62. package/src/styles/appearance.css +81 -0
  63. package/src/styles/bagel.css +2 -0
  64. package/src/styles/layout.css +162 -17
  65. package/src/styles/mobilLayout.css +1476 -0
  66. package/src/styles/text.css +153 -1
  67. package/src/types/index.ts +9 -1
  68. package/src/types/materialIcons.ts +1180 -1178
  69. package/src/utils/BagelFormUtils.ts +1 -1
  70. package/src/utils/index.ts +0 -1
  71. package/src/components/ComboBox.vue +0 -165
  72. package/src/components/TabbedLayout.vue +0 -87
@@ -1,65 +1,110 @@
1
1
  <template>
2
- <div class="bagel-input text-input" :class="{
3
- dense, small, shrink, toggleEdit, editMode, code,
4
- textInputIconWrap: icon,
5
- txtInputIconStart: iconStart,
6
- }" :title="title">
7
- <label :for="id">
8
- {{ label }}
9
- <input v-if="!multiline && !autoheight && !code" :title="title" :autocomplete="autocomplete" :id="id"
10
- v-model="inputVal" :type="type" :rows="1" ref="input" :placeholder="placeholder || label" :disabled="!editMode"
11
- :required="required" :pattern="pattern" v-bind="nativeInputAttrs" @dblclick="toggleEditAction">
12
- <textarea v-else :title="title" :id="id" v-model="inputVal" :type="type" :rows="rows" ref="input"
13
- :placeholder="placeholder || label" :disabled="!editMode" :required="required" :pattern="pattern"
14
- v-bind="nativeInputAttrs" @dblclick="toggleEditAction" />
15
- <p v-if="helptext">{{ helptext }}</p>
16
- </label>
17
-
18
- <MaterialIcon class="iconStart" v-if="iconStart" :icon="iconStart" />
19
- <MaterialIcon v-if="icon" :icon="icon" />
20
- <Btn class="toggleEditBtn" v-if="toggleEdit" thin @click="toggleEditAction" :icon="editMode ? 'check'
21
- : 'edit'" flat />
22
- </div>
2
+ <div
3
+ class="bagel-input text-input"
4
+ :class="{
5
+ dense, small, shrink, toggleEdit, editMode, code,
6
+ textInputIconWrap: icon,
7
+ txtInputIconStart: iconStart,
8
+ }"
9
+ :title="title"
10
+ >
11
+ <label :for="id">
12
+ {{ label }}
13
+ <input
14
+ v-if="!multiline && !autoheight && !code"
15
+ :title="title"
16
+ :autocomplete="autocomplete"
17
+ :id="id"
18
+ v-model="inputVal"
19
+ :type="type"
20
+ :rows="1"
21
+ ref="input"
22
+ :placeholder="placeholder || label"
23
+ :disabled="!editMode"
24
+ :required="required"
25
+ :pattern="pattern"
26
+ v-bind="nativeInputAttrs"
27
+ @dblclick="toggleEditAction"
28
+ >
29
+ <textarea
30
+ v-else
31
+ :title="title"
32
+ :id="id"
33
+ v-model="inputVal"
34
+ :type="type"
35
+ :rows="rows"
36
+ ref="input"
37
+ :placeholder="placeholder || label"
38
+ :disabled="!editMode"
39
+ :required="required"
40
+ :pattern="pattern"
41
+ v-bind="nativeInputAttrs"
42
+ @dblclick="toggleEditAction"
43
+ />
44
+ <p v-if="helptext">{{ helptext }}</p>
45
+ </label>
46
+
47
+ <MaterialIcon
48
+ class="iconStart"
49
+ v-if="iconStart"
50
+ :icon="iconStart"
51
+ />
52
+ <MaterialIcon
53
+ v-if="icon"
54
+ :icon="icon"
55
+ />
56
+ <Btn
57
+ class="toggleEditBtn"
58
+ v-if="toggleEdit"
59
+ thin
60
+ @click="toggleEditAction"
61
+ :icon="editMode ? 'check' : 'edit'"
62
+ flat
63
+ />
64
+ </div>
23
65
  </template>
24
66
 
25
- <script setup lang="ts">
67
+ <script
68
+ setup
69
+ lang="ts"
70
+ >
26
71
  import { onMounted, watch } from 'vue';
27
72
  import {
28
- debounce, Btn, type MaterialIcons, MaterialIcon,
73
+ debounce, Btn, type MaterialIcons, MaterialIcon,
29
74
  } from '@bagelink/vue';
30
75
 
31
76
  const emit = defineEmits(['update:modelValue', 'debounce']);
32
77
  const props = withDefaults(
33
- defineProps<{
34
- id?: string;
35
- title?: string;
36
- helptext?: string;
37
- placeholder?: string;
38
- modelValue?: string | number;
39
- label?: string;
40
- small?: boolean;
41
- dense?: boolean;
42
- required?: boolean;
43
- pattern?: string;
44
- shrink?: boolean;
45
- toggleEdit?: boolean;
46
- type?: string;
47
- nativeInputAttrs?: Record<string, any>;
48
- icon?: MaterialIcons;
49
- iconStart?: MaterialIcons;
50
- multiline?: boolean;
51
- autoheight?: boolean;
52
- code?: boolean;
53
- lines?: number;
54
- autocomplete?: string;
55
- autofocus?: boolean;
56
- }>(),
57
- {
58
- type: 'text',
59
- toggleEdit: false,
60
- modelValue: '',
61
- autocomplete: 'off',
62
- },
78
+ defineProps<{
79
+ id?: string;
80
+ title?: string;
81
+ helptext?: string;
82
+ placeholder?: string;
83
+ modelValue?: string | number;
84
+ label?: string;
85
+ small?: boolean;
86
+ dense?: boolean;
87
+ required?: boolean;
88
+ pattern?: string;
89
+ shrink?: boolean;
90
+ toggleEdit?: boolean;
91
+ type?: string;
92
+ nativeInputAttrs?: Record<string, any>;
93
+ icon?: MaterialIcons;
94
+ iconStart?: MaterialIcons;
95
+ multiline?: boolean;
96
+ autoheight?: boolean;
97
+ code?: boolean;
98
+ lines?: number;
99
+ autocomplete?: string;
100
+ autofocus?: boolean;
101
+ }>(),
102
+ {
103
+ type: 'text',
104
+ toggleEdit: false,
105
+ modelValue: '',
106
+ autocomplete: 'off',
107
+ },
63
108
  );
64
109
  let inputVal = $ref<string | number>();
65
110
  let editMode = $ref<boolean>(!props.toggleEdit);
@@ -67,53 +112,54 @@ let editMode = $ref<boolean>(!props.toggleEdit);
67
112
  const input = $ref<HTMLInputElement>();
68
113
 
69
114
  const toggleEditAction = () => {
70
- if (!props.toggleEdit) return;
71
- if (editMode) {
72
- editMode = false;
73
- emit('debounce', inputVal);
74
- emit('update:modelValue', inputVal as string);
75
- } else {
76
- editMode = true;
77
- setTimeout(() => input?.focus(), 10);
78
- }
115
+ if (!props.toggleEdit) return;
116
+ if (editMode) {
117
+ editMode = false;
118
+ emit('debounce', inputVal);
119
+ emit('update:modelValue', inputVal as string);
120
+ } else {
121
+ editMode = true;
122
+ setTimeout(() => input?.focus(), 10);
123
+ }
79
124
  };
80
125
 
81
126
  const rows = $computed(() => {
82
- if (props.lines) return props.lines;
83
- if (props.autoheight) return `${inputVal}`?.split('\n').length || 1;
84
- if (props.multiline || props.code) return 4;
85
- return 1;
127
+ if (props.lines) return props.lines;
128
+ if (props.autoheight) return `${inputVal}`?.split('\n').length || 1;
129
+ if (props.multiline || props.code) return 4;
130
+ return 1;
86
131
  });
87
132
 
88
133
  watch(
89
- () => inputVal,
90
- (newVal) => {
91
- if (props.toggleEdit) return;
92
- if (newVal === props.modelValue) return;
93
- emit('update:modelValue', newVal as string);
94
- debounce(() => emit('debounce', newVal));
95
- },
134
+ () => inputVal,
135
+ (newVal) => {
136
+ if (props.toggleEdit) return;
137
+ if (newVal === props.modelValue) return;
138
+ emit('update:modelValue', newVal as string);
139
+ debounce(() => emit('debounce', newVal));
140
+ },
96
141
  );
97
142
 
98
143
  watch(
99
- () => props.modelValue,
100
- (newVal) => {
101
- if (newVal !== inputVal) inputVal = newVal;
102
- },
103
- { immediate: true },
144
+ () => props.modelValue,
145
+ (newVal) => {
146
+ if (newVal !== inputVal) inputVal = newVal;
147
+ },
148
+ { immediate: true },
104
149
  );
105
150
 
106
151
  onMounted(() => {
107
- if (props.autofocus) setTimeout(() => input?.focus(), 10);
152
+ if (props.autofocus) setTimeout(() => input?.focus(), 10);
108
153
  });
109
154
  </script>
110
155
 
111
156
  <style>
112
157
  .bagel-input.shrink,
113
158
  .bagel-input.shrink input {
114
- min-width: unset !important;
115
- /* width: auto; */
159
+ min-width: unset !important;
160
+ /* width: auto; */
116
161
  }
162
+
117
163
  .bagel-input label {
118
164
  font-size: var(--label-font-size);
119
165
  }
@@ -121,76 +167,76 @@ onMounted(() => {
121
167
 
122
168
  <style scoped>
123
169
  .bagel-input textarea {
124
- min-height: unset;
125
- font-size: var(--input-font-size);
170
+ min-height: unset;
171
+ font-size: var(--input-font-size);
126
172
  }
127
173
 
128
174
  .bagel-input.text-input textarea {
129
- resize: none;
175
+ resize: none;
130
176
  }
131
177
 
132
178
  .code textarea {
133
- font-family: 'Inconsolata', monospace;
134
- background: var(--bgl-black) !important;
135
- color: var(--bgl-white) !important;
179
+ font-family: 'Inconsolata', monospace;
180
+ background: var(--bgl-black) !important;
181
+ color: var(--bgl-white) !important;
136
182
  }
137
183
 
138
184
  .bagel-input.toggleEdit:hover {
139
- background-color: var(--input-bg);
185
+ background-color: var(--input-bg);
140
186
  }
141
187
 
142
188
  .bagel-input.small {
143
- margin-bottom: 0;
144
- height: 30px;
189
+ margin-bottom: 0;
190
+ height: 30px;
145
191
  }
146
192
 
147
193
  .bagel-input.dense label {
148
- display: flex;
149
- align-items: center;
150
- gap: 0.5rem;
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 0.5rem;
151
197
  }
152
198
 
153
199
  .bagel-input label {
154
- font-size: var(--label-font-size);
200
+ font-size: var(--label-font-size);
155
201
  }
156
202
 
157
203
  .toggleEditBtn {
158
- position: absolute;
159
- right: -24px;
160
- top: 4;
161
- opacity: 0;
204
+ position: absolute;
205
+ right: -24px;
206
+ top: 4;
207
+ opacity: 0;
162
208
  }
163
209
 
164
210
  .textInputIconWrap {
165
- position: relative;
211
+ position: relative;
166
212
  }
167
213
 
168
214
  .textInputIconWrap .bgl_icon-font {
169
- position: absolute;
170
- inset-inline-end: 0.7rem;
171
- bottom: 50%;
172
- line-height: 0;
173
- color: var(--bgl-gray);
215
+ position: absolute;
216
+ inset-inline-end: 0.7rem;
217
+ bottom: 50%;
218
+ line-height: 0;
219
+ color: var(--bgl-gray);
174
220
  }
175
221
 
176
222
  .txtInputIconStart .iconStart {
177
- position: absolute;
178
- inset-inline-start: 0.7rem;
179
- top: 50%;
180
- line-height: 0;
181
- color: var(--bgl-gray);
223
+ position: absolute;
224
+ inset-inline-start: 0.7rem;
225
+ top: 50%;
226
+ line-height: 0;
227
+ color: var(--bgl-gray);
182
228
  }
183
229
 
184
230
  .txtInputIconStart textarea {
185
- padding-inline-start: 2rem;
231
+ padding-inline-start: 2rem;
186
232
  }
187
233
 
188
234
  .bagel-input:hover .toggleEditBtn,
189
235
  .bagel-input.editMode .toggleEditBtn {
190
- opacity: 1;
236
+ opacity: 1;
191
237
  }
192
238
 
193
239
  .bagel-input.small textarea {
194
- height: 30px;
240
+ height: 30px;
195
241
  }
196
242
  </style>
@@ -8,7 +8,6 @@ export { default as ModalForm } from './ModalForm.vue';
8
8
  export { default as AccordionItem } from './AccordionItem.vue';
9
9
  export { default as ListView } from './ListView.vue';
10
10
  export { default as ListItem } from './ListItem.vue';
11
- export { default as TabbedLayout } from './TabbedLayout.vue';
12
11
  export { default as Comments } from './Comments.vue';
13
12
  export { default as PageTitle } from './PageTitle.vue';
14
13
  export { default as TableSchema } from './TableSchema.vue';
@@ -19,7 +18,6 @@ export { default as Card } from './Card.vue';
19
18
  export { default as Avatar } from './Avatar.vue';
20
19
  export { default as Title } from './Title.vue';
21
20
  export { default as Accordion } from './Accordion.vue';
22
- export { default as ComboBox } from './ComboBox.vue';
23
21
  export { default as Alert } from './Alert.vue';
24
22
  export { default as Badge } from './Badge.vue';
25
23
  export { default as BglVideo } from './BglVideo.vue';
@@ -1,7 +1,7 @@
1
1
  <template>
2
- <div class="layout" :class="{ 'layout-h-100': h100 }" :style="style">
3
- <slot />
4
- </div>
2
+ <div class="layout" :class="{ 'layout-h-100': h100 }">
3
+ <slot />
4
+ </div>
5
5
  </template>
6
6
 
7
7
  <script lang="ts" setup>
@@ -9,27 +9,48 @@ type LayoutProrps = {
9
9
  gap?: number,
10
10
  h100?: boolean,
11
11
  columns?: string[]
12
+ mColumns?: string[],
13
+ mRows?: string[]
12
14
  rows?: string[],
13
15
  }
14
16
 
15
17
  const props = withDefaults(defineProps<LayoutProrps>(), {
16
- gap: 1,
17
- h100: false,
18
- columns: () => [],
19
- rows: () => [],
18
+ gap: 1,
19
+ h100: false,
20
+ columns: () => ([]),
21
+ rows: () => ([]),
22
+ mColumns: () => ([]),
23
+ mRows: () => ([]),
20
24
  });
21
25
 
22
- const style = $computed(() => {
23
- const obj: Record<string, any> = { gap: `${props.gap}rem` };
24
- if (props.columns.length) obj['grid-template-columns'] = props.columns.join(' ');
25
- if (props.rows.length) obj['grid-template-rows'] = props.rows.join(' ');
26
- return obj;
26
+ const gridTemplateRows = $computed(() => (props.rows.length ? props.rows.join(' ') : 'auto'));
27
+ const gapSize = $computed(() => `${props.gap}rem`);
28
+ const mGridTemplateRows = $computed(() => {
29
+ if (props.mRows?.length) return props.mRows.join(' ');
30
+ return gridTemplateRows;
31
+ });
32
+
33
+ const gridTemplateColumns = $computed(() => (props.columns.length ? props.columns.join(' ') : 'auto'));
34
+
35
+ const mGridTemplateColumns = $computed(() => {
36
+ if (props.mColumns?.length) return props.mColumns.join(' ');
37
+ return gridTemplateColumns;
27
38
  });
28
39
  </script>
29
40
 
30
- <style>
41
+ <style scoped>
31
42
  .layout {
32
43
  min-height: 100px;
33
44
  display: grid;
45
+ gap: v-bind(gapSize);
46
+ grid-template-rows: v-bind(gridTemplateRows);
47
+ grid-template-columns: v-bind(gridTemplateColumns);
48
+ }
49
+
50
+ @media screen and (max-width: 910px) {
51
+ .layout {
52
+ grid-template-rows: v-bind(mGridTemplateRows);
53
+ grid-template-columns: v-bind(mGridTemplateColumns);
54
+ }
34
55
  }
35
56
  </style>
@@ -1,28 +1,27 @@
1
1
  <template>
2
- <Card class="flex column gap-05 pt-1 bgl_sidebar" :class="{ 'wideNav': isOpen }">
3
- <div class="w-100 px-075">
4
- <Btn thin color="light" class="toggleNav mb-05" icon="keyboard_arrow_right" @click="toggleMenu" />
5
- </div> <slot v-if="!isOpen || !slots['brand-open']" name="brand" />
6
- <slot v-if="isOpen" name="brand-open" />
7
- <slot v-if="!navLinks" />
8
- <Btn
9
- v-for="(nav, i) in navLinks" :key="i" :to="nav.to" class="nav-button px-075"
10
- v-tooltip.right="{ content: nav.label, disabled: open, class: ['nav-tooltip'] }"
11
- >
12
- <template #default >
13
- <Icon :icon="nav.icon" :size="1.4"/>
14
- <p v-if="open">
15
- {{ nav.label }}
16
- </p>
17
- </template>
18
- </Btn>
19
- <slot name="footer" />
20
- </Card>
2
+ <Card class="flex column gap-05 pt-1 bgl_sidebar" :class="{ 'wideNav': isOpen }">
3
+ <div class="w-100 px-075">
4
+ <Btn thin color="light" class="toggleNav mb-05" icon="keyboard_arrow_right" @click="toggleMenu" />
5
+ </div>
6
+ <slot v-if="!isOpen || !slots['brand-open']" name="brand" />
7
+ <slot v-if="isOpen" name="brand-open" />
8
+ <slot v-if="!navLinks" />
9
+ <Btn v-for="(nav, i) in navLinks" :key="i" :to="nav.to" class="nav-button px-075"
10
+ v-tooltip.right="{ content: nav.label, disabled: open, class: ['nav-tooltip'] }">
11
+ <template #default>
12
+ <Icon :icon="nav.icon" :size="1.4" />
13
+ <p v-if="open">
14
+ {{ nav.label }}
15
+ </p>
16
+ </template>
17
+ </Btn>
18
+ <slot name="footer" />
19
+ </Card>
21
20
  </template>
22
21
 
23
22
  <script lang="ts" setup>
24
23
  import { useSlots } from 'vue';
25
- import { Btn, Icon } from '@bagelink/vue';
24
+ import { Btn, Icon, Card } from '@bagelink/vue';
26
25
  import type { NavLink } from '@bagelink/vue';
27
26
 
28
27
  const slots = useSlots();
@@ -33,8 +32,8 @@ let isOpen = $ref(props.open);
33
32
 
34
33
  const emit = defineEmits(['update:open']);
35
34
  function toggleMenu() {
36
- isOpen = !isOpen;
37
- emit('update:open', isOpen);
35
+ isOpen = !isOpen;
36
+ emit('update:open', isOpen);
38
37
  }
39
38
  </script>
40
39
 
@@ -50,6 +49,7 @@ function toggleMenu() {
50
49
  .wideNav .nav-button .bgl_btn-flex {
51
50
  justify-content: flex-start !important;
52
51
  }
52
+
53
53
  .nav-button.router-link-active {
54
54
  background-color: var(--bgl-primary-tint) !important;
55
55
  }
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <div class="h-100 grid list-view gap-1" :class="{ 'side-tabs': sideTabs }">
3
+ <div class="bgl_card tabs-top">
4
+ <slot name="top-section" />
5
+ <div class="tabs grid auto-flow-columns fit-content">
6
+ <div v-for="tab in tabs" :class="{
7
+ active:
8
+ tab === activeTab ||
9
+ tab === router?.currentRoute.value.path.split('/').slice(-1)[0],
10
+ }" class="tab" :key="tab" @click="changeTab(tab)">
11
+ {{ tab }}
12
+ </div>
13
+ </div>
14
+ </div>
15
+ <div class="list-content" v-for="tab in tabs.filter((item) => item === activeTab)" :key="tab">
16
+ <slot :name="tab" :key="tab" />
17
+ </div>
18
+ </div>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import { onMounted } from 'vue';
23
+ import type { Router } from 'vue-router';
24
+
25
+ const emit = defineEmits(['update:modelValue']);
26
+
27
+ let activeTab = $ref<string>();
28
+ const props = defineProps<{
29
+ title?: string;
30
+ tabs: string[];
31
+ modelValue?: string;
32
+ router?: Router;
33
+ sideTabs?: boolean;
34
+ }>();
35
+
36
+ function changeTab(tab: string) {
37
+ activeTab = tab;
38
+ emit('update:modelValue', activeTab);
39
+ if (!props.router) return;
40
+ void props.router?.push({
41
+ path: props.router.currentRoute.value.path,
42
+ query: { ...props.router.currentRoute.value.query, t: tab },
43
+ hash: props.router.currentRoute.value.hash,
44
+ });
45
+ }
46
+
47
+ onMounted(() => {
48
+ const firstTab =
49
+ props.modelValue ||
50
+ props.router?.currentRoute.value.query.t ||
51
+ props.tabs[0];
52
+ activeTab = firstTab as string;
53
+ });
54
+ </script>
55
+
56
+ <style scoped>
57
+ .tab {
58
+ text-transform: capitalize;
59
+ }
60
+
61
+ .side-tabs {
62
+ display: flex;
63
+ }
64
+
65
+ .side-tabs .tabs-top {
66
+ margin-inline-end: 1rem;
67
+ }
68
+
69
+ .side-tabs .tabs {
70
+ display: block;
71
+ padding: 0;
72
+ margin: 0;
73
+ border: none;
74
+ }
75
+
76
+ .side-tabs .tab {
77
+ border: none;
78
+ }
79
+ </style>
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <TabsNav :tabs="tabs" :group="group" />
3
+ <div v-if="currentTab">
4
+ <slot :name="currentTab" />
5
+ </div>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import { TabsNav, type Tab } from '@bagelink/vue';
10
+ import { useTabs } from './tabsManager';
11
+
12
+ defineProps<{
13
+ tabs: Tab[];
14
+ modelValue?: string;
15
+ }>();
16
+
17
+ const group = Math.random().toString(36).substring(7);
18
+ const { currentTab } = useTabs(group);
19
+ </script>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <div v-if="currentTab">
3
+ <slot :name="currentTab"></slot>
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { useTabs } from './tabsManager';
9
+
10
+ const props = defineProps<{
11
+ group: string;
12
+ }>();
13
+
14
+ const { currentTab } = useTabs(props.group);
15
+ </script>
@@ -0,0 +1,48 @@
1
+ <template>
2
+ <div class="tabs grid auto-flow-columns fit-content">
3
+ <button :class="{ active: isActive(tab) }" @click="selectTab(tab)" class="tab" v-for="(tab, i) in tabs" :key="i">
4
+ <Icon v-if="typeof tab !== 'string' && tab.icon" :icon="tab.icon" />
5
+ {{ tabLabel(tab) }}
6
+ </button>
7
+ </div>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ import { type Tab, Icon } from '@bagelink/vue';
12
+ import { useTabs } from './tabsManager';
13
+
14
+ const props = defineProps<{
15
+ title?: string;
16
+ tabs: Tab[];
17
+ modelValue?: string;
18
+ sideTabs?: boolean;
19
+ group: string;
20
+ }>();
21
+
22
+ const { currentTab } = useTabs(props.group);
23
+ currentTab.value = props.modelValue || typeof props.tabs[0] === 'string' ? props.tabs[0] : props.tabs[0].id;
24
+ const selectTab = (tab: Tab) => {
25
+ currentTab.value = typeof tab === 'string' ? tab : tab.id;
26
+ };
27
+
28
+ const isActive = (tab: Tab) => {
29
+ if (typeof tab === 'string') return currentTab.value === tab;
30
+ return currentTab.value === tab.id;
31
+ };
32
+ const tabLabel = (tab: Tab) => {
33
+ if (typeof tab === 'string') return tab;
34
+ return tab.label;
35
+ };
36
+ </script>
37
+
38
+ <style scoped>
39
+ .tab {
40
+ border: none;
41
+ border-bottom: 1px solid var(--border-color);
42
+ background: transparent;
43
+ }
44
+
45
+ .tab.currentTab {
46
+ border-bottom: 2px solid var(--primary-color);
47
+ }
48
+ </style>
@@ -1,2 +1,6 @@
1
1
  export { default as Layout } from './Layout.vue';
2
2
  export { default as SidebarMenu } from './SidebarMenu.vue';
3
+ export { default as Tabs } from './Tabs.vue';
4
+ export { default as TabsNav } from './TabsNav.vue';
5
+ export { default as TabsBody } from './TabsBody.vue';
6
+ export { default as TabbedLayout } from './TabbedLayout.vue';