datajunction-ui 0.0.18 → 0.0.19
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/package.json +1 -1
- package/src/app/components/NotificationBell.tsx +223 -0
- package/src/app/components/UserMenu.tsx +100 -0
- package/src/app/components/__tests__/NotificationBell.test.tsx +302 -0
- package/src/app/components/__tests__/UserMenu.test.tsx +241 -0
- package/src/app/icons/NotificationIcon.jsx +27 -0
- package/src/app/icons/SettingsIcon.jsx +28 -0
- package/src/app/index.tsx +12 -0
- package/src/app/pages/NotificationsPage/Loadable.jsx +6 -0
- package/src/app/pages/NotificationsPage/__tests__/index.test.jsx +287 -0
- package/src/app/pages/NotificationsPage/index.jsx +136 -0
- package/src/app/pages/Root/__tests__/index.test.jsx +18 -53
- package/src/app/pages/Root/index.tsx +23 -19
- package/src/app/pages/SettingsPage/CreateServiceAccountModal.jsx +152 -0
- package/src/app/pages/SettingsPage/Loadable.jsx +16 -0
- package/src/app/pages/SettingsPage/NotificationSubscriptionsSection.jsx +189 -0
- package/src/app/pages/SettingsPage/ProfileSection.jsx +41 -0
- package/src/app/pages/SettingsPage/ServiceAccountsSection.jsx +95 -0
- package/src/app/pages/SettingsPage/__tests__/CreateServiceAccountModal.test.jsx +318 -0
- package/src/app/pages/SettingsPage/__tests__/NotificationSubscriptionsSection.test.jsx +233 -0
- package/src/app/pages/SettingsPage/__tests__/ProfileSection.test.jsx +65 -0
- package/src/app/pages/SettingsPage/__tests__/ServiceAccountsSection.test.jsx +150 -0
- package/src/app/pages/SettingsPage/__tests__/index.test.jsx +184 -0
- package/src/app/pages/SettingsPage/index.jsx +148 -0
- package/src/app/services/DJService.js +81 -0
- package/src/app/utils/__tests__/date.test.js +198 -0
- package/src/app/utils/date.js +65 -0
- package/src/styles/index.css +1 -1
- package/src/styles/nav-bar.css +274 -0
- package/src/styles/settings.css +787 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { formatRelativeTime, getDateGroup, groupByDate } from '../date';
|
|
2
|
+
|
|
3
|
+
describe('date utilities', () => {
|
|
4
|
+
describe('formatRelativeTime', () => {
|
|
5
|
+
it('returns "just now" for times less than 1 minute ago', () => {
|
|
6
|
+
const now = new Date();
|
|
7
|
+
expect(formatRelativeTime(now.toISOString())).toBe('just now');
|
|
8
|
+
|
|
9
|
+
const thirtySecondsAgo = new Date(Date.now() - 30000);
|
|
10
|
+
expect(formatRelativeTime(thirtySecondsAgo.toISOString())).toBe(
|
|
11
|
+
'just now',
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('returns minutes ago for times less than 1 hour ago', () => {
|
|
16
|
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60000);
|
|
17
|
+
expect(formatRelativeTime(fiveMinutesAgo.toISOString())).toBe('5m ago');
|
|
18
|
+
|
|
19
|
+
const thirtyMinutesAgo = new Date(Date.now() - 30 * 60000);
|
|
20
|
+
expect(formatRelativeTime(thirtyMinutesAgo.toISOString())).toBe(
|
|
21
|
+
'30m ago',
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const fiftyNineMinutesAgo = new Date(Date.now() - 59 * 60000);
|
|
25
|
+
expect(formatRelativeTime(fiftyNineMinutesAgo.toISOString())).toBe(
|
|
26
|
+
'59m ago',
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns hours ago for times less than 24 hours ago', () => {
|
|
31
|
+
const oneHourAgo = new Date(Date.now() - 1 * 3600000);
|
|
32
|
+
expect(formatRelativeTime(oneHourAgo.toISOString())).toBe('1h ago');
|
|
33
|
+
|
|
34
|
+
const twelveHoursAgo = new Date(Date.now() - 12 * 3600000);
|
|
35
|
+
expect(formatRelativeTime(twelveHoursAgo.toISOString())).toBe('12h ago');
|
|
36
|
+
|
|
37
|
+
const twentyThreeHoursAgo = new Date(Date.now() - 23 * 3600000);
|
|
38
|
+
expect(formatRelativeTime(twentyThreeHoursAgo.toISOString())).toBe(
|
|
39
|
+
'23h ago',
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns days ago for times less than 7 days ago', () => {
|
|
44
|
+
const oneDayAgo = new Date(Date.now() - 1 * 86400000);
|
|
45
|
+
expect(formatRelativeTime(oneDayAgo.toISOString())).toBe('1d ago');
|
|
46
|
+
|
|
47
|
+
const threeDaysAgo = new Date(Date.now() - 3 * 86400000);
|
|
48
|
+
expect(formatRelativeTime(threeDaysAgo.toISOString())).toBe('3d ago');
|
|
49
|
+
|
|
50
|
+
const sixDaysAgo = new Date(Date.now() - 6 * 86400000);
|
|
51
|
+
expect(formatRelativeTime(sixDaysAgo.toISOString())).toBe('6d ago');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns formatted date for times 7 or more days ago', () => {
|
|
55
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 86400000);
|
|
56
|
+
const result = formatRelativeTime(sevenDaysAgo.toISOString());
|
|
57
|
+
// Should return a locale date string, not "Xd ago"
|
|
58
|
+
expect(result).not.toContain('d ago');
|
|
59
|
+
expect(result).toMatch(/\d/); // Should contain numbers (date)
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('getDateGroup', () => {
|
|
64
|
+
it('returns "Today" for dates from today', () => {
|
|
65
|
+
const now = new Date();
|
|
66
|
+
expect(getDateGroup(now.toISOString())).toBe('Today');
|
|
67
|
+
|
|
68
|
+
// Earlier today (midnight)
|
|
69
|
+
const midnight = new Date();
|
|
70
|
+
midnight.setHours(0, 0, 0, 0);
|
|
71
|
+
expect(getDateGroup(midnight.toISOString())).toBe('Today');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns "Yesterday" for dates from yesterday', () => {
|
|
75
|
+
const yesterday = new Date();
|
|
76
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
77
|
+
expect(getDateGroup(yesterday.toISOString())).toBe('Yesterday');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('returns "This Week" for dates from this week (but not today or yesterday)', () => {
|
|
81
|
+
const now = new Date();
|
|
82
|
+
const dayOfWeek = now.getDay();
|
|
83
|
+
|
|
84
|
+
// Only test if we're not on Sunday (0) or Monday (1)
|
|
85
|
+
// because "This Week" starts on Sunday
|
|
86
|
+
if (dayOfWeek >= 2) {
|
|
87
|
+
const twoDaysAgo = new Date();
|
|
88
|
+
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
|
|
89
|
+
expect(getDateGroup(twoDaysAgo.toISOString())).toBe('This Week');
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('returns "Last Week" for dates from last week', () => {
|
|
94
|
+
const now = new Date();
|
|
95
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
96
|
+
const dayOfWeek = today.getDay();
|
|
97
|
+
|
|
98
|
+
// Calculate start of this week (Sunday)
|
|
99
|
+
const thisWeekStart = new Date(today);
|
|
100
|
+
thisWeekStart.setDate(thisWeekStart.getDate() - dayOfWeek);
|
|
101
|
+
|
|
102
|
+
// Last week is 1-7 days before this week's start
|
|
103
|
+
const lastWeekDate = new Date(thisWeekStart);
|
|
104
|
+
lastWeekDate.setDate(lastWeekDate.getDate() - 3); // Middle of last week
|
|
105
|
+
|
|
106
|
+
expect(getDateGroup(lastWeekDate.toISOString())).toBe('Last Week');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('returns "Older" for dates older than last week', () => {
|
|
110
|
+
const threeWeeksAgo = new Date();
|
|
111
|
+
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
|
|
112
|
+
expect(getDateGroup(threeWeeksAgo.toISOString())).toBe('Older');
|
|
113
|
+
|
|
114
|
+
const monthAgo = new Date();
|
|
115
|
+
monthAgo.setMonth(monthAgo.getMonth() - 1);
|
|
116
|
+
expect(getDateGroup(monthAgo.toISOString())).toBe('Older');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('groupByDate', () => {
|
|
121
|
+
it('groups items by their date', () => {
|
|
122
|
+
const now = new Date();
|
|
123
|
+
const yesterday = new Date();
|
|
124
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
125
|
+
const lastMonth = new Date();
|
|
126
|
+
lastMonth.setMonth(lastMonth.getMonth() - 1);
|
|
127
|
+
|
|
128
|
+
const items = [
|
|
129
|
+
{ id: 1, created_at: now.toISOString() },
|
|
130
|
+
{ id: 2, created_at: now.toISOString() },
|
|
131
|
+
{ id: 3, created_at: yesterday.toISOString() },
|
|
132
|
+
{ id: 4, created_at: lastMonth.toISOString() },
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const result = groupByDate(items);
|
|
136
|
+
|
|
137
|
+
expect(result).toHaveLength(3); // Today, Yesterday, Older
|
|
138
|
+
|
|
139
|
+
const todayGroup = result.find(g => g.label === 'Today');
|
|
140
|
+
expect(todayGroup.items).toHaveLength(2);
|
|
141
|
+
expect(todayGroup.items.map(i => i.id)).toEqual([1, 2]);
|
|
142
|
+
|
|
143
|
+
const yesterdayGroup = result.find(g => g.label === 'Yesterday');
|
|
144
|
+
expect(yesterdayGroup.items).toHaveLength(1);
|
|
145
|
+
expect(yesterdayGroup.items[0].id).toBe(3);
|
|
146
|
+
|
|
147
|
+
const olderGroup = result.find(g => g.label === 'Older');
|
|
148
|
+
expect(olderGroup.items).toHaveLength(1);
|
|
149
|
+
expect(olderGroup.items[0].id).toBe(4);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('returns groups in correct order', () => {
|
|
153
|
+
const now = new Date();
|
|
154
|
+
const yesterday = new Date();
|
|
155
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
156
|
+
const lastMonth = new Date();
|
|
157
|
+
lastMonth.setMonth(lastMonth.getMonth() - 1);
|
|
158
|
+
|
|
159
|
+
// Items in random order
|
|
160
|
+
const items = [
|
|
161
|
+
{ id: 1, created_at: lastMonth.toISOString() },
|
|
162
|
+
{ id: 2, created_at: now.toISOString() },
|
|
163
|
+
{ id: 3, created_at: yesterday.toISOString() },
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const result = groupByDate(items);
|
|
167
|
+
const labels = result.map(g => g.label);
|
|
168
|
+
|
|
169
|
+
// Should be in order: Today, Yesterday, Older
|
|
170
|
+
expect(labels).toEqual(['Today', 'Yesterday', 'Older']);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('uses custom date field when specified', () => {
|
|
174
|
+
const now = new Date();
|
|
175
|
+
const items = [{ id: 1, updated_at: now.toISOString() }];
|
|
176
|
+
|
|
177
|
+
const result = groupByDate(items, 'updated_at');
|
|
178
|
+
|
|
179
|
+
expect(result).toHaveLength(1);
|
|
180
|
+
expect(result[0].label).toBe('Today');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('returns empty array for empty input', () => {
|
|
184
|
+
const result = groupByDate([]);
|
|
185
|
+
expect(result).toEqual([]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('only includes groups that have items', () => {
|
|
189
|
+
const now = new Date();
|
|
190
|
+
const items = [{ id: 1, created_at: now.toISOString() }];
|
|
191
|
+
|
|
192
|
+
const result = groupByDate(items);
|
|
193
|
+
|
|
194
|
+
expect(result).toHaveLength(1);
|
|
195
|
+
expect(result[0].label).toBe('Today');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format a date string as relative time (e.g., "2h ago", "3d ago")
|
|
3
|
+
*/
|
|
4
|
+
export const formatRelativeTime = dateString => {
|
|
5
|
+
const date = new Date(dateString);
|
|
6
|
+
const now = new Date();
|
|
7
|
+
const diffMs = now.getTime() - date.getTime();
|
|
8
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
9
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
10
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
11
|
+
|
|
12
|
+
if (diffMins < 1) return 'just now';
|
|
13
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
14
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
15
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
16
|
+
return date.toLocaleDateString();
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get a date group label for grouping items (Today, Yesterday, This Week, etc.)
|
|
21
|
+
*/
|
|
22
|
+
export const getDateGroup = dateString => {
|
|
23
|
+
const date = new Date(dateString);
|
|
24
|
+
const now = new Date();
|
|
25
|
+
|
|
26
|
+
// Reset times to compare dates only
|
|
27
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
28
|
+
const yesterday = new Date(today);
|
|
29
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
30
|
+
const thisWeekStart = new Date(today);
|
|
31
|
+
thisWeekStart.setDate(thisWeekStart.getDate() - today.getDay());
|
|
32
|
+
const lastWeekStart = new Date(thisWeekStart);
|
|
33
|
+
lastWeekStart.setDate(lastWeekStart.getDate() - 7);
|
|
34
|
+
|
|
35
|
+
const dateOnly = new Date(
|
|
36
|
+
date.getFullYear(),
|
|
37
|
+
date.getMonth(),
|
|
38
|
+
date.getDate(),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (dateOnly >= today) return 'Today';
|
|
42
|
+
if (dateOnly >= yesterday) return 'Yesterday';
|
|
43
|
+
if (dateOnly >= thisWeekStart) return 'This Week';
|
|
44
|
+
if (dateOnly >= lastWeekStart) return 'Last Week';
|
|
45
|
+
return 'Older';
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Group items by date using getDateGroup
|
|
50
|
+
*/
|
|
51
|
+
export const groupByDate = (items, dateField = 'created_at') => {
|
|
52
|
+
const groups = {};
|
|
53
|
+
const order = ['Today', 'Yesterday', 'This Week', 'Last Week', 'Older'];
|
|
54
|
+
|
|
55
|
+
items.forEach(item => {
|
|
56
|
+
const group = getDateGroup(item[dateField]);
|
|
57
|
+
if (!groups[group]) groups[group] = [];
|
|
58
|
+
groups[group].push(item);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Return in order
|
|
62
|
+
return order
|
|
63
|
+
.filter(g => groups[g]?.length > 0)
|
|
64
|
+
.map(g => ({ label: g, items: groups[g] }));
|
|
65
|
+
};
|
package/src/styles/index.css
CHANGED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/* Nav bar right section */
|
|
2
|
+
.nav-right {
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
gap: 0.5rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* Icon button (notification bell) */
|
|
9
|
+
.nav-icon-button {
|
|
10
|
+
width: 36px;
|
|
11
|
+
height: 36px;
|
|
12
|
+
border-radius: 50%;
|
|
13
|
+
background-color: transparent;
|
|
14
|
+
border: 1px solid #e0e0e0;
|
|
15
|
+
cursor: pointer;
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
justify-content: center;
|
|
19
|
+
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
|
|
20
|
+
color: #666;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.nav-icon-button:hover {
|
|
24
|
+
background-color: #f5f5f5;
|
|
25
|
+
border-color: #ccc;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.nav-icon-button svg {
|
|
29
|
+
width: 18px;
|
|
30
|
+
height: 18px;
|
|
31
|
+
fill: currentColor;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Avatar button with initials */
|
|
35
|
+
.avatar-button {
|
|
36
|
+
width: 36px;
|
|
37
|
+
height: 36px;
|
|
38
|
+
border-radius: 50%;
|
|
39
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
40
|
+
color: white;
|
|
41
|
+
font-size: 13px;
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
letter-spacing: 0.5px;
|
|
44
|
+
border: none;
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
transition: transform 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.avatar-button:hover {
|
|
53
|
+
transform: scale(1.05);
|
|
54
|
+
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Dropdown container */
|
|
58
|
+
.nav-dropdown {
|
|
59
|
+
position: relative;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Dropdown menu - align to right edge */
|
|
63
|
+
.nav-dropdown-menu {
|
|
64
|
+
position: absolute;
|
|
65
|
+
top: 100%;
|
|
66
|
+
right: 0;
|
|
67
|
+
left: auto;
|
|
68
|
+
min-width: 200px;
|
|
69
|
+
margin-top: 0.5rem;
|
|
70
|
+
border: 1px solid #e0e0e0;
|
|
71
|
+
border-radius: 8px;
|
|
72
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
73
|
+
background: white;
|
|
74
|
+
z-index: 1000;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.nav-dropdown-menu .dropdown-header {
|
|
78
|
+
padding: 0.75rem 1rem 0.5rem;
|
|
79
|
+
font-size: 13px;
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
color: #333;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.nav-dropdown-menu .dropdown-item {
|
|
85
|
+
display: block;
|
|
86
|
+
padding: 0.5rem 1rem;
|
|
87
|
+
font-size: 14px;
|
|
88
|
+
color: #444;
|
|
89
|
+
text-decoration: none;
|
|
90
|
+
cursor: pointer;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.nav-dropdown-menu .dropdown-item:hover {
|
|
94
|
+
background-color: #f5f5f5;
|
|
95
|
+
text-decoration: none;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.nav-dropdown-menu .dropdown-divider {
|
|
99
|
+
margin: 0.25rem 0;
|
|
100
|
+
border: none;
|
|
101
|
+
border-top: 1px solid #e0e0e0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.nav-dropdown-menu .text-muted {
|
|
105
|
+
color: #999 !important;
|
|
106
|
+
font-size: 13px;
|
|
107
|
+
cursor: default;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.nav-dropdown-menu .text-muted:hover {
|
|
111
|
+
background-color: transparent;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Notification badge */
|
|
115
|
+
.nav-icon-button {
|
|
116
|
+
position: relative;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.notification-badge {
|
|
120
|
+
position: absolute;
|
|
121
|
+
top: -4px;
|
|
122
|
+
right: -4px;
|
|
123
|
+
min-width: 18px;
|
|
124
|
+
height: 18px;
|
|
125
|
+
padding: 0 5px;
|
|
126
|
+
font-size: 11px;
|
|
127
|
+
font-weight: 600;
|
|
128
|
+
color: white;
|
|
129
|
+
background-color: #ef4444;
|
|
130
|
+
border-radius: 9px;
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: center;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* Notifications menu */
|
|
137
|
+
.notifications-menu {
|
|
138
|
+
min-width: 240px;
|
|
139
|
+
max-width: 600px;
|
|
140
|
+
padding: 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.notifications-menu .dropdown-header {
|
|
144
|
+
padding: 0.5rem 0.75rem;
|
|
145
|
+
font-size: 11px;
|
|
146
|
+
font-weight: 600;
|
|
147
|
+
text-transform: uppercase;
|
|
148
|
+
letter-spacing: 0.5px;
|
|
149
|
+
color: #888;
|
|
150
|
+
border-bottom: 1px solid #eee;
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
justify-content: space-between;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.notifications-menu .dropdown-header .header-left {
|
|
157
|
+
display: flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
gap: 0.35rem;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.notifications-menu .dropdown-header .header-left svg {
|
|
163
|
+
width: 12px;
|
|
164
|
+
height: 12px;
|
|
165
|
+
fill: none;
|
|
166
|
+
stroke: #888;
|
|
167
|
+
stroke-width: 1.5;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.notifications-menu .dropdown-header .header-settings {
|
|
171
|
+
display: flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
padding: 2px;
|
|
174
|
+
border-radius: 4px;
|
|
175
|
+
color: #aaa;
|
|
176
|
+
transition: color 0.15s, background-color 0.15s;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.notifications-menu .dropdown-header .header-settings:hover {
|
|
180
|
+
color: #666;
|
|
181
|
+
background-color: #f0f0f0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.notifications-menu .dropdown-header .header-settings svg {
|
|
185
|
+
width: 14px;
|
|
186
|
+
height: 14px;
|
|
187
|
+
fill: currentColor;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.notification-group {
|
|
191
|
+
margin-bottom: 1.5rem;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.card-body {
|
|
195
|
+
margin: 0 2.5rem;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.notification-item {
|
|
199
|
+
display: flex;
|
|
200
|
+
flex-direction: column;
|
|
201
|
+
padding: 0.5rem 0.75rem;
|
|
202
|
+
text-decoration: none;
|
|
203
|
+
border-bottom: 1px solid #f5f5f5;
|
|
204
|
+
gap: 2px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.notification-item:last-child {
|
|
208
|
+
border-bottom: none;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.notification-item:hover {
|
|
212
|
+
background-color: #f8f9fa;
|
|
213
|
+
text-decoration: none;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.notification-node {
|
|
217
|
+
display: flex;
|
|
218
|
+
flex-direction: column;
|
|
219
|
+
gap: 0.15rem;
|
|
220
|
+
margin-bottom: 0.25rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.notification-title {
|
|
224
|
+
font-size: 13px;
|
|
225
|
+
color: #333;
|
|
226
|
+
font-weight: 500;
|
|
227
|
+
display: flex;
|
|
228
|
+
align-items: center;
|
|
229
|
+
gap: 0.4rem;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.notification-entity {
|
|
233
|
+
font-size: 11px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.notification-title .badge.version {
|
|
237
|
+
font-size: 10px;
|
|
238
|
+
padding: 3px 5px 2px 4px;
|
|
239
|
+
background-color: transparent;
|
|
240
|
+
color: #888;
|
|
241
|
+
border: 1px solid #ccc;
|
|
242
|
+
font-weight: 500;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.notification-meta {
|
|
246
|
+
font-size: 11px;
|
|
247
|
+
color: #999;
|
|
248
|
+
display: flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
gap: 0.35rem;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.notification-meta .badge.node_type {
|
|
254
|
+
font-size: 9px;
|
|
255
|
+
padding: 4px;
|
|
256
|
+
margin-right: 2px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.notifications-menu .dropdown-divider {
|
|
260
|
+
margin: 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.notifications-menu .view-all {
|
|
264
|
+
display: block;
|
|
265
|
+
padding: 0.5rem 0.75rem;
|
|
266
|
+
font-size: 12px;
|
|
267
|
+
color: #667eea;
|
|
268
|
+
text-align: center;
|
|
269
|
+
text-decoration: none;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.notifications-menu .view-all:hover {
|
|
273
|
+
background-color: #f8f9fa;
|
|
274
|
+
}
|