@apptegy/nuxt-navigation 0.1.104 → 0.1.106

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
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=3.0.0"
5
5
  },
6
6
  "configKey": "@apptegy/nuxt-navigation",
7
- "version": "0.1.104",
7
+ "version": "0.1.106",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.1",
10
10
  "unbuild": "unknown"
@@ -12,18 +12,19 @@
12
12
  <div class="user-control-actions">
13
13
  <div
14
14
  v-if="showNotifications"
15
- ref="notificationsRef"
16
- v-on-click-outside="onNotificationsClickOutside"
17
15
  class="header__notifications"
18
16
  >
19
- <button
17
+ <Button
20
18
  type="button"
19
+ severity="secondary"
20
+ text
21
+ rounded
21
22
  class="notification-bell"
22
23
  :class="{ 'notification-bell--active': panelOpen }"
23
24
  :aria-label="$t('navigation.notifications')"
24
25
  :aria-expanded="panelOpen"
25
26
  aria-haspopup="dialog"
26
- @click="toggleNotificationsPanel"
27
+ @click="onBellClick"
27
28
  >
28
29
  <svg
29
30
  class="notification-bell__icon"
@@ -50,12 +51,41 @@
50
51
  >
51
52
  {{ formattedNotificationCount }}
52
53
  </span>
53
- </button>
54
+ </Button>
54
55
 
55
56
  <slot name="notifications">
56
- <Teleport to="body" :disabled="!isMobilePanel">
57
+ <Popover
58
+ v-if="!isMobilePanel"
59
+ ref="popoverRef"
60
+ append-to="body"
61
+ class="notifications-popover"
62
+ :pt="popoverPt"
63
+ @show="onPanelShow"
64
+ @hide="onPanelHide"
65
+ >
66
+ <NavNotificationsPanel
67
+ v-if="notificationsPanel"
68
+ embedded
69
+ :data="notificationsPanel"
70
+ :loading="notificationsLoading"
71
+ :active-tab-id="activeTabId"
72
+ :active-filter-id="activeFilterId"
73
+ @update:active-tab-id="handleTabChange"
74
+ @update:active-filter-id="handleFilterChange"
75
+ @clear-all="(payload) => $emit('notifications-clear-all', payload)"
76
+ @dismiss="(payload) => $emit('notifications-dismiss', payload)"
77
+ @select="(payload) => $emit('notifications-select', payload)"
78
+ />
79
+ </Popover>
80
+
81
+ <Teleport
82
+ v-else
83
+ to="body"
84
+ >
57
85
  <NavNotificationsPanel
58
86
  v-if="notificationsPanel && panelOpen"
87
+ embedded
88
+ mobile
59
89
  :data="notificationsPanel"
60
90
  :loading="notificationsLoading"
61
91
  :active-tab-id="activeTabId"
@@ -108,9 +138,10 @@
108
138
 
109
139
  <script setup>
110
140
  import { computed, nextTick, ref, watch } from "vue";
111
- import { vOnClickOutside } from "@vueuse/components";
112
141
  import { useMediaQuery } from "@vueuse/core";
113
142
  import { useNuxtApp } from "#app";
143
+ import Button from "primevue/button";
144
+ import Popover from "primevue/popover";
114
145
  import {
115
146
  getNotificationsTotalCount,
116
147
  mapNotificationsApiResponse
@@ -140,10 +171,16 @@ const emit = defineEmits([
140
171
  "notifications-select",
141
172
  "notification-click"
142
173
  ]);
143
- const notificationsRef = ref();
174
+ const popoverRef = ref(null);
144
175
  const isMobilePanel = useMediaQuery("(max-width: 960px)");
145
176
  const panelOpen = ref(props.notificationPanelOpen);
146
177
  const isBellShaking = ref(false);
178
+ const popoverPt = {
179
+ content: {
180
+ class: "notifications-popover__content",
181
+ style: "padding: 0; border: none; box-shadow: none; background: transparent;"
182
+ }
183
+ };
147
184
  const notificationDateLabels = computed(() => ({
148
185
  today: nuxtApp.$t("navigation.notificationsPanel.dateGroups.today"),
149
186
  yesterday: nuxtApp.$t("navigation.notificationsPanel.dateGroups.yesterday"),
@@ -216,17 +253,21 @@ const formattedNotificationCount = computed(() => {
216
253
  }
217
254
  return notificationCount.value;
218
255
  });
219
- function toggleNotificationsPanel() {
220
- panelOpen.value = !panelOpen.value;
256
+ function onBellClick(event) {
257
+ if (isMobilePanel.value) {
258
+ panelOpen.value = !panelOpen.value;
259
+ } else {
260
+ popoverRef.value?.toggle(event);
261
+ }
221
262
  emit("notification-click", panelOpen.value);
222
263
  }
223
- function closeNotificationsPanel() {
264
+ function onPanelShow() {
265
+ panelOpen.value = true;
266
+ }
267
+ function onPanelHide() {
224
268
  panelOpen.value = false;
225
269
  }
226
- function onNotificationsClickOutside() {
227
- if (isMobilePanel.value || !panelOpen.value) {
228
- return;
229
- }
270
+ function closeNotificationsPanel() {
230
271
  panelOpen.value = false;
231
272
  }
232
273
  function handleTabChange(id) {
@@ -263,32 +304,16 @@ header.header .header__notifications {
263
304
  display: flex;
264
305
  align-items: center;
265
306
  }
266
- header.header .notification-bell {
307
+ header.header :deep(.notification-bell) {
267
308
  position: relative;
268
- display: flex;
269
- align-items: center;
270
- justify-content: center;
271
309
  width: 32px;
272
310
  height: 32px;
273
311
  padding: 0;
274
- border: none;
275
- border-radius: 8px;
276
- background: transparent;
277
- cursor: pointer;
278
- transition: background-color 0.15s ease;
312
+ overflow: visible;
279
313
  }
280
- header.header .notification-bell:hover, header.header .notification-bell--active {
314
+ header.header :deep(.notification-bell).notification-bell--active {
281
315
  background: var(--grey-10, #f3f4f6);
282
316
  }
283
- header.header .notification-bell__icon {
284
- display: block;
285
- flex-shrink: 0;
286
- color: var(--grey-80, #374151);
287
- transform-origin: 50% 20%;
288
- }
289
- header.header .notification-bell__icon--shaking {
290
- animation: bellShake 600ms cubic-bezier(0.36, 0.07, 0.19, 0.97);
291
- }
292
317
  header.header .notification-bell__badge {
293
318
  position: absolute;
294
319
  top: 2px;
@@ -305,6 +330,16 @@ header.header .notification-bell__badge {
305
330
  line-height: 12px;
306
331
  text-align: center;
307
332
  box-sizing: border-box;
333
+ pointer-events: none;
334
+ }
335
+ header.header .notification-bell__icon {
336
+ display: block;
337
+ flex-shrink: 0;
338
+ color: var(--grey-80, #374151);
339
+ transform-origin: 50% 20%;
340
+ }
341
+ header.header .notification-bell__icon--shaking {
342
+ animation: bellShake 600ms cubic-bezier(0.36, 0.07, 0.19, 0.97);
308
343
  }
309
344
  header.header .logout-impersonate-user {
310
345
  width: 32px;
@@ -359,3 +394,15 @@ header.header.sticky {
359
394
  }
360
395
  }
361
396
  </style>
397
+
398
+ <style>
399
+ .p-popover.notifications-popover::before, .p-popover.notifications-popover::after {
400
+ display: none;
401
+ }
402
+
403
+ .notifications-popover__content {
404
+ width: min(480px, 100vw - 32px);
405
+ max-height: calc(100vh - 130px);
406
+ overflow: hidden;
407
+ }
408
+ </style>
@@ -19,7 +19,7 @@ interface IUserDropdown {
19
19
  userDropdownOptions: Array<unknown>;
20
20
  }
21
21
  type __VLS_Props = IHeaderProps & IUserDropdown;
22
- declare var __VLS_1: {}, __VLS_3: {}, __VLS_5: {}, __VLS_7: {};
22
+ declare var __VLS_1: {}, __VLS_3: {}, __VLS_5: {}, __VLS_15: {};
23
23
  type __VLS_Slots = {} & {
24
24
  breadcrumbs?: (props: typeof __VLS_1) => any;
25
25
  } & {
@@ -27,9 +27,29 @@ type __VLS_Slots = {} & {
27
27
  } & {
28
28
  'user-controls'?: (props: typeof __VLS_5) => any;
29
29
  } & {
30
- notifications?: (props: typeof __VLS_7) => any;
30
+ notifications?: (props: typeof __VLS_15) => any;
31
31
  };
32
- declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, any, string, import("vue").PublicProps, any, {
32
+ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
33
+ "selected-option": (...args: any[]) => void;
34
+ "user-logout": (...args: any[]) => void;
35
+ "update:notificationPanelOpen": (...args: any[]) => void;
36
+ "update:activeTabId": (...args: any[]) => void;
37
+ "update:activeFilterId": (...args: any[]) => void;
38
+ "notifications-clear-all": (...args: any[]) => void;
39
+ "notifications-dismiss": (...args: any[]) => void;
40
+ "notifications-select": (...args: any[]) => void;
41
+ "notification-click": (...args: any[]) => void;
42
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
43
+ "onSelected-option"?: ((...args: any[]) => any) | undefined;
44
+ "onUser-logout"?: ((...args: any[]) => any) | undefined;
45
+ "onUpdate:notificationPanelOpen"?: ((...args: any[]) => any) | undefined;
46
+ "onUpdate:activeTabId"?: ((...args: any[]) => any) | undefined;
47
+ "onUpdate:activeFilterId"?: ((...args: any[]) => any) | undefined;
48
+ "onNotifications-clear-all"?: ((...args: any[]) => any) | undefined;
49
+ "onNotifications-dismiss"?: ((...args: any[]) => any) | undefined;
50
+ "onNotifications-select"?: ((...args: any[]) => any) | undefined;
51
+ "onNotification-click"?: ((...args: any[]) => any) | undefined;
52
+ }>, {
33
53
  showNotifications: boolean;
34
54
  notificationsResponse: INotificationsApiResponse | null;
35
55
  notificationsLoading: boolean;
@@ -10,55 +10,55 @@
10
10
  @click="handleClick"
11
11
  @keydown.enter.prevent="handleClick"
12
12
  >
13
- <div
14
- class="notification-item__avatar"
15
- :style="{ '--type-accent': typeAccent }"
16
- >
17
- <div class="notification-item__avatar-ring">
18
- <NavAvatar
19
- :name="primaryActor.name"
20
- :src="primaryActor.avatarUrl"
21
- class="notification-item__avatar-image"
22
- style="--size: 40px"
23
- />
24
- </div>
25
- <span
26
- v-if="groupBadgeLabel"
27
- class="notification-item__group-badge"
28
- aria-hidden="true"
13
+ <div
14
+ class="notification-item__avatar"
15
+ :style="{ '--type-accent': typeAccent }"
29
16
  >
30
- {{ groupBadgeLabel }}
31
- </span>
32
- <span
33
- class="notification-item__type-badge"
34
- aria-hidden="true"
35
- >
36
- <component :is="typeIcon" />
37
- </span>
38
- </div>
17
+ <div class="notification-item__avatar-ring">
18
+ <NavAvatar
19
+ :name="primaryActor.name"
20
+ :src="primaryActor.avatarUrl"
21
+ class="notification-item__avatar-image"
22
+ style="--size: 40px"
23
+ />
24
+ </div>
25
+ <span
26
+ v-if="groupBadgeLabel"
27
+ class="notification-item__group-badge"
28
+ aria-hidden="true"
29
+ >
30
+ {{ groupBadgeLabel }}
31
+ </span>
32
+ <span
33
+ class="notification-item__type-badge"
34
+ aria-hidden="true"
35
+ >
36
+ <component :is="typeIcon" />
37
+ </span>
38
+ </div>
39
39
 
40
- <div class="notification-item__content">
41
- <p class="notification-item__message">
42
- <strong>{{ primaryActor.name }}</strong>
43
- <template v-if="additionalActorsLabel">
44
- {{ additionalActorsLabel }}
45
- </template>
46
- {{ item.actionText }}
47
- </p>
48
- <p class="notification-item__meta">
49
- <span>{{ item.timestampLabel }}</span>
50
- <template v-if="item.contextLabel">
51
- <span class="notification-item__meta-separator" aria-hidden="true">·</span>
52
- <span>{{ item.contextLabel }}</span>
53
- </template>
54
- </p>
55
- <p
56
- v-if="item.stateLabel"
57
- class="notification-item__state-chip"
58
- >
59
- {{ item.stateLabel }}
60
- </p>
61
- </div>
40
+ <div class="notification-item__content">
41
+ <p class="notification-item__message">
42
+ <strong>{{ primaryActor.name }}</strong>
43
+ <template v-if="additionalActorsLabel">
44
+ {{ additionalActorsLabel }}
45
+ </template>
46
+ {{ item.actionText }}
47
+ </p>
48
+ <p class="notification-item__meta">
49
+ <span>{{ item.timestampLabel }}</span>
50
+ <template v-if="item.contextLabel">
51
+ <span class="notification-item__meta-separator" aria-hidden="true">·</span>
52
+ <span>{{ item.contextLabel }}</span>
53
+ </template>
54
+ </p>
55
+ <Tag
56
+ v-if="item.stateLabel"
57
+ :value="item.stateLabel"
58
+ severity="secondary"
59
+ class="notification-item__state-chip"
60
+ />
61
+ </div>
62
62
  </div>
63
63
 
64
64
  <div class="notification-item__actions">
@@ -67,9 +67,12 @@
67
67
  class="notification-item__unread-dot"
68
68
  aria-hidden="true"
69
69
  />
70
- <button
70
+ <Button
71
71
  v-if="showBadgeActions"
72
72
  type="button"
73
+ severity="secondary"
74
+ text
75
+ rounded
73
76
  class="notification-item__dismiss"
74
77
  :aria-label="$t('navigation.notificationsPanel.dismiss')"
75
78
  @click="handleDismiss"
@@ -89,13 +92,15 @@
89
92
  <path d="M18 6 6 18" />
90
93
  <path d="m6 6 12 12" />
91
94
  </svg>
92
- </button>
95
+ </Button>
93
96
  </div>
94
97
  </div>
95
98
  </template>
96
99
 
97
100
  <script setup>
98
101
  import { computed, h } from "vue";
102
+ import Button from "primevue/button";
103
+ import Tag from "primevue/tag";
99
104
  import { getTypeAccent, isInBadge } from "../../utils/notifications";
100
105
  const props = defineProps({
101
106
  item: { type: Object, required: true }
@@ -312,15 +317,7 @@ function handleDismiss(event) {
312
317
  padding: 0 2px;
313
318
  }
314
319
  .notification-item__state-chip {
315
- display: inline-flex;
316
- margin: 8px 0 0;
317
- padding: 4px 8px;
318
- border-radius: 4px;
319
- background: var(--grey-10, #f3f3f3);
320
- color: var(--grey-60, #515151);
321
- font-size: 12px;
322
- font-weight: 600;
323
- line-height: 16px;
320
+ margin-top: 8px;
324
321
  }
325
322
  .notification-item__actions {
326
323
  display: flex;
@@ -336,20 +333,9 @@ function handleDismiss(event) {
336
333
  flex-shrink: 0;
337
334
  }
338
335
  .notification-item__dismiss {
339
- display: flex;
340
- align-items: center;
341
- justify-content: center;
342
336
  width: 24px;
343
337
  height: 24px;
344
338
  padding: 0;
345
- border: none;
346
- border-radius: 4px;
347
- background: transparent;
348
339
  color: var(--grey-50, #9ca3af);
349
- cursor: pointer;
350
- }
351
- .notification-item__dismiss:hover {
352
- background: var(--grey-10, #f3f4f6);
353
- color: var(--grey-80, #374151);
354
340
  }
355
341
  </style>
@@ -3,10 +3,10 @@ type __VLS_Props = {
3
3
  item: INotificationItem;
4
4
  };
5
5
  declare const _default: import("vue").DefineComponent<__VLS_Props, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
- dismiss: (id: string) => any;
7
6
  select: (item: INotificationItem) => any;
7
+ dismiss: (id: string) => any;
8
8
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
9
- onDismiss?: ((id: string) => any) | undefined;
10
9
  onSelect?: ((item: INotificationItem) => any) | undefined;
10
+ onDismiss?: ((id: string) => any) | undefined;
11
11
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
12
12
  export default _default;
@@ -1,24 +1,33 @@
1
1
  <template>
2
2
  <div
3
3
  class="notifications-panel"
4
- :class="{ 'notifications-panel--fullscreen': isMobilePanel }"
4
+ :class="{
5
+ 'notifications-panel--embedded': embedded,
6
+ 'notifications-panel--mobile': mobile
7
+ }"
5
8
  role="dialog"
6
9
  :aria-label="$t('navigation.notifications')"
7
10
  >
8
11
  <div class="notifications-panel__header">
9
12
  <div class="notifications-panel__title-row">
10
- <h2 class="notifications-panel__title">
13
+ <h2
14
+ v-if="!mobile"
15
+ class="notifications-panel__title"
16
+ >
11
17
  {{ $t("navigation.notifications") }}
12
18
  </h2>
13
19
  <span
14
- v-if="data.retentionLabel && !isMobilePanel"
20
+ v-if="data.retentionLabel && !mobile"
15
21
  class="notifications-panel__retention"
16
22
  >
17
23
  {{ data.retentionLabel }}
18
24
  </span>
19
- <button
20
- v-if="isMobilePanel"
25
+ <Button
26
+ v-if="mobile"
21
27
  type="button"
28
+ severity="secondary"
29
+ text
30
+ rounded
22
31
  class="notifications-panel__close"
23
32
  :aria-label="$t('navigation.notificationsPanel.close')"
24
33
  @click="$emit('close')"
@@ -38,34 +47,31 @@
38
47
  <path d="M18 6 6 18" />
39
48
  <path d="m6 6 12 12" />
40
49
  </svg>
41
- </button>
50
+ </Button>
42
51
  </div>
43
52
 
44
- <div
53
+ <Tabs
54
+ :value="activeTabId"
45
55
  class="notifications-panel__tabs"
46
- role="tablist"
47
- :aria-label="$t('navigation.notificationsPanel.tabsLabel')"
56
+ @update:value="selectTab"
48
57
  >
49
- <button
50
- v-for="tab in data.tabs"
51
- :key="tab.id"
52
- type="button"
53
- role="tab"
54
- class="notifications-panel__tab"
55
- :class="{ 'notifications-panel__tab--active': tab.id === activeTabId }"
56
- :aria-selected="tab.id === activeTabId"
57
- @click="selectTab(tab.id)"
58
- >
59
- <span>{{ tab.label }}</span>
60
- <span
61
- v-if="getTabBadgeCount(tab) > 0"
62
- class="notifications-panel__tab-badge"
63
- aria-hidden="true"
58
+ <TabList>
59
+ <Tab
60
+ v-for="tab in data.tabs"
61
+ :key="tab.id"
62
+ :value="tab.id"
63
+ class="notifications-panel__tab-item"
64
64
  >
65
- {{ formatCount(getTabBadgeCount(tab)) }}
66
- </span>
67
- </button>
68
- </div>
65
+ <span>{{ tab.label }}</span>
66
+ <Badge
67
+ v-if="getTabBadgeCount(tab) > 0"
68
+ :value="formatCount(getTabBadgeCount(tab))"
69
+ severity="danger"
70
+ class="notifications-panel__tab-badge"
71
+ />
72
+ </Tab>
73
+ </TabList>
74
+ </Tabs>
69
75
  </div>
70
76
 
71
77
  <div
@@ -76,20 +82,32 @@
76
82
  ref="filtersRef"
77
83
  class="notifications-panel__filters-scroll"
78
84
  >
79
- <button
85
+ <Button
80
86
  v-for="filter in visibleFilters"
81
87
  :key="filter.id"
82
88
  type="button"
89
+ size="small"
90
+ rounded
91
+ :severity="filter.id === activeFilterId ? 'primary' : 'secondary'"
92
+ :outlined="filter.id !== activeFilterId"
83
93
  class="notifications-panel__filter"
84
- :class="{ 'notifications-panel__filter--active': filter.id === activeFilterId }"
85
94
  @click="selectFilter(filter.id)"
86
95
  >
87
- <span>{{ filter.label }}</span>
88
- <span v-if="getFilterBadgeCount(filter) > 0">{{ formatCount(getFilterBadgeCount(filter)) }}</span>
89
- </button>
96
+ <span class="notifications-panel__filter-option">
97
+ <span>{{ filter.label }}</span>
98
+ <Badge
99
+ v-if="getFilterBadgeCount(filter) > 0"
100
+ :value="formatCount(getFilterBadgeCount(filter))"
101
+ severity="secondary"
102
+ />
103
+ </span>
104
+ </Button>
90
105
  </div>
91
- <button
106
+ <Button
92
107
  type="button"
108
+ severity="secondary"
109
+ outlined
110
+ rounded
93
111
  class="notifications-panel__filters-next"
94
112
  :aria-label="$t('navigation.notificationsPanel.scrollFilters')"
95
113
  @click="scrollFilters"
@@ -108,12 +126,20 @@
108
126
  >
109
127
  <path d="m9 18 6-6-6-6" />
110
128
  </svg>
111
- </button>
129
+ </Button>
112
130
  </div>
113
131
 
114
132
  <div class="notifications-panel__body">
115
- <div v-if="loading" class="notifications-panel__loading">
116
- {{ $t("navigation.notificationsPanel.loading") }}
133
+ <div
134
+ v-if="loading"
135
+ class="notifications-panel__loading"
136
+ >
137
+ <Skeleton
138
+ v-for="index in 3"
139
+ :key="index"
140
+ height="4.5rem"
141
+ class="notifications-panel__skeleton"
142
+ />
117
143
  </div>
118
144
 
119
145
  <template v-else>
@@ -126,14 +152,15 @@
126
152
  <h3 class="notifications-panel__group-label">
127
153
  {{ group.label }}
128
154
  </h3>
129
- <button
155
+ <Button
130
156
  v-if="showClearAll(groupIndex)"
131
157
  type="button"
158
+ link
159
+ size="small"
132
160
  class="notifications-panel__clear-all"
161
+ :label="$t('navigation.notificationsPanel.clearAll')"
133
162
  @click="$emit('clear-all', { groupId: group.id, tabId: activeTabId, filterId: activeFilterId })"
134
- >
135
- {{ $t("navigation.notificationsPanel.clearAll") }}
136
- </button>
163
+ />
137
164
  </div>
138
165
 
139
166
  <div class="notifications-panel__list">
@@ -147,12 +174,14 @@
147
174
  </div>
148
175
  </section>
149
176
 
150
- <p
177
+ <Message
151
178
  v-if="isEmpty"
179
+ severity="secondary"
180
+ variant="simple"
152
181
  class="notifications-panel__empty"
153
182
  >
154
183
  {{ $t("navigation.notificationsPanel.empty") }}
155
- </p>
184
+ </Message>
156
185
  </template>
157
186
  </div>
158
187
  </div>
@@ -160,7 +189,13 @@
160
189
 
161
190
  <script setup>
162
191
  import { computed, ref, watch } from "vue";
163
- import { useMediaQuery } from "@vueuse/core";
192
+ import Badge from "primevue/badge";
193
+ import Button from "primevue/button";
194
+ import Message from "primevue/message";
195
+ import Skeleton from "primevue/skeleton";
196
+ import Tab from "primevue/tab";
197
+ import TabList from "primevue/tablist";
198
+ import Tabs from "primevue/tabs";
164
199
  import {
165
200
  flattenNotifications,
166
201
  formatNotificationCount,
@@ -176,10 +211,11 @@ const props = defineProps({
176
211
  data: { type: Object, required: true },
177
212
  loading: { type: Boolean, required: false, default: false },
178
213
  activeTabId: { type: String, required: false },
179
- activeFilterId: { type: String, required: false }
214
+ activeFilterId: { type: String, required: false },
215
+ embedded: { type: Boolean, required: false, default: false },
216
+ mobile: { type: Boolean, required: false, default: false }
180
217
  });
181
218
  const emit = defineEmits(["update:activeTabId", "update:activeFilterId", "clear-all", "dismiss", "select", "close"]);
182
- const isMobilePanel = useMediaQuery("(max-width: 960px)");
183
219
  const filtersRef = ref();
184
220
  const activeTabId = ref(props.activeTabId || props.data.tabs[0]?.id || "");
185
221
  const activeFilterId = ref(props.activeFilterId || props.data.filters?.[0]?.id || "all-types");
@@ -286,9 +322,10 @@ const isEmpty = computed(
286
322
  () => !props.loading && filteredGroups.value.length === 0
287
323
  );
288
324
  function selectTab(id) {
289
- activeTabId.value = id;
290
- emit("update:activeTabId", id);
291
- if (id === "all") {
325
+ const tabId = String(id);
326
+ activeTabId.value = tabId;
327
+ emit("update:activeTabId", tabId);
328
+ if (tabId === "all") {
292
329
  activeFilterId.value = "all-types";
293
330
  emit("update:activeFilterId", "all-types");
294
331
  }
@@ -318,6 +355,33 @@ function scrollFilters() {
318
355
  box-shadow: 0 20px 48px rgba(0, 0, 0, 0.16);
319
356
  overflow: hidden;
320
357
  }
358
+ .notifications-panel--embedded {
359
+ position: static;
360
+ top: auto;
361
+ right: auto;
362
+ z-index: auto;
363
+ width: 100%;
364
+ max-height: calc(100vh - 130px);
365
+ border: none;
366
+ border-radius: 0;
367
+ box-shadow: none;
368
+ }
369
+ .notifications-panel--mobile {
370
+ position: fixed;
371
+ inset: 0;
372
+ z-index: 10000;
373
+ width: 100%;
374
+ max-width: none;
375
+ height: 100dvh;
376
+ max-height: none;
377
+ border: none;
378
+ border-radius: 0;
379
+ box-shadow: none;
380
+ }
381
+ .notifications-panel--mobile .notifications-panel__body {
382
+ flex: 1;
383
+ min-height: 0;
384
+ }
321
385
  .notifications-panel__header {
322
386
  padding: 16px 16px 0;
323
387
  }
@@ -341,16 +405,12 @@ function scrollFilters() {
341
405
  color: var(--grey-60, #6b7280);
342
406
  white-space: nowrap;
343
407
  }
344
- .notifications-panel__tabs {
345
- display: flex;
408
+ .notifications-panel__tabs :deep(.p-tablist-tab-list) {
346
409
  gap: 20px;
347
410
  border-bottom: 1px solid var(--grey-20, #e5e7eb);
411
+ background: transparent;
348
412
  }
349
- .notifications-panel__tab {
350
- display: inline-flex;
351
- align-items: center;
352
- gap: 8px;
353
- margin-bottom: -1px;
413
+ .notifications-panel__tabs :deep(.p-tab) {
354
414
  padding: 0 0 12px;
355
415
  border: none;
356
416
  border-bottom: 2px solid transparent;
@@ -359,25 +419,23 @@ function scrollFilters() {
359
419
  font-size: 13px;
360
420
  font-weight: 700;
361
421
  line-height: 20px;
362
- cursor: pointer;
363
422
  }
364
- .notifications-panel__tab--active {
423
+ .notifications-panel__tabs :deep(.p-tab.p-tab-active) {
365
424
  border-bottom-color: #662e80;
366
425
  color: #662e80;
367
426
  }
368
- .notifications-panel__tab-badge {
427
+ .notifications-panel__tab-item {
428
+ display: inline-flex;
429
+ align-items: center;
430
+ gap: 8px;
431
+ }
432
+ .notifications-panel__tab-badge :deep(.p-badge) {
369
433
  min-width: 16px;
370
434
  height: 16px;
371
435
  padding: 0 4px;
372
- border: 2px solid var(--primary-white, #fff);
373
- border-radius: 8px;
374
- background: #c84b31;
375
- color: var(--primary-white, #fff);
376
436
  font-size: 10px;
377
437
  font-weight: 800;
378
438
  line-height: 12px;
379
- text-align: center;
380
- box-sizing: border-box;
381
439
  }
382
440
  .notifications-panel__filters {
383
441
  display: flex;
@@ -389,6 +447,8 @@ function scrollFilters() {
389
447
  .notifications-panel__filters-scroll {
390
448
  display: flex;
391
449
  gap: 8px;
450
+ flex: 1;
451
+ min-width: 0;
392
452
  overflow-x: auto;
393
453
  scrollbar-width: none;
394
454
  }
@@ -396,50 +456,41 @@ function scrollFilters() {
396
456
  display: none;
397
457
  }
398
458
  .notifications-panel__filter {
399
- display: inline-flex;
400
- align-items: center;
401
- gap: 6px;
402
- padding: 6px 12px;
403
- border: 1px solid var(--grey-20, #e5e7eb);
404
- border-radius: 999px;
405
- background: var(--primary-white, #fff);
406
- color: var(--grey-80, #374151);
459
+ flex-shrink: 0;
407
460
  font-size: 11.5px;
408
461
  line-height: 16px;
409
462
  white-space: nowrap;
410
- cursor: pointer;
411
463
  }
412
- .notifications-panel__filter--active {
413
- border-color: #662e80;
414
- background: #f5f2fb;
415
- color: #662e80;
464
+ .notifications-panel__filter-option {
465
+ display: inline-flex;
466
+ align-items: center;
467
+ gap: 6px;
416
468
  }
417
469
  .notifications-panel__filters-next {
418
- display: flex;
419
- align-items: center;
420
- justify-content: center;
470
+ flex-shrink: 0;
421
471
  width: 28px;
422
472
  height: 28px;
423
473
  padding: 0;
424
- border: 1px solid var(--grey-20, #e5e7eb);
425
- border-radius: 999px;
426
- background: var(--primary-white, #fff);
427
- color: var(--grey-70, #4b5563);
428
- flex-shrink: 0;
429
- cursor: pointer;
430
474
  }
431
475
  .notifications-panel__body {
432
476
  overflow-y: auto;
433
477
  padding: 16px;
434
478
  background: #f7f7f8;
479
+ flex: 1;
480
+ min-height: 0;
435
481
  }
436
- .notifications-panel__loading, .notifications-panel__empty {
437
- margin: 0;
438
- padding: 24px 0;
439
- text-align: center;
440
- font-size: 14px;
441
- line-height: 20px;
442
- color: var(--grey-60, #6b7280);
482
+ .notifications-panel__loading {
483
+ display: flex;
484
+ flex-direction: column;
485
+ gap: 8px;
486
+ }
487
+ .notifications-panel__skeleton {
488
+ border-radius: 8px;
489
+ }
490
+ .notifications-panel__empty {
491
+ justify-content: center;
492
+ width: 100%;
493
+ background: transparent;
443
494
  }
444
495
  .notifications-panel__group + .notifications-panel__group {
445
496
  margin-top: 20px;
@@ -462,13 +513,10 @@ function scrollFilters() {
462
513
  }
463
514
  .notifications-panel__clear-all {
464
515
  padding: 0;
465
- border: none;
466
- background: transparent;
467
516
  color: #662e80;
468
517
  font-size: 12px;
469
518
  font-weight: 600;
470
519
  line-height: 18px;
471
- cursor: pointer;
472
520
  }
473
521
  .notifications-panel__list {
474
522
  display: flex;
@@ -476,40 +524,6 @@ function scrollFilters() {
476
524
  gap: 8px;
477
525
  }
478
526
  .notifications-panel__close {
479
- display: flex;
480
- align-items: center;
481
- justify-content: center;
482
- width: 32px;
483
- height: 32px;
484
527
  margin-left: auto;
485
- padding: 0;
486
- border: none;
487
- border-radius: 8px;
488
- background: transparent;
489
- color: var(--grey-70, #4b5563);
490
- cursor: pointer;
491
- }
492
- .notifications-panel__close:hover {
493
- background: var(--grey-10, #f3f4f6);
494
- }
495
- .notifications-panel--fullscreen {
496
- position: fixed;
497
- inset: 0;
498
- top: 0;
499
- right: 0;
500
- bottom: 0;
501
- left: 0;
502
- z-index: 10000;
503
- width: 100%;
504
- max-width: none;
505
- height: 100dvh;
506
- max-height: none;
507
- border: none;
508
- border-radius: 0;
509
- box-shadow: none;
510
- }
511
- .notifications-panel--fullscreen .notifications-panel__body {
512
- flex: 1;
513
- min-height: 0;
514
528
  }
515
529
  </style>
@@ -4,8 +4,15 @@ type __VLS_Props = {
4
4
  loading?: boolean;
5
5
  activeTabId?: string;
6
6
  activeFilterId?: string;
7
+ embedded?: boolean;
8
+ mobile?: boolean;
7
9
  };
8
10
  declare const _default: import("vue").DefineComponent<__VLS_Props, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
+ select: (payload: {
12
+ item: INotificationsPanelData["groups"][number]["notifications"][number];
13
+ tabId: string;
14
+ filterId: string;
15
+ }) => any;
9
16
  "update:activeTabId": (id: string) => any;
10
17
  "update:activeFilterId": (id: string) => any;
11
18
  "clear-all": (payload: {
@@ -18,13 +25,13 @@ declare const _default: import("vue").DefineComponent<__VLS_Props, void, {}, {},
18
25
  tabId: string;
19
26
  filterId: string;
20
27
  }) => any;
21
- select: (payload: {
28
+ close: () => any;
29
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
30
+ onSelect?: ((payload: {
22
31
  item: INotificationsPanelData["groups"][number]["notifications"][number];
23
32
  tabId: string;
24
33
  filterId: string;
25
- }) => any;
26
- close: () => any;
27
- }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
34
+ }) => any) | undefined;
28
35
  "onUpdate:activeTabId"?: ((id: string) => any) | undefined;
29
36
  "onUpdate:activeFilterId"?: ((id: string) => any) | undefined;
30
37
  "onClear-all"?: ((payload: {
@@ -37,13 +44,10 @@ declare const _default: import("vue").DefineComponent<__VLS_Props, void, {}, {},
37
44
  tabId: string;
38
45
  filterId: string;
39
46
  }) => any) | undefined;
40
- onSelect?: ((payload: {
41
- item: INotificationsPanelData["groups"][number]["notifications"][number];
42
- tabId: string;
43
- filterId: string;
44
- }) => any) | undefined;
45
47
  onClose?: (() => any) | undefined;
46
48
  }>, {
49
+ embedded: boolean;
47
50
  loading: boolean;
51
+ mobile: boolean;
48
52
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
49
53
  export default _default;
@@ -10,8 +10,8 @@ export interface INotificationApiRecord {
10
10
  source_record_type: string;
11
11
  title: string;
12
12
  summary?: string;
13
- actors: INotificationApiActor[];
14
- detail_link?: string;
13
+ actors?: INotificationApiActor[] | null;
14
+ detail_link?: string | null;
15
15
  notification_state: NotificationApiState | string;
16
16
  created_at: string;
17
17
  }
@@ -20,7 +20,8 @@ export const DEFAULT_NOTIFICATION_TABS = [
20
20
  { id: "all", label: "All" }
21
21
  ];
22
22
  export function mapSourceRecordType(sourceRecordType) {
23
- return SOURCE_RECORD_TYPE_MAP[sourceRecordType] ?? "unknown";
23
+ const normalizedSourceRecordType = sourceRecordType.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
24
+ return SOURCE_RECORD_TYPE_MAP[normalizedSourceRecordType] ?? "unknown";
24
25
  }
25
26
  export function mapNotificationApiActor(actor) {
26
27
  const name = [actor.first_name, actor.last_name].filter(Boolean).join(" ").trim();
@@ -34,14 +35,14 @@ export function mapNotificationApiRecord(record, options) {
34
35
  return {
35
36
  id: String(record.id),
36
37
  type: mapSourceRecordType(record.source_record_type),
37
- actors: record.actors.map(mapNotificationApiActor),
38
+ actors: (record.actors ?? []).map(mapNotificationApiActor),
38
39
  actionText: record.title,
39
40
  timestampLabel: formatNotificationTimeAgo(record.created_at, {
40
41
  locale: options.locale,
41
42
  now: options.now,
42
43
  labels: {
43
44
  justNow: options.dateLabels.justNow,
44
- yesterday: options.dateLabels.yesterdayTimeAgo
45
+ yesterdayTimeAgo: options.dateLabels.yesterdayTimeAgo
45
46
  }
46
47
  }),
47
48
  createdAt: record.created_at,
@@ -49,7 +50,7 @@ export function mapNotificationApiRecord(record, options) {
49
50
  dismissed: state === "dismissed",
50
51
  read: state === "read",
51
52
  unread: state === "unread",
52
- href: record.detail_link
53
+ href: record.detail_link ?? void 0
53
54
  };
54
55
  }
55
56
  export function groupNotificationItems(items, options) {
package/package.json CHANGED
@@ -1,49 +1,54 @@
1
1
  {
2
- "name": "@apptegy/nuxt-navigation",
3
- "version": "0.1.104",
4
- "type": "module",
5
- "exports": {
6
- ".": {
7
- "import": "./dist/module.mjs",
8
- "types": "./dist/types.d.mts"
9
- },
10
- "./locale/*": "./locale/*"
2
+ "name": "@apptegy/nuxt-navigation",
3
+ "version": "0.1.106",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "import": "./dist/module.mjs",
8
+ "types": "./dist/types.d.mts"
11
9
  },
12
- "main": "./dist/module.mjs",
13
- "files": [
14
- "dist",
15
- "locale"
16
- ],
17
- "scripts": {
18
- "prepack": "nuxt-module-build build",
19
- "dev": "nuxi dev playground",
20
- "dev:build": "nuxi build playground",
21
- "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
22
- "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
23
- "lint": "eslint .",
24
- "test": "vitest run",
25
- "test:watch": "vitest watch",
26
- "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
27
- },
28
- "dependencies": {
29
- "@apptegy/nuxt-intl": "^2.4.3",
30
- "@nuxt/kit": "^3.17.0",
31
- "@vueuse/components": "^12.0.0",
32
- "@vueuse/core": "^12.0.0"
33
- },
34
- "devDependencies": {
35
- "@nuxt/devtools": "catalog:",
36
- "@nuxt/eslint-config": "catalog:",
37
- "@nuxt/module-builder": "catalog:",
38
- "@nuxt/schema": "catalog:",
39
- "@nuxt/test-utils": "catalog:",
40
- "@types/node": "catalog:",
41
- "eslint": "catalog:",
42
- "nuxt": "catalog:",
43
- "sass": "catalog:",
44
- "typescript": "catalog:",
45
- "vitest": "catalog:",
46
- "vue": "catalog:",
47
- "vue-tsc": "catalog:"
48
- }
10
+ "./locale/*": "./locale/*"
11
+ },
12
+ "main": "./dist/module.mjs",
13
+ "files": [
14
+ "dist",
15
+ "locale"
16
+ ],
17
+ "scripts": {
18
+ "prepack": "nuxt-module-build build",
19
+ "dev": "nuxi dev playground",
20
+ "dev:build": "nuxi build playground",
21
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
22
+ "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
23
+ "lint": "eslint .",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest watch",
26
+ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
27
+ },
28
+ "dependencies": {
29
+ "@apptegy/nuxt-intl": "^2.4.3",
30
+ "@nuxt/kit": "^3.17.0",
31
+ "@vueuse/components": "^12.0.0",
32
+ "@vueuse/core": "^12.0.0"
33
+ },
34
+ "peerDependencies": {
35
+ "primevue": "^4.2.5"
36
+ },
37
+ "devDependencies": {
38
+ "@nuxt/devtools": "^2.6.2",
39
+ "@nuxt/eslint-config": "^1.6.0",
40
+ "@nuxt/module-builder": "^1.0.1",
41
+ "@nuxt/schema": "^3.17.0",
42
+ "@nuxt/test-utils": "^3.17.0",
43
+ "@types/node": "^22.0.0",
44
+ "eslint": "^9.31.0",
45
+ "nuxt": "^3.17.0",
46
+ "primevue": "^4.5.4",
47
+ "sass": "^1.89.2",
48
+ "typescript": "~5.8.0",
49
+ "vitest": "^3.2.4",
50
+ "vue": "^3.5.13",
51
+ "vue-tsc": "^3.0.3"
52
+ },
53
+ "gitHead": "4a69cd943d4e30d94b74f25311ea5b627a4fd568"
49
54
  }