qdadm 0.32.0 → 0.35.0
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 +2 -1
- package/src/auth/SessionAuthAdapter.js +254 -0
- package/src/auth/index.js +10 -0
- package/src/components/layout/AppLayout.vue +2 -0
- package/src/components/pages/LoginPage.vue +3 -2
- package/src/composables/useFormPageBuilder.js +7 -1
- package/src/composables/useListPageBuilder.js +7 -1
- package/src/debug/Collector.js +2 -1
- package/src/debug/EntitiesCollector.js +6 -3
- package/src/debug/SignalCollector.js +4 -4
- package/src/debug/components/DebugBar.vue +8 -1
- package/src/debug/components/panels/EntitiesPanel.vue +9 -0
- package/src/debug/components/panels/EntriesPanel.vue +17 -5
- package/src/debug/components/panels/SignalsPanel.vue +335 -0
- package/src/debug/components/panels/index.js +1 -0
- package/src/entity/EntityManager.js +69 -17
- package/src/entity/auth/CompositeAuthAdapter.js +2 -2
- package/src/entity/auth/{AuthAdapter.js → EntityAuthAdapter.js} +10 -37
- package/src/entity/auth/PermissiveAdapter.js +4 -6
- package/src/entity/auth/factory.js +7 -7
- package/src/entity/auth/factory.test.js +4 -4
- package/src/entity/auth/index.js +1 -1
- package/src/index.js +3 -0
- package/src/kernel/EventRouter.js +7 -4
- package/src/kernel/Kernel.js +18 -18
- package/src/kernel/KernelContext.js +142 -0
- package/src/orchestrator/Orchestrator.js +21 -1
- package/src/orchestrator/useOrchestrator.js +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qdadm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Vue 3 framework for admin dashboards with PrimeVue",
|
|
5
5
|
"author": "quazardous",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
22
22
|
".": "./src/index.js",
|
|
23
|
+
"./auth": "./src/auth/index.js",
|
|
23
24
|
"./composables": "./src/composables/index.js",
|
|
24
25
|
"./components": "./src/components/index.js",
|
|
25
26
|
"./editors": "./src/editors/index.js",
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionAuthAdapter - Base class for user authentication
|
|
3
|
+
*
|
|
4
|
+
* Applications extend this class to implement their authentication logic.
|
|
5
|
+
* The adapter handles user sessions: login, logout, token management.
|
|
6
|
+
*
|
|
7
|
+
* This is different from EntityAuthAdapter which handles entity-level permissions.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```js
|
|
11
|
+
* class MyAuthAdapter extends SessionAuthAdapter {
|
|
12
|
+
* async login({ username, password }) {
|
|
13
|
+
* const response = await api.post('/auth/login', { username, password })
|
|
14
|
+
* this.setSession(response.token, response.user)
|
|
15
|
+
* return { token: response.token, user: response.user }
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* logout() {
|
|
19
|
+
* this.clearSession()
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @abstract
|
|
25
|
+
*/
|
|
26
|
+
export class SessionAuthAdapter {
|
|
27
|
+
/**
|
|
28
|
+
* Internal session state
|
|
29
|
+
* @protected
|
|
30
|
+
*/
|
|
31
|
+
_token = null
|
|
32
|
+
_user = null
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Authenticate user with credentials
|
|
36
|
+
*
|
|
37
|
+
* @param {object} credentials - Login credentials
|
|
38
|
+
* @param {string} credentials.username - Username or email
|
|
39
|
+
* @param {string} credentials.password - Password
|
|
40
|
+
* @returns {Promise<{token: string, user: object}>} Session data
|
|
41
|
+
* @throws {Error} If authentication fails
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const { token, user } = await adapter.login({ username: 'admin', password: 'secret' })
|
|
45
|
+
*/
|
|
46
|
+
async login(credentials) {
|
|
47
|
+
throw new Error('[SessionAuthAdapter] login() must be implemented by subclass')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* End the current session
|
|
52
|
+
*
|
|
53
|
+
* Should clear all session data (tokens, user info).
|
|
54
|
+
* Called by AppLayout logout button and useAuth().logout()
|
|
55
|
+
*/
|
|
56
|
+
logout() {
|
|
57
|
+
throw new Error('[SessionAuthAdapter] logout() must be implemented by subclass')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if user is currently authenticated
|
|
62
|
+
*
|
|
63
|
+
* @returns {boolean} True if user has valid session
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* if (adapter.isAuthenticated()) {
|
|
67
|
+
* // Show dashboard
|
|
68
|
+
* } else {
|
|
69
|
+
* // Redirect to login
|
|
70
|
+
* }
|
|
71
|
+
*/
|
|
72
|
+
isAuthenticated() {
|
|
73
|
+
throw new Error('[SessionAuthAdapter] isAuthenticated() must be implemented by subclass')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the current authentication token
|
|
78
|
+
*
|
|
79
|
+
* Used by API clients to include in request headers.
|
|
80
|
+
*
|
|
81
|
+
* @returns {string|null} JWT token or null if not authenticated
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* const token = adapter.getToken()
|
|
85
|
+
* fetch('/api/data', {
|
|
86
|
+
* headers: { Authorization: `Bearer ${token}` }
|
|
87
|
+
* })
|
|
88
|
+
*/
|
|
89
|
+
getToken() {
|
|
90
|
+
throw new Error('[SessionAuthAdapter] getToken() must be implemented by subclass')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get the current user object
|
|
95
|
+
*
|
|
96
|
+
* Returns user data from the session. The shape depends on your backend.
|
|
97
|
+
*
|
|
98
|
+
* @returns {object|null} User object or null if not authenticated
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* const user = adapter.getUser()
|
|
102
|
+
* console.log(user.username, user.email, user.roles)
|
|
103
|
+
*/
|
|
104
|
+
getUser() {
|
|
105
|
+
throw new Error('[SessionAuthAdapter] getUser() must be implemented by subclass')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Synchronous user getter (optional)
|
|
110
|
+
*
|
|
111
|
+
* Some implementations prefer a property instead of method.
|
|
112
|
+
* useAuth() supports both patterns.
|
|
113
|
+
*
|
|
114
|
+
* @type {object|null}
|
|
115
|
+
*/
|
|
116
|
+
get user() {
|
|
117
|
+
return this.getUser()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─────────────────────────────────────────────────────────────────
|
|
121
|
+
// Helper methods for subclasses
|
|
122
|
+
// ─────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Set session data (helper for subclasses)
|
|
126
|
+
*
|
|
127
|
+
* @param {string} token - Authentication token
|
|
128
|
+
* @param {object} user - User object
|
|
129
|
+
* @protected
|
|
130
|
+
*/
|
|
131
|
+
setSession(token, user) {
|
|
132
|
+
this._token = token
|
|
133
|
+
this._user = user
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Clear session data (helper for subclasses)
|
|
138
|
+
*
|
|
139
|
+
* @protected
|
|
140
|
+
*/
|
|
141
|
+
clearSession() {
|
|
142
|
+
this._token = null
|
|
143
|
+
this._user = null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Validate that the adapter is properly configured
|
|
148
|
+
*
|
|
149
|
+
* Called during bootstrap to catch configuration errors early.
|
|
150
|
+
*
|
|
151
|
+
* @throws {Error} If required methods are not implemented
|
|
152
|
+
*/
|
|
153
|
+
static validate(adapter) {
|
|
154
|
+
const required = ['login', 'logout', 'isAuthenticated', 'getToken', 'getUser']
|
|
155
|
+
const missing = required.filter(method => typeof adapter[method] !== 'function')
|
|
156
|
+
|
|
157
|
+
if (missing.length > 0) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`[SessionAuthAdapter] Missing required methods: ${missing.join(', ')}\n` +
|
|
160
|
+
'Ensure your authAdapter implements all required methods or extends SessionAuthAdapter.'
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* LocalStorage-based SessionAuthAdapter implementation
|
|
168
|
+
*
|
|
169
|
+
* Ready-to-use adapter that stores session in localStorage.
|
|
170
|
+
* Extend and override login() to add your API call.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```js
|
|
174
|
+
* class MyAuthAdapter extends LocalStorageSessionAuthAdapter {
|
|
175
|
+
* constructor() {
|
|
176
|
+
* super('my_app_auth') // localStorage key
|
|
177
|
+
* }
|
|
178
|
+
*
|
|
179
|
+
* async login({ username, password }) {
|
|
180
|
+
* const res = await fetch('/api/login', {
|
|
181
|
+
* method: 'POST',
|
|
182
|
+
* body: JSON.stringify({ username, password })
|
|
183
|
+
* })
|
|
184
|
+
* const data = await res.json()
|
|
185
|
+
* this.setSession(data.token, data.user)
|
|
186
|
+
* this.persist()
|
|
187
|
+
* return data
|
|
188
|
+
* }
|
|
189
|
+
* }
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
export class LocalStorageSessionAuthAdapter extends SessionAuthAdapter {
|
|
193
|
+
/**
|
|
194
|
+
* @param {string} storageKey - localStorage key for session data
|
|
195
|
+
*/
|
|
196
|
+
constructor(storageKey = 'qdadm_auth') {
|
|
197
|
+
super()
|
|
198
|
+
this._storageKey = storageKey
|
|
199
|
+
this._restore()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Restore session from localStorage on init
|
|
204
|
+
* @private
|
|
205
|
+
*/
|
|
206
|
+
_restore() {
|
|
207
|
+
try {
|
|
208
|
+
const stored = localStorage.getItem(this._storageKey)
|
|
209
|
+
if (stored) {
|
|
210
|
+
const { token, user } = JSON.parse(stored)
|
|
211
|
+
this._token = token
|
|
212
|
+
this._user = user
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// Invalid stored data, ignore
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Persist session to localStorage
|
|
221
|
+
* Call after login() to save session
|
|
222
|
+
* @protected
|
|
223
|
+
*/
|
|
224
|
+
persist() {
|
|
225
|
+
if (this._token && this._user) {
|
|
226
|
+
localStorage.setItem(this._storageKey, JSON.stringify({
|
|
227
|
+
token: this._token,
|
|
228
|
+
user: this._user
|
|
229
|
+
}))
|
|
230
|
+
} else {
|
|
231
|
+
localStorage.removeItem(this._storageKey)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Arrow functions to preserve `this` when used as callbacks
|
|
236
|
+
logout = () => {
|
|
237
|
+
this.clearSession()
|
|
238
|
+
localStorage.removeItem(this._storageKey)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
isAuthenticated = () => {
|
|
242
|
+
return !!this._token
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getToken = () => {
|
|
246
|
+
return this._token
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
getUser = () => {
|
|
250
|
+
return this._user
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export default SessionAuthAdapter
|
|
@@ -35,6 +35,7 @@ const route = useRoute()
|
|
|
35
35
|
const app = useApp()
|
|
36
36
|
const { navSections, isNavActive, sectionHasActiveItem, handleNavClick } = useNavigation()
|
|
37
37
|
const { isAuthenticated, user, logout, authEnabled } = useAuth()
|
|
38
|
+
const signals = inject('qdadmSignals', null)
|
|
38
39
|
|
|
39
40
|
// LocalStorage key for collapsed sections state (namespaced by app)
|
|
40
41
|
const STORAGE_KEY = computed(() => `${app.shortName.toLowerCase()}_nav_collapsed`)
|
|
@@ -150,6 +151,7 @@ const userSubtitle = computed(() => {
|
|
|
150
151
|
|
|
151
152
|
function handleLogout() {
|
|
152
153
|
logout()
|
|
154
|
+
signals?.emit('auth:logout', { reason: 'user' })
|
|
153
155
|
router.push({ name: 'login' })
|
|
154
156
|
}
|
|
155
157
|
|
|
@@ -90,11 +90,12 @@ const props = defineProps({
|
|
|
90
90
|
default: ''
|
|
91
91
|
},
|
|
92
92
|
/**
|
|
93
|
-
* Emit
|
|
93
|
+
* Emit auth:login signal on successful login
|
|
94
|
+
* Required for debug bar auth tracking and other signal listeners
|
|
94
95
|
*/
|
|
95
96
|
emitSignal: {
|
|
96
97
|
type: Boolean,
|
|
97
|
-
default:
|
|
98
|
+
default: true
|
|
98
99
|
}
|
|
99
100
|
})
|
|
100
101
|
|
|
@@ -120,7 +120,13 @@ export function useFormPageBuilder(config = {}) {
|
|
|
120
120
|
// Get EntityManager via orchestrator
|
|
121
121
|
const orchestrator = inject('qdadmOrchestrator')
|
|
122
122
|
if (!orchestrator) {
|
|
123
|
-
throw new Error(
|
|
123
|
+
throw new Error(
|
|
124
|
+
'[qdadm] Orchestrator not provided.\n' +
|
|
125
|
+
'Possible causes:\n' +
|
|
126
|
+
'1. Kernel not initialized - ensure createKernel().createApp() is called before mounting\n' +
|
|
127
|
+
'2. Component used outside of qdadm app context\n' +
|
|
128
|
+
'3. Missing entityFactory in Kernel options'
|
|
129
|
+
)
|
|
124
130
|
}
|
|
125
131
|
const manager = orchestrator.get(entity)
|
|
126
132
|
|
|
@@ -143,7 +143,13 @@ export function useListPageBuilder(config = {}) {
|
|
|
143
143
|
// Get EntityManager via orchestrator
|
|
144
144
|
const orchestrator = inject('qdadmOrchestrator')
|
|
145
145
|
if (!orchestrator) {
|
|
146
|
-
throw new Error(
|
|
146
|
+
throw new Error(
|
|
147
|
+
'[qdadm] Orchestrator not provided.\n' +
|
|
148
|
+
'Possible causes:\n' +
|
|
149
|
+
'1. Kernel not initialized - ensure createKernel().createApp() is called before mounting\n' +
|
|
150
|
+
'2. Component used outside of qdadm app context\n' +
|
|
151
|
+
'3. Missing entityFactory in Kernel options'
|
|
152
|
+
)
|
|
147
153
|
}
|
|
148
154
|
const manager = orchestrator.get(entity)
|
|
149
155
|
|
package/src/debug/Collector.js
CHANGED
|
@@ -179,10 +179,11 @@ export class Collector {
|
|
|
179
179
|
|
|
180
180
|
/**
|
|
181
181
|
* Get all entries
|
|
182
|
+
* Returns a shallow copy to trigger Vue reactivity when used in computed
|
|
182
183
|
* @returns {Array<object>} All recorded entries
|
|
183
184
|
*/
|
|
184
185
|
getEntries() {
|
|
185
|
-
return this.entries
|
|
186
|
+
return [...this.entries]
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
/**
|
|
@@ -77,12 +77,12 @@ export class EntitiesCollector extends Collector {
|
|
|
77
77
|
})
|
|
78
78
|
this._signalCleanups.push(entityCleanup)
|
|
79
79
|
|
|
80
|
-
// Listen to
|
|
81
|
-
const
|
|
80
|
+
// Listen to entity data changes (CRUD operations)
|
|
81
|
+
const dataCleanup = signals.on('entity:data-invalidate', () => {
|
|
82
82
|
this._lastUpdate = Date.now()
|
|
83
83
|
this.notifyChange()
|
|
84
84
|
})
|
|
85
|
-
this._signalCleanups.push(
|
|
85
|
+
this._signalCleanups.push(dataCleanup)
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
@@ -248,6 +248,9 @@ export class EntitiesCollector extends Collector {
|
|
|
248
248
|
canList: manager.canList?.() ?? true
|
|
249
249
|
},
|
|
250
250
|
|
|
251
|
+
// Auth sensitivity (auto-invalidates on auth events)
|
|
252
|
+
authSensitive: manager._authSensitive ?? false,
|
|
253
|
+
|
|
251
254
|
// Warmup
|
|
252
255
|
warmup: {
|
|
253
256
|
enabled: manager.warmupEnabled ?? false
|
|
@@ -18,8 +18,8 @@ import { Collector } from './Collector.js'
|
|
|
18
18
|
* Collector for SignalBus events
|
|
19
19
|
*
|
|
20
20
|
* Records all signals with their name, data, and source for debugging.
|
|
21
|
-
* Uses the
|
|
22
|
-
*
|
|
21
|
+
* Uses the `**` wildcard pattern to capture all signals including
|
|
22
|
+
* multi-segment names (e.g., entity:data-invalidate, auth:impersonate:start).
|
|
23
23
|
*/
|
|
24
24
|
export class SignalCollector extends Collector {
|
|
25
25
|
/**
|
|
@@ -43,8 +43,8 @@ export class SignalCollector extends Collector {
|
|
|
43
43
|
|
|
44
44
|
// Subscribe to all signals using wildcard pattern
|
|
45
45
|
// QuarKernel supports wildcards with the configured delimiter (:)
|
|
46
|
-
// '
|
|
47
|
-
this._unsubscribe = ctx.signals.on('
|
|
46
|
+
// '**' matches all signals including multi-segment (entity:data-invalidate)
|
|
47
|
+
this._unsubscribe = ctx.signals.on('**', (event) => {
|
|
48
48
|
this.record({
|
|
49
49
|
name: event.name,
|
|
50
50
|
data: event.data,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
|
13
13
|
import Badge from 'primevue/badge'
|
|
14
14
|
import Button from 'primevue/button'
|
|
15
|
-
import { ZonesPanel, AuthPanel, EntitiesPanel, ToastsPanel, EntriesPanel } from './panels'
|
|
15
|
+
import { ZonesPanel, AuthPanel, EntitiesPanel, ToastsPanel, EntriesPanel, SignalsPanel } from './panels'
|
|
16
16
|
|
|
17
17
|
const props = defineProps({
|
|
18
18
|
bridge: { type: Object, required: true },
|
|
@@ -658,6 +658,13 @@ function getCollectorColor(name) {
|
|
|
658
658
|
@update="notifyBridge"
|
|
659
659
|
/>
|
|
660
660
|
|
|
661
|
+
<!-- Signals collector -->
|
|
662
|
+
<SignalsPanel
|
|
663
|
+
v-else-if="currentCollector.name === 'signals' || currentCollector.name === 'SignalCollector'"
|
|
664
|
+
:collector="currentCollector.collector"
|
|
665
|
+
:entries="currentCollector.entries"
|
|
666
|
+
/>
|
|
667
|
+
|
|
661
668
|
<div v-else-if="currentCollector.entries.length === 0" class="debug-empty">
|
|
662
669
|
<i class="pi pi-inbox" />
|
|
663
670
|
<span>No entries</span>
|
|
@@ -138,6 +138,11 @@ function getCapabilityLabel(cap) {
|
|
|
138
138
|
class="pi pi-eye perm-icon-readonly"
|
|
139
139
|
title="Read-only entity"
|
|
140
140
|
/>
|
|
141
|
+
<i
|
|
142
|
+
v-if="entity.authSensitive"
|
|
143
|
+
class="pi pi-shield perm-icon-auth-sensitive"
|
|
144
|
+
title="Auth-sensitive (auto-invalidates on auth events)"
|
|
145
|
+
/>
|
|
141
146
|
</div>
|
|
142
147
|
<span class="entity-label">{{ entity.label }}</span>
|
|
143
148
|
<span v-if="entity.cache.enabled" class="entity-cache" :class="{ 'entity-cache-valid': entity.cache.valid }">
|
|
@@ -397,6 +402,10 @@ function getCapabilityLabel(cap) {
|
|
|
397
402
|
color: #f59e0b;
|
|
398
403
|
margin-left: 2px;
|
|
399
404
|
}
|
|
405
|
+
.perm-icon-auth-sensitive {
|
|
406
|
+
color: #8b5cf6;
|
|
407
|
+
margin-left: 2px;
|
|
408
|
+
}
|
|
400
409
|
.entity-label {
|
|
401
410
|
color: #a1a1aa;
|
|
402
411
|
font-size: 11px;
|
|
@@ -46,6 +46,7 @@ async function copyEntry(entry, idx) {
|
|
|
46
46
|
:class="{ 'entry-new': entry._isNew }"
|
|
47
47
|
>
|
|
48
48
|
<div class="entry-meta">
|
|
49
|
+
<span v-if="entry._isNew" class="entry-new-dot" title="New (unseen)" />
|
|
49
50
|
<span class="entry-time">{{ formatTime(entry.timestamp) }}</span>
|
|
50
51
|
<span v-if="entry.name" class="entry-name">{{ entry.name }}</span>
|
|
51
52
|
</div>
|
|
@@ -62,6 +63,7 @@ async function copyEntry(entry, idx) {
|
|
|
62
63
|
:class="{ 'entry-new': entry._isNew }"
|
|
63
64
|
>
|
|
64
65
|
<div class="entry-header">
|
|
66
|
+
<span v-if="entry._isNew" class="entry-new-dot" title="New (unseen)" />
|
|
65
67
|
<span class="entry-time">{{ formatTime(entry.timestamp) }}</span>
|
|
66
68
|
<span v-if="entry.name" class="entry-name">{{ entry.name }}</span>
|
|
67
69
|
<span v-if="entry.message" class="entry-message">{{ entry.message }}</span>
|
|
@@ -98,9 +100,6 @@ async function copyEntry(entry, idx) {
|
|
|
98
100
|
.entry-h:last-child {
|
|
99
101
|
border-right: none;
|
|
100
102
|
}
|
|
101
|
-
.entry-h.entry-new {
|
|
102
|
-
border-left: 3px solid #8b5cf6;
|
|
103
|
-
}
|
|
104
103
|
|
|
105
104
|
/* Vertical entries (right/fullscreen mode) */
|
|
106
105
|
.entries-v {
|
|
@@ -118,7 +117,7 @@ async function copyEntry(entry, idx) {
|
|
|
118
117
|
/* New entry indicator */
|
|
119
118
|
.entry-new {
|
|
120
119
|
position: relative;
|
|
121
|
-
background: rgba(
|
|
120
|
+
background: rgba(245, 158, 11, 0.08);
|
|
122
121
|
}
|
|
123
122
|
.entry-v.entry-new::before {
|
|
124
123
|
content: '';
|
|
@@ -127,7 +126,20 @@ async function copyEntry(entry, idx) {
|
|
|
127
126
|
top: 0;
|
|
128
127
|
bottom: 0;
|
|
129
128
|
width: 3px;
|
|
130
|
-
background: #
|
|
129
|
+
background: #f59e0b;
|
|
130
|
+
}
|
|
131
|
+
.entry-h.entry-new {
|
|
132
|
+
border-left: 3px solid #f59e0b;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.entry-new-dot {
|
|
136
|
+
display: inline-block;
|
|
137
|
+
width: 6px;
|
|
138
|
+
height: 6px;
|
|
139
|
+
background: #f59e0b;
|
|
140
|
+
border-radius: 50%;
|
|
141
|
+
margin-right: 4px;
|
|
142
|
+
flex-shrink: 0;
|
|
131
143
|
}
|
|
132
144
|
|
|
133
145
|
/* Entry parts */
|