@apptegy/nuxt-navigation 0.1.96 → 0.1.98

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.96",
7
+ "version": "0.1.98",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.1",
10
10
  "unbuild": "unknown"
@@ -10,6 +10,66 @@
10
10
  <slot name="org-select" />
11
11
  <slot name="user-controls" />
12
12
  <div class="user-control-actions">
13
+ <div
14
+ v-if="showNotifications"
15
+ ref="notificationsRef"
16
+ v-on-click-outside="onNotificationsClickOutside"
17
+ class="header__notifications"
18
+ >
19
+ <button
20
+ type="button"
21
+ class="notification-bell"
22
+ :class="{ 'notification-bell--active': panelOpen }"
23
+ :aria-label="$t('navigation.notifications')"
24
+ :aria-expanded="panelOpen"
25
+ aria-haspopup="dialog"
26
+ @click="toggleNotificationsPanel"
27
+ >
28
+ <svg
29
+ class="notification-bell__icon"
30
+ :class="{ 'notification-bell__icon--shaking': isBellShaking }"
31
+ xmlns="http://www.w3.org/2000/svg"
32
+ width="22"
33
+ height="22"
34
+ viewBox="0 0 24 24"
35
+ fill="none"
36
+ stroke="currentColor"
37
+ stroke-width="2"
38
+ stroke-linecap="round"
39
+ stroke-linejoin="round"
40
+ aria-hidden="true"
41
+ @animationend="onBellShakeEnd"
42
+ >
43
+ <path d="M10.268 21a2 2 0 0 0 3.464 0" />
44
+ <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" />
45
+ </svg>
46
+ <span
47
+ v-if="notificationCount > 0"
48
+ class="notification-bell__badge"
49
+ aria-hidden="true"
50
+ >
51
+ {{ formattedNotificationCount }}
52
+ </span>
53
+ </button>
54
+
55
+ <slot name="notifications">
56
+ <Teleport to="body" :disabled="!isMobilePanel">
57
+ <NavNotificationsPanel
58
+ v-if="notificationsPanel && panelOpen"
59
+ :data="notificationsPanel"
60
+ :loading="notificationsLoading"
61
+ :active-tab-id="activeTabId"
62
+ :active-filter-id="activeFilterId"
63
+ @update:active-tab-id="handleTabChange"
64
+ @update:active-filter-id="handleFilterChange"
65
+ @clear-all="(payload) => $emit('notifications-clear-all', payload)"
66
+ @dismiss="(payload) => $emit('notifications-dismiss', payload)"
67
+ @select="(payload) => $emit('notifications-select', payload)"
68
+ @close="closeNotificationsPanel"
69
+ />
70
+ </Teleport>
71
+ </slot>
72
+ </div>
13
73
  <NavUserDropdown
14
74
  :user="user"
15
75
  :user-dropdown-options="userDropdownOptions"
@@ -47,14 +107,136 @@
47
107
  </template>
48
108
 
49
109
  <script setup>
50
- defineProps({
110
+ import { computed, nextTick, ref, watch } from "vue";
111
+ import { vOnClickOutside } from "@vueuse/components";
112
+ import { useMediaQuery } from "@vueuse/core";
113
+ import { useNuxtApp } from "#app";
114
+ import {
115
+ getNotificationsTotalCount,
116
+ mapNotificationsApiResponse
117
+ } from "../../utils/notification-api";
118
+ const props = defineProps({
51
119
  sticky: { type: Boolean, required: false },
120
+ showNotifications: { type: Boolean, required: false, default: false },
121
+ notificationsResponse: { type: [Object, null], required: false, default: null },
122
+ notificationsLoading: { type: Boolean, required: false, default: false },
123
+ notificationPanelOpen: { type: Boolean, required: false, default: false },
124
+ activeTabId: { type: String, required: false, default: "" },
125
+ activeFilterId: { type: String, required: false, default: "" },
52
126
  user: { type: Object, required: true },
53
127
  impersonatedUser: { type: Object, required: false },
54
128
  useLogoutEmit: { type: Boolean, required: false },
55
129
  userDropdownOptions: { type: Array, required: true }
56
130
  });
57
- defineEmits(["selected-option", "user-logout"]);
131
+ const nuxtApp = useNuxtApp();
132
+ const emit = defineEmits([
133
+ "selected-option",
134
+ "user-logout",
135
+ "update:notificationPanelOpen",
136
+ "update:activeTabId",
137
+ "update:activeFilterId",
138
+ "notifications-clear-all",
139
+ "notifications-dismiss",
140
+ "notifications-select",
141
+ "notification-click"
142
+ ]);
143
+ const notificationsRef = ref();
144
+ const isMobilePanel = useMediaQuery("(max-width: 960px)");
145
+ const panelOpen = ref(props.notificationPanelOpen);
146
+ const isBellShaking = ref(false);
147
+ const notificationDateLabels = computed(() => ({
148
+ today: nuxtApp.$t("navigation.notificationsPanel.dateGroups.today"),
149
+ yesterday: nuxtApp.$t("navigation.notificationsPanel.dateGroups.yesterday"),
150
+ yesterdayTimeAgo: nuxtApp.$t("navigation.notificationsPanel.yesterday"),
151
+ justNow: nuxtApp.$t("navigation.notificationsPanel.justNow")
152
+ }));
153
+ const notificationsPanel = computed(() => {
154
+ if (!props.notificationsResponse) {
155
+ return null;
156
+ }
157
+ return mapNotificationsApiResponse(props.notificationsResponse, {
158
+ locale: nuxtApp.$intl.locale,
159
+ dateLabels: notificationDateLabels.value,
160
+ retentionLabel: nuxtApp.$t("navigation.notificationsPanel.retentionLabel"),
161
+ tabs: [
162
+ { id: "new", label: nuxtApp.$t("navigation.notificationsPanel.tabs.new"), unreadOnly: true },
163
+ { id: "all", label: nuxtApp.$t("navigation.notificationsPanel.tabs.all") }
164
+ ]
165
+ });
166
+ });
167
+ const notificationCount = computed(
168
+ () => getNotificationsTotalCount(props.notificationsResponse)
169
+ );
170
+ const activeTabId = ref(props.activeTabId || notificationsPanel.value?.tabs[0]?.id || "");
171
+ const activeFilterId = ref(props.activeFilterId || notificationsPanel.value?.filters?.[0]?.id || "all-types");
172
+ watch(
173
+ notificationCount,
174
+ (newCount, oldCount) => {
175
+ if (oldCount !== void 0 && newCount > oldCount) {
176
+ playBellShake();
177
+ }
178
+ }
179
+ );
180
+ async function playBellShake() {
181
+ isBellShaking.value = false;
182
+ await nextTick();
183
+ isBellShaking.value = true;
184
+ }
185
+ function onBellShakeEnd() {
186
+ isBellShaking.value = false;
187
+ }
188
+ watch(
189
+ () => props.notificationPanelOpen,
190
+ (value) => {
191
+ panelOpen.value = value;
192
+ }
193
+ );
194
+ watch(panelOpen, (value) => {
195
+ emit("update:notificationPanelOpen", value);
196
+ });
197
+ watch(
198
+ () => props.activeTabId,
199
+ (value) => {
200
+ if (value) {
201
+ activeTabId.value = value;
202
+ }
203
+ }
204
+ );
205
+ watch(
206
+ () => props.activeFilterId,
207
+ (value) => {
208
+ if (value) {
209
+ activeFilterId.value = value;
210
+ }
211
+ }
212
+ );
213
+ const formattedNotificationCount = computed(() => {
214
+ if (notificationCount.value > 99) {
215
+ return "99+";
216
+ }
217
+ return notificationCount.value;
218
+ });
219
+ function toggleNotificationsPanel() {
220
+ panelOpen.value = !panelOpen.value;
221
+ emit("notification-click", panelOpen.value);
222
+ }
223
+ function closeNotificationsPanel() {
224
+ panelOpen.value = false;
225
+ }
226
+ function onNotificationsClickOutside() {
227
+ if (isMobilePanel.value || !panelOpen.value) {
228
+ return;
229
+ }
230
+ panelOpen.value = false;
231
+ }
232
+ function handleTabChange(id) {
233
+ activeTabId.value = id;
234
+ emit("update:activeTabId", id);
235
+ }
236
+ function handleFilterChange(id) {
237
+ activeFilterId.value = id;
238
+ emit("update:activeFilterId", id);
239
+ }
58
240
  </script>
59
241
 
60
242
  <style scoped>
@@ -73,8 +255,57 @@ header.header {
73
255
  }
74
256
  header.header .user-control-actions {
75
257
  display: flex;
258
+ align-items: center;
76
259
  gap: 8px;
77
260
  }
261
+ header.header .header__notifications {
262
+ position: relative;
263
+ display: flex;
264
+ align-items: center;
265
+ }
266
+ header.header .notification-bell {
267
+ position: relative;
268
+ display: flex;
269
+ align-items: center;
270
+ justify-content: center;
271
+ width: 32px;
272
+ height: 32px;
273
+ padding: 0;
274
+ border: none;
275
+ border-radius: 8px;
276
+ background: transparent;
277
+ cursor: pointer;
278
+ transition: background-color 0.15s ease;
279
+ }
280
+ header.header .notification-bell:hover, header.header .notification-bell--active {
281
+ background: var(--grey-10, #f3f4f6);
282
+ }
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
+ header.header .notification-bell__badge {
293
+ position: absolute;
294
+ top: 2px;
295
+ right: -4px;
296
+ min-width: 16px;
297
+ height: 16px;
298
+ padding: 0 4px;
299
+ border: 2px solid var(--primary-white, #fff);
300
+ border-radius: 8px;
301
+ background: #c84b31;
302
+ color: var(--primary-white, #fff);
303
+ font-size: 10px;
304
+ font-weight: 800;
305
+ line-height: 12px;
306
+ text-align: center;
307
+ box-sizing: border-box;
308
+ }
78
309
  header.header .logout-impersonate-user {
79
310
  width: 32px;
80
311
  height: 32px;
@@ -98,4 +329,33 @@ header.header .logout-impersonate-user:hover {
98
329
  header.header.sticky {
99
330
  position: sticky;
100
331
  }
332
+
333
+ @keyframes bellShake {
334
+ 0% {
335
+ transform: rotate(0deg);
336
+ }
337
+ 15% {
338
+ transform: rotate(14deg);
339
+ }
340
+ 30% {
341
+ transform: rotate(-12deg);
342
+ }
343
+ 45% {
344
+ transform: rotate(10deg);
345
+ }
346
+ 60% {
347
+ transform: rotate(-7deg);
348
+ }
349
+ 75% {
350
+ transform: rotate(4deg);
351
+ }
352
+ 100% {
353
+ transform: rotate(0deg);
354
+ }
355
+ }
356
+ @media (prefers-reduced-motion: reduce) {
357
+ .notification-bell__icon--shaking {
358
+ animation: none !important;
359
+ }
360
+ }
101
361
  </style>
@@ -1,6 +1,12 @@
1
- import type { IUser } from '../../types/index.js';
1
+ import type { INotificationsApiResponse, IUser } from '../../types/index.js';
2
2
  interface IHeaderProps {
3
3
  sticky?: boolean;
4
+ showNotifications?: boolean;
5
+ notificationsResponse?: INotificationsApiResponse | null;
6
+ notificationsLoading?: boolean;
7
+ notificationPanelOpen?: boolean;
8
+ activeTabId?: string;
9
+ activeFilterId?: string;
4
10
  }
5
11
  interface IUserDropdown {
6
12
  user: {
@@ -13,15 +19,24 @@ interface IUserDropdown {
13
19
  userDropdownOptions: Array<unknown>;
14
20
  }
15
21
  type __VLS_Props = IHeaderProps & IUserDropdown;
16
- declare var __VLS_1: {}, __VLS_3: {}, __VLS_5: {};
22
+ declare var __VLS_1: {}, __VLS_3: {}, __VLS_5: {}, __VLS_7: {};
17
23
  type __VLS_Slots = {} & {
18
24
  breadcrumbs?: (props: typeof __VLS_1) => any;
19
25
  } & {
20
26
  'org-select'?: (props: typeof __VLS_3) => any;
21
27
  } & {
22
28
  'user-controls'?: (props: typeof __VLS_5) => any;
29
+ } & {
30
+ notifications?: (props: typeof __VLS_7) => any;
23
31
  };
24
- declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, any, string, import("vue").PublicProps, any, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
32
+ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, any, string, import("vue").PublicProps, any, {
33
+ showNotifications: boolean;
34
+ notificationsResponse: INotificationsApiResponse | null;
35
+ notificationsLoading: boolean;
36
+ notificationPanelOpen: boolean;
37
+ activeTabId: string;
38
+ activeFilterId: string;
39
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
25
40
  declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
26
41
  export default _default;
27
42
  type __VLS_WithSlots<T, S> = T & {
@@ -0,0 +1,353 @@
1
+ <template>
2
+ <div
3
+ class="notification-item"
4
+ :class="{ 'notification-item--in-badge': showBadgeActions }"
5
+ >
6
+ <div
7
+ class="notification-item__main"
8
+ role="button"
9
+ tabindex="0"
10
+ @click="handleClick"
11
+ @keydown.enter.prevent="handleClick"
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"
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
+
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>
62
+ </div>
63
+
64
+ <div class="notification-item__actions">
65
+ <span
66
+ v-if="showBadgeActions"
67
+ class="notification-item__unread-dot"
68
+ aria-hidden="true"
69
+ />
70
+ <button
71
+ v-if="showBadgeActions"
72
+ type="button"
73
+ class="notification-item__dismiss"
74
+ :aria-label="$t('navigation.notificationsPanel.dismiss')"
75
+ @click="handleDismiss"
76
+ >
77
+ <svg
78
+ xmlns="http://www.w3.org/2000/svg"
79
+ width="16"
80
+ height="16"
81
+ viewBox="0 0 24 24"
82
+ fill="none"
83
+ stroke="currentColor"
84
+ stroke-width="2"
85
+ stroke-linecap="round"
86
+ stroke-linejoin="round"
87
+ aria-hidden="true"
88
+ >
89
+ <path d="M18 6 6 18" />
90
+ <path d="m6 6 12 12" />
91
+ </svg>
92
+ </button>
93
+ </div>
94
+ </div>
95
+ </template>
96
+
97
+ <script setup>
98
+ import { computed, h } from "vue";
99
+ import { getTypeAccent, isInBadge } from "../../utils/notifications";
100
+ const props = defineProps({
101
+ item: { type: Object, required: true }
102
+ });
103
+ const emit = defineEmits(["dismiss", "select"]);
104
+ const primaryActor = computed(() => props.item.actors[0] || { name: "" });
105
+ const typeAccent = computed(() => getTypeAccent(props.item.type));
106
+ const showBadgeActions = computed(() => isInBadge(props.item));
107
+ const groupBadgeLabel = computed(() => {
108
+ const count = props.item.groupCount ?? Math.max(props.item.actors.length - 1, 0);
109
+ return count > 0 ? `+${count}` : "";
110
+ });
111
+ const additionalActorsLabel = computed(() => {
112
+ if (props.item.groupCount && props.item.groupCount > 0) {
113
+ return ` and ${props.item.groupCount} others`;
114
+ }
115
+ if (props.item.actors.length <= 1) {
116
+ return "";
117
+ }
118
+ const names = props.item.actors.slice(1).map((actor) => actor.name);
119
+ return `, ${names.join(", ")}`;
120
+ });
121
+ function svgIcon(paths) {
122
+ return h("svg", {
123
+ xmlns: "http://www.w3.org/2000/svg",
124
+ width: 12,
125
+ height: 12,
126
+ viewBox: "0 0 24 24",
127
+ fill: "none",
128
+ stroke: "currentColor",
129
+ "stroke-width": 2,
130
+ "stroke-linecap": "round",
131
+ "stroke-linejoin": "round"
132
+ }, paths);
133
+ }
134
+ const typeIcon = computed(() => {
135
+ switch (props.item.type) {
136
+ case "announcement":
137
+ return svgIcon([
138
+ h("path", { d: "m3 11 18-5v12L3 14v-3z" }),
139
+ h("path", { d: "M11.6 16.8a3 3 0 1 1-5.8-1.6" })
140
+ ]);
141
+ case "message":
142
+ return svgIcon([
143
+ h("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })
144
+ ]);
145
+ case "comment":
146
+ return svgIcon([
147
+ h("path", { d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" })
148
+ ]);
149
+ case "mention":
150
+ return svgIcon([
151
+ h("circle", { cx: 12, cy: 12, r: 4 }),
152
+ h("path", { d: "M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8" })
153
+ ]);
154
+ case "reply":
155
+ return svgIcon([
156
+ h("path", { d: "M9 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-5l-5 5z" }),
157
+ h("path", { d: "m10 9-3 3 3 3" })
158
+ ]);
159
+ case "reaction":
160
+ return svgIcon([
161
+ h("path", { d: "M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" })
162
+ ]);
163
+ case "school-post":
164
+ return svgIcon([
165
+ h("path", { d: "M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2" }),
166
+ h("path", { d: "M18 14h-8" }),
167
+ h("path", { d: "M15 18h-5" }),
168
+ h("path", { d: "M10 6h8v4h-8V6Z" })
169
+ ]);
170
+ case "system":
171
+ return svgIcon([
172
+ h("circle", { cx: 12, cy: 12, r: 10 }),
173
+ h("path", { d: "M12 16v-4" }),
174
+ h("path", { d: "M12 8h.01" })
175
+ ]);
176
+ default:
177
+ return svgIcon([
178
+ h("path", { d: "M10.268 21a2 2 0 0 0 3.464 0" }),
179
+ h("path", { d: "M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" })
180
+ ]);
181
+ }
182
+ });
183
+ function handleClick() {
184
+ emit("select", props.item);
185
+ }
186
+ function handleDismiss(event) {
187
+ event.preventDefault();
188
+ event.stopPropagation();
189
+ emit("dismiss", props.item.id);
190
+ }
191
+ </script>
192
+
193
+ <style scoped>
194
+ .notification-item {
195
+ display: grid;
196
+ grid-template-columns: 1fr auto;
197
+ gap: 12px;
198
+ align-items: start;
199
+ width: 100%;
200
+ padding: 12px;
201
+ border: 1px solid var(--grey-20, #e5e7eb);
202
+ border-radius: 8px;
203
+ background: var(--primary-white, #fff);
204
+ text-align: left;
205
+ color: inherit;
206
+ transition: background-color 0.15s ease;
207
+ }
208
+ .notification-item__main {
209
+ display: grid;
210
+ grid-template-columns: auto 1fr;
211
+ gap: 12px;
212
+ align-items: start;
213
+ min-width: 0;
214
+ cursor: pointer;
215
+ border: none;
216
+ background: transparent;
217
+ padding: 0;
218
+ text-align: left;
219
+ color: inherit;
220
+ }
221
+ .notification-item__main:hover .notification-item__message strong {
222
+ color: var(--purple-60, #662e80);
223
+ }
224
+ .notification-item__main:focus-visible {
225
+ outline: 2px solid var(--purple-60, #662e80);
226
+ outline-offset: 2px;
227
+ border-radius: 4px;
228
+ }
229
+ .notification-item:hover {
230
+ background: var(--grey-10, #f9fafb);
231
+ }
232
+ .notification-item__avatar {
233
+ position: relative;
234
+ width: 44px;
235
+ height: 44px;
236
+ flex-shrink: 0;
237
+ }
238
+ .notification-item__avatar-ring {
239
+ width: 44px;
240
+ height: 44px;
241
+ padding: 2px;
242
+ border-radius: 50%;
243
+ background: var(--type-accent);
244
+ box-sizing: border-box;
245
+ }
246
+ .notification-item__avatar-image :deep(span) {
247
+ background-color: #f3e6d4;
248
+ color: var(--purple-40, #ab8cb9);
249
+ border: 1px solid var(--primary-white, #fff);
250
+ }
251
+ .notification-item__avatar-image :deep(img), .notification-item__avatar-image :deep(span) {
252
+ width: 40px;
253
+ height: 40px;
254
+ }
255
+ .notification-item__group-badge {
256
+ position: absolute;
257
+ right: -4px;
258
+ bottom: 0;
259
+ min-width: 22px;
260
+ height: 22px;
261
+ padding: 0 4px;
262
+ border: 2px solid var(--primary-white, #fff);
263
+ border-radius: 999px;
264
+ background: var(--grey-80, #374151);
265
+ color: var(--primary-white, #fff);
266
+ font-size: 10px;
267
+ font-weight: 600;
268
+ line-height: 18px;
269
+ text-align: center;
270
+ z-index: 1;
271
+ }
272
+ .notification-item__type-badge {
273
+ position: absolute;
274
+ right: -2px;
275
+ bottom: -2px;
276
+ display: flex;
277
+ align-items: center;
278
+ justify-content: center;
279
+ width: 22px;
280
+ height: 22px;
281
+ border: 2px solid var(--primary-white, #fff);
282
+ border-radius: 999px;
283
+ background: var(--type-accent);
284
+ color: var(--primary-white, #fff);
285
+ z-index: 2;
286
+ }
287
+ .notification-item__content {
288
+ min-width: 0;
289
+ }
290
+ .notification-item__message {
291
+ margin: 0;
292
+ font-size: 14.5px;
293
+ font-weight: 600;
294
+ line-height: 20px;
295
+ color: var(--grey-90, #111827);
296
+ }
297
+ .notification-item__message strong {
298
+ font-weight: 600;
299
+ }
300
+ .notification-item__meta {
301
+ display: flex;
302
+ flex-wrap: wrap;
303
+ gap: 4px;
304
+ margin: 4px 0 0;
305
+ font-size: 12.5px;
306
+ line-height: 16px;
307
+ color: var(--grey-50, #747474);
308
+ }
309
+ .notification-item__meta-separator {
310
+ padding: 0 2px;
311
+ }
312
+ .notification-item__state-chip {
313
+ display: inline-flex;
314
+ margin: 8px 0 0;
315
+ padding: 4px 8px;
316
+ border-radius: 4px;
317
+ background: var(--grey-10, #f3f3f3);
318
+ color: var(--grey-60, #515151);
319
+ font-size: 12px;
320
+ font-weight: 600;
321
+ line-height: 16px;
322
+ }
323
+ .notification-item__actions {
324
+ display: flex;
325
+ align-items: center;
326
+ gap: 8px;
327
+ padding-top: 2px;
328
+ }
329
+ .notification-item__unread-dot {
330
+ width: 8px;
331
+ height: 8px;
332
+ border-radius: 50%;
333
+ background: #c84b31;
334
+ flex-shrink: 0;
335
+ }
336
+ .notification-item__dismiss {
337
+ display: flex;
338
+ align-items: center;
339
+ justify-content: center;
340
+ width: 24px;
341
+ height: 24px;
342
+ padding: 0;
343
+ border: none;
344
+ border-radius: 4px;
345
+ background: transparent;
346
+ color: var(--grey-50, #9ca3af);
347
+ cursor: pointer;
348
+ }
349
+ .notification-item__dismiss:hover {
350
+ background: var(--grey-10, #f3f4f6);
351
+ color: var(--grey-80, #374151);
352
+ }
353
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { INotificationItem } from '../../types/index.js';
2
+ type __VLS_Props = {
3
+ item: INotificationItem;
4
+ };
5
+ declare const _default: import("vue").DefineComponent<__VLS_Props, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ dismiss: (id: string) => any;
7
+ select: (item: INotificationItem) => any;
8
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
9
+ onDismiss?: ((id: string) => any) | undefined;
10
+ onSelect?: ((item: INotificationItem) => any) | undefined;
11
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
12
+ export default _default;