manolis-ui 1.1.2 → 1.1.4

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/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "manolis-ui",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "configKey": "manolisUI",
5
5
  "compatibility": {
6
6
  "nuxt": ">=3.10.0"
package/dist/module.mjs CHANGED
@@ -13,7 +13,7 @@ const __filename = __cjs_url__.fileURLToPath(import.meta.url);
13
13
  const __dirname = __cjs_path__.dirname(__filename);
14
14
  const require = __cjs_mod__.createRequire(import.meta.url);
15
15
  const name = "manolis-ui";
16
- const version = "1.1.2";
16
+ const version = "1.1.4";
17
17
 
18
18
  function installTailwind(moduleOptions, nuxt = useNuxt(), resolve = createResolver(import.meta.url).resolve) {
19
19
  const runtimeDir = resolve("./runtime");
@@ -110,7 +110,6 @@ const module = defineNuxtModule({
110
110
  "data-input",
111
111
  "feedback",
112
112
  "layout",
113
- "misc",
114
113
  "navigation"
115
114
  ];
116
115
  for (const dir of componentDirs) {
@@ -1,6 +1,8 @@
1
1
  <script setup lang="ts">
2
- import { ref, defineProps, defineEmits, onMounted, onBeforeUnmount, defineAsyncComponent, nextTick } from 'vue';
2
+ import { ref, defineProps, defineAsyncComponent, nextTick } from 'vue';
3
3
  import { Search } from 'lucide-vue-next';
4
+ import { onMounted } from 'vue';
5
+ import { onBeforeUnmount } from 'vue';
4
6
 
5
7
  interface Tab {
6
8
  name: string;
@@ -8,31 +10,21 @@ interface Tab {
8
10
  type: "date" | "time" | "datetime";
9
11
  range: boolean;
10
12
  props?: Object;
11
- }
12
-
13
- export interface SearchTab {
14
- category: string;
15
- tabs: Array<Tab>;
13
+ value?: any;
16
14
  }
17
15
 
18
16
  interface Props {
19
- searchOptions: Array<SearchTab>;
17
+ searchOptions: Array<{ category: string; tabs: Tab[] }>;
18
+ currentCategory: string;
20
19
  }
21
20
 
22
- const props = withDefaults(defineProps<Props>(), {
23
- searchOptions: () => [],
24
- });
25
-
26
- const currentCategory = ref(props.searchOptions[0]?.category || '');
21
+ const props = defineProps<Props>();
22
+ const emit = defineEmits<{
23
+ (e: 'search'): void;
24
+ (e: 'update:search-data', payload: { tab: string, data: any }): void;
25
+ }>();
27
26
  const activeTab = ref<Tab | null>(null);
28
27
 
29
- const emit = defineEmits(['category-changed']);
30
-
31
- function setCurrentCategory(category: string) {
32
- currentCategory.value = category;
33
- emit('category-changed', category);
34
- }
35
-
36
28
  // Dynamic component loader
37
29
  const componentMap = {
38
30
  datetime: defineAsyncComponent(() => import('./datetimePicker.vue')),
@@ -41,39 +33,82 @@ const componentMap = {
41
33
  // Refs for tabs and popup positioning
42
34
  const tabRefs = ref<Record<string, HTMLElement | null>>({});
43
35
  const popupStyle = ref({ left: '0px', top: '0px' });
44
-
45
36
  const searchContainer = ref<HTMLElement | null>(null);
46
37
 
47
- function handleOutsideClick(event: MouseEvent) {
38
+ // made this function compatible with mobile and with desktop
39
+ function componentValueUpdated(data: any, currenTab: number) {
40
+
41
+ if (activeTab.value?.name) {
42
+ c('update:search-data', {
43
+ tab: activeTab.name,
44
+ data: data
45
+ })
46
+ return;
47
+ }
48
+
49
+ let _currentTab = props.searchOptions.find((option: { category: any; }) => option.category === props.currentCategory).tabs[currenTab]
50
+ _currentTab.value = data
51
+ emit('update:search-data', {
52
+ tab: _currentTab.name,
53
+ data: _currentTab.value
54
+ })
55
+
56
+ }
57
+
58
+
59
+ function searchClicked() {
60
+ emit('search');
61
+ }
62
+
63
+ async function handleOutsideClick(event: MouseEvent) {
48
64
  if (searchContainer.value && !searchContainer.value.contains(event.target as Node)) {
65
+ await nextTick();
49
66
  activeTab.value = null;
50
67
  }
51
68
  }
52
69
 
70
+
71
+ async function openMobileView() {
72
+ if (typeof window !== 'undefined' && window.innerWidth <= 768) {
73
+ const modal = document.getElementById('advancedSearchMobile') as HTMLDialogElement | null;
74
+ if (modal) {
75
+ modal.showModal();
76
+ } else {
77
+ console.warn('Modal element not found.');
78
+ }
79
+ // return; // Early return to avoid running desktop logic
80
+ }
81
+ }
82
+
53
83
  async function openTab(tab: Tab) {
54
84
  activeTab.value = tab;
55
- await nextTick(); // Ensure DOM updates
85
+ await nextTick();
56
86
 
57
87
  const tabElement = tabRefs.value[tab.name];
58
88
  if (tabElement) {
59
89
  const rect = tabElement.getBoundingClientRect();
60
90
  const parentRect = searchContainer.value?.getBoundingClientRect() || { left: 0, top: 0 };
61
-
62
- // Center the popup
63
- popupStyle.value = {
64
- left: `${rect.left + (rect.width / 2) - (parentRect.left)}px`,
65
- top: `${rect.bottom - parentRect.top}px`,
66
- transform: `translateX(-50%)`, // Center horizontally
67
- };
91
+
92
+ // Calculate the popup position for centering on larger screens
93
+ if (window.innerWidth > 768) { // Desktop or tablet screen
94
+ popupStyle.value = {
95
+ left: `${rect.left + (rect.width / 2) - (parentRect.left)}px`,
96
+ top: `${rect.bottom - parentRect.top}px`,
97
+ transform: `translateX(-50%)`,
98
+ };
99
+ } else { // Mobile screen
100
+ popupStyle.value = {
101
+ left: '50%',
102
+ top: `${rect.bottom - parentRect.top}px`,
103
+ transform: `translateX(-50%)`, // Center the popup horizontally on mobile
104
+ };
105
+ }
68
106
  } else {
69
107
  console.error('Tab element not found for:', tab.name);
70
108
  }
71
109
  }
72
110
 
73
111
 
74
- function setDynamicData(data: any) {
75
- console.log(data);
76
- }
77
112
 
78
113
  onMounted(() => {
79
114
  document.addEventListener('click', handleOutsideClick);
@@ -85,42 +120,68 @@ onBeforeUnmount(() => {
85
120
  </script>
86
121
 
87
122
  <template>
88
- <div>
89
- <!-- Category Navigation -->
90
- <div class="flex gap-4 place-content-center w-full"
91
- :class="{ 'flex': props.searchOptions.length > 1, 'hidden': props.searchOptions.length <= 1 }">
92
- <button v-for="searchOption in props.searchOptions" :key="searchOption.category" type="button"
93
- :class="{ 'font-medium': searchOption.category === currentCategory }"
94
- @click="setCurrentCategory(searchOption.category)">
95
- {{ searchOption.category }}
96
- </button>
97
- </div>
98
-
99
- <!-- Tab Navigation and Content -->
100
- <div ref="searchContainer"
101
- class="relative my-8 rounded border-2 border-opacity-25 group/search flex shadow-md transition-all p-2 place-items-center">
123
+ <div ref="searchContainer" class="w-full">
124
+ <!-- desktop/tablet -->
125
+ <div
126
+ class="relative rounded border-2 border-opacity-25 group/search flex shadow-md transition-all p-2 place-items-center w-full cursor-pointer md:cursor-auto"
127
+ @click="openMobileView">
102
128
  <div class="tabs tabs-boxed bg-base-100 flex gap-4 w-full">
103
- <button v-for="tab in props.searchOptions.find(opt => opt.category === currentCategory)?.tabs || []"
129
+ <button v-for="tab in props.searchOptions.find(opt => opt.category === props.currentCategory)?.tabs || []"
104
130
  :key="tab.name" @click="openTab(tab)" :class="{ 'tab-active': activeTab?.name === tab.name }"
105
- class="group/searchitem text-start relative p-1 hover:bg-base-200 rounded"
131
+ class="group/searchitem pointer-events-none md:pointer-events-auto w-auto overflow-x-hidden text-start relative p-1 hover:bg-base-200 rounded after:h-10 after:bg-base-200 after:absolute after:-right-2 after:w-[1px] after:top-0 last-of-type:after:content-none last-of-type:flex-auto first-of-type:flex-auto after:content-none after:md:content-['']"
106
132
  :ref="(el) => tabRefs[tab.name] = el">
107
133
  <p class="text-sm">{{ tab.name }}</p>
108
- <p class="text-xs opacity-35">{{ tab.description }}</p>
134
+ <p class="text-xs opacity-35 truncate ... overflow-hidden md:block hidden">{{ tab.description }}</p>
109
135
  </button>
110
136
  </div>
111
137
 
112
- <!-- Dynamic Component Rendering -->
113
138
  <div class="tab-content absolute w-fit max-w-full flex mt-4 transition-all" v-if="activeTab" :style="popupStyle">
114
- <component :is="componentMap[activeTab.type]" @updated="setDynamicData" v-bind="activeTab.props ? activeTab.props : null" />
139
+ <component :is="componentMap[activeTab.type]" v-bind="activeTab.props ? activeTab.props : null"
140
+ @updated="componentValueUpdated" />
115
141
  </div>
116
- <!-- Search Button -->
117
- <button title="search" type="submit" class="btn btn-primary btn-square ml-1">
142
+
143
+ <button title="search" type="submit" class="btn btn-primary btn-square ml-1" @click="searchClicked">
144
+ <Search :size="24" color="white" />
145
+ </button>
146
+ </div>
147
+ <div class="hidden">
148
+ <button title="search" @click="">
118
149
  <Search :size="24" color="white" />
119
150
  </button>
120
151
  </div>
121
152
  </div>
153
+
154
+
155
+ <dialog id="advancedSearchMobile" class="modal">
156
+ <div class="modal-box">
157
+ <slot name="additionalForMobile">
158
+ <h3 class="text-lg font-bold">{{ currentCategory }}</h3>
159
+ </slot>
160
+
161
+ <div class="collapse bg-base-200 my-4"
162
+ v-for="(tab, index) in props.searchOptions.find(opt => opt.category === props.currentCategory)?.tabs || []"
163
+ :key="tab.name">
164
+ <input type="radio" name="my-accordion-1" :checked="index === 0" />
165
+ <div class="collapse-title text-xl font-medium flex justify-between pr-4 items-center w-full">{{ tab.name }} <p class="text-sm">{{ tab.description }}</p></div>
166
+ <div class="collapse-content flex place-content-center p-0">
167
+ <br>
168
+
169
+ <component :is="componentMap[tab.type]" v-bind="tab.props || {}"
170
+ @updated="(data: any) => componentValueUpdated(data, index)" />
171
+ </div>
172
+ </div>
173
+
174
+ <div class="modal-action">
175
+ <form method="dialog">
176
+ <!-- if there is a button in form, it will close the modal -->
177
+ <button class="btn btn-primary" @click="searchClicked">Close and Search</button>
178
+ </form>
179
+ </div>
180
+ </div>
181
+ </dialog>
182
+
122
183
  </template>
123
184
 
124
185
  <style scoped>
125
- .tabs button{cursor:pointer}.tab-content{position:absolute;transform-origin:top center;transition:opacity .3s ease;z-index:10}.tabs-boxed :is(.tab-active,[aria-selected=true]):not(.tab-disabled):not([disabled]),.tabs-boxed :is(input:checked){@apply bg-base-300 text-base-content}
186
+ .tabs button{cursor:pointer}.tab-content{position:absolute;transform-origin:top center;transition:opacity .3s ease;z-index:10}.tabs-boxed :is(.tab-active,[aria-selected=true]):not(.tab-disabled):not([disabled]),.tabs-boxed :is(input:checked){@apply bg-base-300 text-base-content}@media (max-width:768px){.tab-content{bottom:0;left:50%;top:auto;transform:translateX(-50%);width:100%}}@media (min-width:769px){.tab-content{left:unset;transform:unset;width:auto}}
126
187
  </style>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="relative inline-block" ref="pickerContainer">
2
+ <div class="relative inline-block w-full lg:w-[360px]" ref="pickerContainer">
3
3
  <div class="flex items-center gap-2 cursor-pointer" @click="togglePopup">
4
4
  <slot v-if="popup">
5
5
  <input
@@ -8,7 +8,7 @@
8
8
  :placeholder="placeholder"
9
9
  :value="formattedValue"
10
10
  readonly
11
- :id="inputId"
11
+ :id="inputId"
12
12
  />
13
13
  <button class="btn btn-ghost">
14
14
  📅
@@ -16,23 +16,23 @@
16
16
  </slot>
17
17
  </div>
18
18
 
19
- <div v-if="showPopup || !popup" class="z-50 bg-base-100 shadow-xl rounded-md p-4 mt-2 w-[360px]" :class="[{'absolute': popup}]" :id="popupId">
19
+ <div v-if="showPopup || !popup" class="z-50 rounded-md px-4 md:p-4 md:mt-2 w-full lg:bg-base-100" :class="[{ absolute: popup }]" :id="popupId">
20
20
  <div v-if="showDate" class="flex items-center justify-between mb-4 place-content-center">
21
- <button class="btn btn-sm btn-primary btn-outline" @click="previousMonth">
21
+ <button class="btn hidden md:block md:btn-sm btn-primary btn-outline" @click="previousMonth">
22
22
  <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
23
23
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
24
24
  </svg>
25
25
  </button>
26
26
 
27
- <select class="select border-none w-fit" v-model="currentMonth" @change="emitDateTimeChanged">
27
+ <select class="select border-none w-fit" v-model="currentMonth" @change="emitDateTimeChanged">
28
28
  <option v-for="(month, index) in months" :key="index" :value="month">{{ month }}</option>
29
29
  </select>
30
30
 
31
- <select class="select w-24 border-none" v-model="currentYear" @change="emitDateTimeChanged">
31
+ <select class="select w-24 border-none" v-model="currentYear" @change="emitDateTimeChanged">
32
32
  <option v-for="year in years" :key="year" :value="year">{{ year }}</option>
33
33
  </select>
34
34
 
35
- <button class="btn btn-sm btn-primary btn-outline" @click="nextMonth">
35
+ <button class="btn hidden md:block md:btn-sm btn-primary btn-outline" @click="nextMonth">
36
36
  <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
37
37
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
38
38
  </svg>
@@ -42,33 +42,27 @@
42
42
  <div v-if="showDate" class="grid grid-cols-7 gap-2">
43
43
  <div v-for="day in daysOfWeek" :key="day" class="text-center">{{ day }}</div>
44
44
  <div v-for="emptyDay in emptyDays" :key="emptyDay" class="text-center"></div>
45
- <div v-for="day in daysInMonth" :key="day" class="text-center cursor-pointer py-1 rounded-full hover:bg-primary/10"
46
- :class="{
47
- 'bg-primary text-primary-content': isDateSelected(day),
48
- 'today': isToday(day),
49
- 'range-start': isRangeStart(day),
50
- 'range-end': isRangeEnd(day),
51
- 'in-range': isInDateRange(day),
52
- }"
53
- @click="selectDate(day)">
45
+ <div v-for="day in daysInMonth" :key="day" class="text-center cursor-pointer py-1 rounded-full hover:bg-primary/10" :class="{
46
+ 'bg-primary text-primary-content': isDateSelected(day),
47
+ 'today': isToday(day),
48
+ 'range-start': isRangeStart(day),
49
+ 'range-end': isRangeEnd(day),
50
+ 'in-range': isInDateRange(day),
51
+ }" @click="selectDate(day)">
54
52
  {{ day.getDate() }}
55
53
  </div>
56
54
  </div>
57
55
 
58
- <div v-if="showTime" class="mt-4">
59
- <h3 class="text-lg font-bold mb-2">Select Time</h3>
56
+ <div v-if="showTime" class="mt-4 lg:w-80">
57
+ <h3 class="text-lg font-bold mb-2" v-if="!props.range">Select Time</h3>
60
58
  <div v-if="!props.range">
61
59
  <input v-if="isMobile" type="time" class="input input-bordered w-full" v-model="selectedTime" @change="emitDateTimeChanged" />
62
60
  <div v-else class="flex gap-4">
63
- <select class="select select-bordered w-full" v-model="selectedHour" @change="emitDateTimeChanged">
64
- <option v-for="hour in hours" :key="hour" :value="hour">{{ hour }}</option>
61
+ <select class="select select-bordered w-full" v-model.number="selectedHour" @change="emitDateTimeChanged">
62
+ <option v-for="hour in hours" :key="hour" :value="hour">{{ hour.toString().padStart(2, '0') }}</option>
65
63
  </select>
66
- <select class="select select-bordered w-full" v-model="selectedMinute" @change="emitDateTimeChanged">
67
- <option v-for="minute in minutes" :key="minute" :value="minute">{{ minute }}</option>
68
- </select>
69
- <select class="select select-bordered w-full" v-model="selectedPeriod" @change="emitDateTimeChanged">
70
- <option value="AM">AM</option>
71
- <option value="PM">PM</option>
64
+ <select class="select select-bordered w-full" v-model.number="selectedMinute" @change="emitDateTimeChanged">
65
+ <option v-for="minute in minutes" :key="minute" :value="minute">{{ minute.toString().padStart(2, '0') }}</option>
72
66
  </select>
73
67
  </div>
74
68
  </div>
@@ -77,15 +71,11 @@
77
71
  <p class="font-bold">Start Time</p>
78
72
  <input v-if="isMobile" type="time" class="input input-bordered w-full" v-model="selectedTime.start" @change="emitDateTimeChanged" />
79
73
  <div v-else class="flex gap-2">
80
- <select class="select select-bordered w-full" v-model="selectedHour.start" @change="emitDateTimeChanged">
81
- <option v-for="hour in hours" :key="hour" :value="hour">{{ hour }}</option>
82
- </select>
83
- <select class="select select-bordered w-full" v-model="selectedMinute.start" @change="emitDateTimeChanged">
84
- <option v-for="minute in minutes" :key="minute" :value="minute">{{ minute }}</option>
74
+ <select class="select select-bordered w-full" v-model.number="selectedHour.start" @change="emitDateTimeChanged">
75
+ <option v-for="hour in hours" :key="hour" :value="hour">{{ hour.toString().padStart(2, '0') }}</option>
85
76
  </select>
86
- <select class="select select-bordered w-full" v-model="selectedPeriod.start" @change="emitDateTimeChanged">
87
- <option value="AM">AM</option>
88
- <option value="PM">PM</option>
77
+ <select class="select select-bordered w-full" v-model.number="selectedMinute.start" @change="emitDateTimeChanged">
78
+ <option v-for="minute in minutes" :key="minute" :value="minute">{{ minute.toString().padStart(2, '0') }}</option>
89
79
  </select>
90
80
  </div>
91
81
  </div>
@@ -93,15 +83,11 @@
93
83
  <p class="font-bold">End Time</p>
94
84
  <input v-if="isMobile" type="time" class="input input-bordered w-full" v-model="selectedTime.end" @change="emitDateTimeChanged" />
95
85
  <div v-else class="flex gap-2">
96
- <select class="select select-bordered w-full" v-model="selectedHour.end" @change="emitDateTimeChanged">
97
- <option v-for="hour in hours" :key="hour" :value="hour">{{ hour }}</option>
86
+ <select class="select select-bordered w-full" v-model.number="selectedHour.end" @change="emitDateTimeChanged">
87
+ <option v-for="hour in hours" :key="hour" :value="hour">{{ hour.toString().padStart(2, '0') }}</option>
98
88
  </select>
99
- <select class="select select-bordered w-full" v-model="selectedMinute.end" @change="emitDateTimeChanged">
100
- <option v-for="minute in minutes" :key="minute" :value="minute">{{ minute }}</option>
101
- </select>
102
- <select class="select select-bordered w-full" v-model="selectedPeriod.end" @change="emitDateTimeChanged">
103
- <option value="AM">AM</option>
104
- <option value="PM">PM</option>
89
+ <select class="select select-bordered w-full" v-model.number="selectedMinute.end" @change="emitDateTimeChanged">
90
+ <option v-for="minute in minutes" :key="minute" :value="minute">{{ minute.toString().padStart(2, '0') }}</option>
105
91
  </select>
106
92
  </div>
107
93
  </div>
@@ -140,10 +126,25 @@ const props = defineProps({
140
126
  },
141
127
  id: {
142
128
  type: String,
143
- default: 'datetimepicker',
129
+ default: 'datetimepicker',
130
+ },
131
+ initialDate: {
132
+ type: Object,
133
+ default: () => null
144
134
  }
145
135
  })
146
136
 
137
+ // Watchers
138
+ watch(() => props.range, (newRange) => {
139
+ if (newRange) {
140
+ selectedDate.value = { start: null, end: null };
141
+ selectedTime.value = { start: null, end: null };
142
+ } else {
143
+ selectedDate.value = null;
144
+ selectedTime.value = null;
145
+ }
146
+ });
147
+
147
148
  // Emits
148
149
  const emit = defineEmits(['updated'])
149
150
 
@@ -153,9 +154,8 @@ const selectedDate = ref(props.range ? { start: null, end: null } : null)
153
154
  const selectedTime = ref(props.range ? { start: null, end: null } : null)
154
155
  const currentMonth = ref(new Date().toLocaleString('default', { month: 'long' }))
155
156
  const currentYear = ref(new Date().getFullYear())
156
- const selectedHour = ref(props.range ? { start: new Date().getHours() % 12 || 12, end: new Date().getHours() % 12 || 12 } : new Date().getHours() % 12 || 12)
157
- const selectedMinute = ref(props.range ? { start: new Date().getMinutes().toString().padStart(2, '0'), end: new Date().getMinutes().toString().padStart(2, '0') } : new Date().getMinutes().toString().padStart(2, '0'))
158
- const selectedPeriod = ref(props.range ? { start: new Date().getHours() < 12 ? 'AM' : 'PM', end: new Date().getHours() < 12 ? 'AM' : 'PM' } : new Date().getHours() < 12 ? 'AM' : 'PM')
157
+ const selectedHour = ref(props.range ? { start: 0, end: 0 } : 0);
158
+ const selectedMinute = ref(props.range ? { start: 0, end: 0 } : 0);
159
159
 
160
160
  const pickerContainer = ref(null)
161
161
 
@@ -167,9 +167,7 @@ const popupId = computed(() => `${props.id}-popup`);
167
167
  const showDate = computed(() => props.mode === 'date' || props.mode === 'datetime')
168
168
  const showTime = computed(() => props.mode === 'time' || props.mode === 'datetime')
169
169
  const currentMonthYear = computed(() => new Date(currentYear.value, monthIndex.value))
170
- const monthIndex = computed(() => {
171
- return months.indexOf(currentMonth.value)
172
- })
170
+ const monthIndex = computed(() => months.indexOf(currentMonth.value))
173
171
  const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
174
172
  const firstDay = computed(() => new Date(currentYear.value, monthIndex.value).getDay())
175
173
  const daysInMonth = computed(() => {
@@ -177,9 +175,9 @@ const daysInMonth = computed(() => {
177
175
  return Array.from({ length: days }, (_, i) => new Date(currentYear.value, monthIndex.value, i + 1));
178
176
  });
179
177
  const emptyDays = computed(() => Array.from({ length: firstDay.value }, (_, i) => i))
180
- const hours = Array.from({ length: 12 }, (_, i) => (i + 1).toString().padStart(2, '0'))
181
- const minutes = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'))
182
- const isMobile = computed(() => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent))
178
+ const hours = Array.from({ length: 24 }, (_, i) => i)
179
+ const minutes = Array.from({ length: 60 }, (_, i) => i)
180
+ const isMobile = computed(() => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(process.client ? navigator.userAgent : 'nope'))
183
181
  const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
184
182
  const years = computed(() => {
185
183
  const currentYear = new Date().getFullYear()
@@ -190,52 +188,45 @@ const formattedDate = computed(() => {
190
188
  if (!showDate.value || !selectedDate.value) return '';
191
189
 
192
190
  if (props.range && selectedDate.value.start && selectedDate.value.end) {
193
- const startDate = selectedDate.value.start.toLocaleDateString('en-GB');
194
- const endDate = selectedDate.value.end.toLocaleDateString('en-GB');
191
+ const startDate = selectedDate.value.start.toLocaleDateString('en-US');
192
+ const endDate = selectedDate.value.end.toLocaleDateString('en-US');
195
193
  return `${startDate} - ${endDate}`;
196
- }
194
+ }
197
195
 
198
196
  if (!props.range && selectedDate.value) {
199
- return selectedDate.value.toLocaleDateString('en-GB');
197
+ return selectedDate.value.toLocaleDateString('en-US');
200
198
  }
201
199
 
202
200
  return '';
203
201
  });
204
202
 
203
+ const ensureDate = (d) => d instanceof Date ? d : new Date(d);
204
+ const isValidDate = (d) => d instanceof Date && !isNaN(d);
205
+
205
206
  const formattedTime = computed(() => {
206
207
  if (!showTime.value) return '';
207
208
 
208
- if (props.range && selectedTime.value.start && selectedTime.value.end) {
209
- const formattedStartTime = isMobile.value
210
- ? selectedTime.value.start
211
- : `${selectedHour.value.start}:${selectedMinute.value.start} ${selectedPeriod.value.start}`;
212
- const formattedEndTime = isMobile.value
213
- ? selectedTime.value.end
214
- : `<span class="math-inline">\{selectedHour\.value\.end\}\:</span>{selectedMinute.value.end} ${selectedPeriod.value.end}`;
215
- return `${formattedStartTime} - ${formattedEndTime}`;
216
- }
217
-
218
- if (!props.range && selectedTime.value) {
209
+ const formatTime = (time) => {
210
+ const hour = selectedHour.value?.[time] ?? selectedHour.value;
211
+ const minute = selectedMinute.value?.[time] ?? selectedMinute.value;
219
212
  return isMobile.value
220
- ? selectedTime.value
221
- : `<span class="math-inline">\{selectedHour\.value\}\:</span>{selectedMinute.value} ${selectedPeriod.value}`;
213
+ ? selectedTime.value?.[time] ?? selectedTime.value
214
+ : `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
215
+ };
216
+
217
+ if (props.range) {
218
+ return `${formatTime('start')} - ${formatTime('end')}`;
222
219
  }
223
220
 
224
- return '';
221
+ return formatTime();
225
222
  });
226
223
 
227
224
  const formattedValue = computed(() => {
228
225
  const date = formattedDate.value;
229
226
  const time = formattedTime.value;
230
-
231
- if (date && time) return `${date} ${time}`;
232
- if (date) return date;
233
- if (time) return time;
234
-
235
- return '';
227
+ return `${date} ${time}`.trim();
236
228
  });
237
229
 
238
-
239
230
  // Methods
240
231
  const togglePopup = () => (showPopup.value = !showPopup.value)
241
232
 
@@ -247,10 +238,10 @@ const closeAndEmit = () => {
247
238
  const closePopup = () => (showPopup.value = false)
248
239
 
249
240
  const clearSelection = () => {
250
- selectedDate.value = props.range ? { start: null, end: null } : null
251
- selectedTime.value = props.range ? { start: null, end: null } : null
252
- emitDateTimeChanged()
253
- }
241
+ selectedDate.value = props.range ? { start: null, end: null } : null;
242
+ selectedTime.value = props.range ? { start: null, end: null } : null;
243
+ emitDateTimeChanged();
244
+ };
254
245
 
255
246
  const selectDate = (date) => {
256
247
  if (props.range) {
@@ -264,42 +255,46 @@ const selectDate = (date) => {
264
255
  } else {
265
256
  selectedDate.value = date
266
257
  }
267
- emitDateTimeChanged()
258
+ emitDateTimeChanged()
268
259
  }
269
260
 
270
261
  const isDateSelected = (date) => {
262
+ date = ensureDate(date);
271
263
  if (props.range) {
272
- return (selectedDate.value.start && date.getTime() === selectedDate.value.start.getTime()) || (selectedDate.value.end && date.getTime() === selectedDate.value.end.getTime())
273
- } else {
274
- return selectedDate.value && date.getTime() === selectedDate.value.getTime()
275
- }
276
- }
264
+ return (selectedDate.value.start && isValidDate(selectedDate.value.start) && date.getTime() === selectedDate.value.start.getTime()) ||
265
+ (selectedDate.value.end && isValidDate(selectedDate.value.end) && date.getTime() === selectedDate.value.end.getTime());
266
+ }
267
+ return isValidDate(selectedDate.value) && date.getTime() === selectedDate.value.getTime();
268
+ };
277
269
 
278
270
  const isToday = (date) => {
279
- const today = new Date()
271
+ date = ensureDate(date);
272
+ const today = new Date();
280
273
  return date.getDate() === today.getDate() &&
281
274
  date.getMonth() === today.getMonth() &&
282
- date.getFullYear() === today.getFullYear()
283
- }
275
+ date.getFullYear() === today.getFullYear();
276
+ };
284
277
 
285
278
  const isRangeStart = (date) => {
286
- return props.range && selectedDate.value.start && date.getTime() === selectedDate.value.start.getTime()
287
- }
279
+ date = ensureDate(date);
280
+ return props.range && isValidDate(selectedDate.value.start) && date.getTime() === selectedDate.value.start.getTime();
281
+ };
288
282
 
289
283
  const isRangeEnd = (date) => {
290
- return props.range && selectedDate.value.end && date.getTime() === selectedDate.value.end.getTime()
291
- }
284
+ date = ensureDate(date);
285
+ return props.range && isValidDate(selectedDate.value.end) && date.getTime() === selectedDate.value.end.getTime();
286
+ };
292
287
 
293
288
  const isInDateRange = (date) => {
294
- if (props.range && selectedDate.value.start && selectedDate.value.end) {
295
- const start = selectedDate.value.start.getTime()
296
- const end = selectedDate.value.end.getTime()
297
- const current = date.getTime()
298
- return current > start && current < end
289
+ date = ensureDate(date);
290
+ if (props.range && isValidDate(selectedDate.value.start) && isValidDate(selectedDate.value.end)) {
291
+ const start = selectedDate.value.start.getTime();
292
+ const end = selectedDate.value.end.getTime();
293
+ const current = date.getTime();
294
+ return current > start && current < end;
299
295
  }
300
- return false
301
- }
302
-
296
+ return false;
297
+ };
303
298
 
304
299
  // Month navigation
305
300
  const previousMonth = () => {
@@ -309,6 +304,7 @@ const previousMonth = () => {
309
304
  currentYear.value--
310
305
  }
311
306
  currentMonth.value = months[newMonth]
307
+ emitDateTimeChanged()
312
308
  }
313
309
 
314
310
  const nextMonth = () => {
@@ -318,6 +314,7 @@ const nextMonth = () => {
318
314
  currentYear.value++
319
315
  }
320
316
  currentMonth.value = months[newMonth]
317
+ emitDateTimeChanged()
321
318
  }
322
319
 
323
320
  // Close on outside click
@@ -328,44 +325,64 @@ const handleClickOutside = (event) => {
328
325
  }
329
326
 
330
327
  const emitDateTimeChanged = () => {
331
- let dateStr = '';
332
- let timeStr = '';
333
-
334
- // Handle date formatting
335
- if (selectedDate.value) {
336
- if (props.range) {
337
- const start = selectedDate.value.start?.toLocaleDateString('en-GB') || '';
338
- const end = selectedDate.value.end?.toLocaleDateString('en-GB') || '';
339
- dateStr = `${start} - ${end}`;
340
- } else {
341
- dateStr = selectedDate.value.toLocaleDateString('en-GB');
328
+ let result = {};
329
+
330
+ const formatUTCDate = (date, hour, minute) => {
331
+ if (!(date instanceof Date && !isNaN(date))) {
332
+ // If no date is provided, use today's date
333
+ date = new Date();
342
334
  }
335
+ const newDate = new Date(date);
336
+ newDate.setUTCHours(hour, minute, 0, 0);
337
+ return newDate.toISOString();
338
+ };
339
+
340
+ if (props.range) {
341
+ result.from = formatUTCDate(
342
+ selectedDate.value.start,
343
+ selectedHour.value.start,
344
+ selectedMinute.value.start
345
+ );
346
+ result.to = formatUTCDate(
347
+ selectedDate.value.end,
348
+ selectedHour.value.end,
349
+ selectedMinute.value.end
350
+ );
351
+ } else {
352
+ result = formatUTCDate(
353
+ selectedDate.value,
354
+ selectedHour.value,
355
+ selectedMinute.value
356
+ );
343
357
  }
344
358
 
345
- // Handle time formatting
346
- if (showTime.value) {
347
- if (props.range) {
348
- const start = selectedTime.value.start || `${selectedHour.value.start}:${selectedMinute.value.start} ${selectedPeriod.value.start}`;
349
- const end = selectedTime.value.end || `${selectedHour.value.end}:${selectedMinute.value.end} ${selectedPeriod.value.end}`;
350
- timeStr = `${start} - ${end}`;
351
- } else {
352
- timeStr = `${selectedHour.value}:${selectedMinute.value} ${selectedPeriod.value}`;
359
+ emit('updated', result);
360
+ };
361
+
362
+ onMounted(() => {
363
+ // Initialize selectedDate from props.initialDate
364
+ if (props.initialDate) {
365
+ if (props.range && props.initialDate.start && props.initialDate.end) {
366
+ selectedDate.value = {
367
+ start: new Date(props.initialDate.start),
368
+ end: new Date(props.initialDate.end)
369
+ };
370
+ } else if (props.initialDate.start){
371
+ selectedDate.value = { start: new Date(props.initialDate.start) };
353
372
  }
354
373
  }
355
374
 
356
- // Combine date and time based on mode
357
- let result = '';
358
- if (props.mode === 'datetime') {
359
- result = props.range ? `${dateStr} ${timeStr}` : `${dateStr} ${timeStr}`;
360
- } else if (props.mode === 'date') {
361
- result = dateStr;
362
- } else if (props.mode === 'time') {
363
- result = timeStr;
375
+ // Initialize selectedHour and selectedMinute if they are null in range mode
376
+ if (props.range) {
377
+ selectedHour.value.start = selectedHour.value.start ?? new Date().getHours();
378
+ selectedHour.value.end = selectedHour.value.end ?? new Date().getHours();
379
+ selectedMinute.value.start = selectedMinute.value.start ?? new Date().getMinutes();
380
+ selectedMinute.value.end = selectedMinute.value.end ?? new Date().getMinutes();
381
+ } else {
382
+ selectedHour.value = selectedHour.value ?? new Date().getHours();
383
+ selectedMinute.value = selectedMinute.value ?? new Date().getMinutes();
364
384
  }
365
-
366
- // Emit the formatted string
367
- emit('updated', result.trim());
368
- };
385
+ });
369
386
 
370
387
  if(props.popup) {
371
388
  onMounted(() => document.addEventListener('click', handleClickOutside))
@@ -375,4 +392,4 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
375
392
 
376
393
  <style scoped>
377
394
  .input{cursor:pointer}.in-range{@apply bg-primary/20}.in-range:hover{@apply bg-primary/10}.range-end,.range-start{@apply bg-primary text-primary-content}
378
- </style>
395
+ </style>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import { defineProps, defineEmits } from 'vue';
3
+
4
+ interface Category {
5
+ category: string;
6
+ tabs: Tab[];
7
+ }
8
+
9
+ interface Props {
10
+ searchOptions: Array<Category>;
11
+ currentCategory: string;
12
+ }
13
+
14
+ const props = defineProps<Props>();
15
+ const emit = defineEmits<{ (e: 'update:currentCategory', category: string): void }>();
16
+
17
+ function changeCategory(category: string) {
18
+ emit('update:currentCategory', category);
19
+ }
20
+ </script>
21
+
22
+ <template>
23
+ <div class="categories flex flex-row gap-4 place-content-center">
24
+ <div v-for="category in props.searchOptions" :key="category.category">
25
+ <button @click="changeCategory(category.category)" class="truncate" :class="{ 'font-semibold': props.currentCategory === category.category }">
26
+ {{ category.category }}
27
+ </button>
28
+ </div>
29
+ </div>
30
+ </template>
@@ -1,19 +1,26 @@
1
1
  <script lang="ts" setup></script>
2
2
 
3
3
  <template>
4
- <nav class="navbar border-b-4 border-primary px-5 py-5 place-items-start">
5
- <div class="navbar-start flex-1">
4
+ <nav class="navbar border-b-4 border-primary px-5 py-5 place-items-start transition-all">
5
+ <div class="navbar-start hidden md:flex">
6
6
  <slot name="start"></slot>
7
7
  </div>
8
-
9
- <div class="navbar-center flex-auto hidden md:block">
8
+
9
+ <div class="navbar-center hidden md:flex justify-center items-center">
10
10
  <slot name="center"></slot>
11
11
  </div>
12
12
 
13
- <div class="navbar-end flex-1">
13
+ <div class="navbar-end md:flex hidden">
14
14
  <slot name="end"></slot>
15
15
  </div>
16
+ <div class="navbar-bottom md:flex hidden">
17
+ <slot name="bottom"></slot>
18
+ </div>
16
19
  </nav>
17
20
  </template>
18
21
 
19
22
 
23
+ <style scoped>
24
+ .navbar{display:grid;gap:0;grid-template-areas:"center center center" "bottom bottom bottom";grid-template-columns:auto auto auto;grid-template-rows:auto auto;width:100%}@media (min-width:768px){.navbar{grid-row-gap:4rem}}.navbar-start{grid-area:start;height:100%}.navbar-center{grid-area:center;height:100%;width:100%}.navbar-end{grid-area:end;height:100%;width:100%}.navbar-bottom{align-items:center;display:flex;grid-area:center;justify-content:center;width:100%}@media (min-width:768px){.navbar-bottom{grid-area:bottom}.navbar{grid-template-areas:"start center end" "bottom bottom bottom";grid-template-columns:.7fr 1.6fr .7fr}}
25
+ </style>
26
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "manolis-ui",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "My new Nuxt module",
5
5
  "repository": "manolis-trading/manolis-ui",
6
6
  "license": "MIT",