@quazardous/qdadm 2.31.0 → 2.33.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.
Files changed (41) hide show
  1. package/dist/styles/qdadm.css +1 -1
  2. package/package.json +8 -2
  3. package/src/components/display/EmailPreview.vue +227 -0
  4. package/src/components/display/EntityTimeline.vue +408 -0
  5. package/src/components/display/LogStream.vue +364 -0
  6. package/src/components/display/PayloadDiff.vue +236 -0
  7. package/src/components/display/PayloadViewer.vue +246 -0
  8. package/src/components/index.ts +8 -0
  9. package/src/components/layout/AppLayout.vue +31 -11
  10. package/src/components/layout/defaults/DefaultMenu.vue +23 -11
  11. package/src/components/tasks/TaskDialog.vue +95 -0
  12. package/src/components/tasks/TaskHost.vue +45 -0
  13. package/src/components/tasks/TaskTray.vue +54 -0
  14. package/src/composables/index.ts +11 -0
  15. package/src/composables/useListPage.filters.ts +26 -1
  16. package/src/composables/useLogStream.ts +264 -0
  17. package/src/composables/useNavigation.ts +39 -3
  18. package/src/email/index.ts +46 -0
  19. package/src/entity/storage/SdkStorage.ts +53 -1
  20. package/src/entity/storage/index.ts +2 -0
  21. package/src/i18n/defaults/core.en.generated.ts +85 -0
  22. package/src/i18n/defaults/core.en.yml +85 -0
  23. package/src/i18n/defaults/core.fr.generated.ts +85 -0
  24. package/src/i18n/defaults/core.fr.yml +85 -0
  25. package/src/index.ts +21 -0
  26. package/src/kernel/Kernel.vue.ts +9 -2
  27. package/src/kernel/KernelContext.types.ts +11 -1
  28. package/src/module/moduleRegistry.ts +17 -1
  29. package/src/styles/_email.scss +188 -0
  30. package/src/styles/_log-stream.scss +167 -0
  31. package/src/styles/_payload.scss +312 -0
  32. package/src/styles/_sidebar.scss +8 -0
  33. package/src/styles/_tasks.scss +212 -0
  34. package/src/styles/_timeline.scss +229 -0
  35. package/src/styles/index.scss +5 -0
  36. package/src/tasks/taskStore.ts +460 -0
  37. package/src/tasks/useTaskRunner.ts +52 -0
  38. package/src/utils/ansi.ts +127 -0
  39. package/src/utils/email.ts +270 -0
  40. package/src/utils/index.ts +32 -0
  41. package/src/utils/payload.ts +480 -0
@@ -71,6 +71,91 @@ export default `core:
71
71
  notRegistered: non enregistrée
72
72
  remove: Retirer
73
73
  via: "via {origin}"
74
+ # LogStream (#4110)
75
+ log:
76
+ streaming: En cours
77
+ done: Terminé
78
+ stopped: Arrêté
79
+ failed: "Échec : {error}"
80
+ lines: "{count} lignes"
81
+ search: Chercher dans le log
82
+ previous: Occurrence précédente
83
+ next: Occurrence suivante
84
+ onlyMatches: Seulement les lignes trouvées
85
+ errors: "{count} erreurs"
86
+ firstError: Aller à la ligne d'erreur suivante
87
+ wrap: Couper les lignes longues
88
+ follow: Suivre la fin du log
89
+ download: Télécharger le log
90
+ newLines: "{count} nouvelles lignes, reprendre"
91
+ empty: Rien pour l'instant
92
+ # EntityTimeline (#4111)
93
+ timeline:
94
+ actors: Filtrer par acteur
95
+ loading: Chargement…
96
+ empty: Rien ne s'est encore passé
97
+ newEvents: "{count} nouveaux événements"
98
+ oldestFirst: Plus anciens d'abord
99
+ newestFirst: Plus récents d'abord
100
+ # PayloadViewer / PayloadDiff (#4112)
101
+ payload:
102
+ headers: "En-têtes ({count})"
103
+ headerRow: La première ligne est un en-tête
104
+ copy: Copier
105
+ copied: Copié dans le presse-papiers
106
+ copyFailed: Copie impossible
107
+ download: Télécharger
108
+ showAll: "Afficher les {count} lignes"
109
+ binary: "Contenu binaire, {size}. Premiers octets :"
110
+ malformed: "{format} invalide ({error}), affiché comme texte"
111
+ expected: Attendu
112
+ captured: Capturé
113
+ identical: Identiques
114
+ unchanged: "{count} lignes inchangées"
115
+ showSame: "Afficher les {count} lignes inchangées"
116
+ inline: En ligne
117
+ split: Côte à côte
118
+ # EmailPreview (#4113)
119
+ email:
120
+ from: De
121
+ to: À
122
+ cc: Cc
123
+ replyTo: Répondre à
124
+ date: Date
125
+ noSubject: (sans objet)
126
+ allHeaders: "Tous les en-têtes ({count})"
127
+ noParser: "Un message brut demande un parser : passer parseMime de @quazardous/qdadm/email. La source est affichée."
128
+ tab:
129
+ html: HTML
130
+ text: Texte
131
+ source: Source
132
+ imagesBlocked: "{count} images distantes bloquées"
133
+ loadImages: Charger les images
134
+ blockImages: Bloquer les images
135
+ empty: Ce mail n'a pas de contenu
136
+ unnamed: (sans nom)
137
+ inline: intégrée
138
+ download: Télécharger
139
+ # TaskRunner (#4114)
140
+ task:
141
+ status:
142
+ starting: Démarrage
143
+ queued: En attente
144
+ running: En cours
145
+ succeeded: Terminée
146
+ failed: Échouée
147
+ cancelled: Annulée
148
+ cancel: Annuler la tâche
149
+ close: Fermer
150
+ background: Continuer en arrière-plan
151
+ open: Ouvrir la tâche
152
+ readError: "Lecture de la tâche impossible ({error}), nouvel essai"
153
+ start: Lancer
154
+ dontStart: Ne pas lancer
155
+ typeToConfirm: "Taper {value} pour confirmer"
156
+ tray: "{count} tâches en cours"
157
+ trayDone: Tâches récentes
158
+ clear: Effacer les terminées
74
159
  # The breadcrumb's View↔Edit toggle (useNavContext).
75
160
  breadcrumb:
76
161
  view: Voir
@@ -69,6 +69,91 @@ core:
69
69
  notRegistered: non enregistrée
70
70
  remove: Retirer
71
71
  via: "via {origin}"
72
+ # LogStream (#4110)
73
+ log:
74
+ streaming: En cours
75
+ done: Terminé
76
+ stopped: Arrêté
77
+ failed: "Échec : {error}"
78
+ lines: "{count} lignes"
79
+ search: Chercher dans le log
80
+ previous: Occurrence précédente
81
+ next: Occurrence suivante
82
+ onlyMatches: Seulement les lignes trouvées
83
+ errors: "{count} erreurs"
84
+ firstError: Aller à la ligne d'erreur suivante
85
+ wrap: Couper les lignes longues
86
+ follow: Suivre la fin du log
87
+ download: Télécharger le log
88
+ newLines: "{count} nouvelles lignes, reprendre"
89
+ empty: Rien pour l'instant
90
+ # EntityTimeline (#4111)
91
+ timeline:
92
+ actors: Filtrer par acteur
93
+ loading: Chargement…
94
+ empty: Rien ne s'est encore passé
95
+ newEvents: "{count} nouveaux événements"
96
+ oldestFirst: Plus anciens d'abord
97
+ newestFirst: Plus récents d'abord
98
+ # PayloadViewer / PayloadDiff (#4112)
99
+ payload:
100
+ headers: "En-têtes ({count})"
101
+ headerRow: La première ligne est un en-tête
102
+ copy: Copier
103
+ copied: Copié dans le presse-papiers
104
+ copyFailed: Copie impossible
105
+ download: Télécharger
106
+ showAll: "Afficher les {count} lignes"
107
+ binary: "Contenu binaire, {size}. Premiers octets :"
108
+ malformed: "{format} invalide ({error}), affiché comme texte"
109
+ expected: Attendu
110
+ captured: Capturé
111
+ identical: Identiques
112
+ unchanged: "{count} lignes inchangées"
113
+ showSame: "Afficher les {count} lignes inchangées"
114
+ inline: En ligne
115
+ split: Côte à côte
116
+ # EmailPreview (#4113)
117
+ email:
118
+ from: De
119
+ to: À
120
+ cc: Cc
121
+ replyTo: Répondre à
122
+ date: Date
123
+ noSubject: (sans objet)
124
+ allHeaders: "Tous les en-têtes ({count})"
125
+ noParser: "Un message brut demande un parser : passer parseMime de @quazardous/qdadm/email. La source est affichée."
126
+ tab:
127
+ html: HTML
128
+ text: Texte
129
+ source: Source
130
+ imagesBlocked: "{count} images distantes bloquées"
131
+ loadImages: Charger les images
132
+ blockImages: Bloquer les images
133
+ empty: Ce mail n'a pas de contenu
134
+ unnamed: (sans nom)
135
+ inline: intégrée
136
+ download: Télécharger
137
+ # TaskRunner (#4114)
138
+ task:
139
+ status:
140
+ starting: Démarrage
141
+ queued: En attente
142
+ running: En cours
143
+ succeeded: Terminée
144
+ failed: Échouée
145
+ cancelled: Annulée
146
+ cancel: Annuler la tâche
147
+ close: Fermer
148
+ background: Continuer en arrière-plan
149
+ open: Ouvrir la tâche
150
+ readError: "Lecture de la tâche impossible ({error}), nouvel essai"
151
+ start: Lancer
152
+ dontStart: Ne pas lancer
153
+ typeToConfirm: "Taper {value} pour confirmer"
154
+ tray: "{count} tâches en cours"
155
+ trayDone: Tâches récentes
156
+ clear: Effacer les terminées
72
157
  # The breadcrumb's View↔Edit toggle (useNavContext).
73
158
  breadcrumb:
74
159
  view: Voir
package/src/index.ts CHANGED
@@ -327,6 +327,16 @@ export {
327
327
  type LayoutMeta,
328
328
  } from './composables/useLayoutResolver'
329
329
  export { useSSEBridge, type UseSSEBridgeReturn } from './composables/useSSEBridge'
330
+ export {
331
+ useLogStream,
332
+ type LogSource,
333
+ type LogLine,
334
+ type LogPollResult,
335
+ type LogPushApi,
336
+ type LogStreamStatus,
337
+ type UseLogStreamOptions,
338
+ type UseLogStreamReturn,
339
+ } from './composables/useLogStream'
330
340
 
331
341
  // Route-state persistence (#2146) — where a route remembers what it shows.
332
342
  export {
@@ -396,6 +406,17 @@ export { useSidebarState, SIDEBAR_COLLAPSED_KEY } from './composables/useSidebar
396
406
  // COMPONENTS
397
407
  // ════════════════════════════════════════════════════════════════════════════
398
408
  export * from './components/index'
409
+ export { useTaskRunner, taskAction } from './tasks/useTaskRunner'
410
+ export {
411
+ getTaskStore,
412
+ registerTaskKind,
413
+ type TaskRun,
414
+ type TaskStatus,
415
+ type TaskKind,
416
+ type TaskFollow,
417
+ type TaskConfirm,
418
+ type RunOptions,
419
+ } from './tasks/taskStore'
399
420
 
400
421
  // ════════════════════════════════════════════════════════════════════════════
401
422
  // MODULE SYSTEM (excluding types that conflict)
@@ -3,6 +3,8 @@ import {
3
3
  h,
4
4
  defineComponent,
5
5
  ref,
6
+ shallowRef,
7
+ markRaw,
6
8
  onErrorCaptured,
7
9
  type App,
8
10
  type Component,
@@ -43,7 +45,10 @@ export function getQdadmDebugBarRef(): Ref<Component | null> {
43
45
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
46
  const w = typeof window !== 'undefined' ? (window as any) : null
45
47
  if (w && w[QDADM_DEBUG_BAR_GLOBAL_KEY]) return w[QDADM_DEBUG_BAR_GLOBAL_KEY]
46
- const r = ref<Component | null>(null)
48
+ // Shallow (#4117): a deep ref turns the component into a reactive proxy,
49
+ // and Vue warns "received a Component that was made a reactive object" on
50
+ // every render of the bar.
51
+ const r = shallowRef<Component | null>(null)
47
52
  if (w) w[QDADM_DEBUG_BAR_GLOBAL_KEY] = r
48
53
  return r
49
54
  }
@@ -56,7 +61,9 @@ export const qdadmDebugBarRef: Ref<Component | null> = getQdadmDebugBarRef()
56
61
  * constructor). Equivalent to `getQdadmDebugBarRef().value = component`.
57
62
  */
58
63
  export function setQdadmDebugBar(component: Component | null): void {
59
- getQdadmDebugBarRef().value = component
64
+ // markRaw as well: the ref on window may be a deep one left by an older
65
+ // copy of this module (HMR keeps it across versions).
66
+ getQdadmDebugBarRef().value = component ? markRaw(component) : null
60
67
  }
61
68
 
62
69
  /**
@@ -87,7 +87,17 @@ export type DeferredFactory = () => Promise<void>
87
87
  /** Navigation item configuration */
88
88
  export interface NavItem {
89
89
  section: string
90
- route: string
90
+ /** Route name — exactly one of `route` and `href`. */
91
+ route?: string
92
+ /**
93
+ * A link out of the app instead of a route (#3675) — exactly one of `route`
94
+ * and `href`. An absolute URL opens in a new tab unless `target` says otherwise.
95
+ */
96
+ href?: string
97
+ /** Where an `href` entry opens: default `_blank` for an absolute URL, same tab otherwise. */
98
+ target?: string
99
+ /** An `href` entry is shown only to whoever is granted this attribute. */
100
+ permission?: string
91
101
  label: string
92
102
  icon?: string
93
103
  exact?: boolean
@@ -74,7 +74,16 @@ export interface ModuleRoute {
74
74
  * Navigation item
75
75
  */
76
76
  export interface NavItem {
77
- route: string
77
+ route?: string
78
+ /**
79
+ * A link out of the app instead of a route (#3675) — exactly one of `route`
80
+ * and `href`. An absolute URL opens in a new tab unless `target` says otherwise.
81
+ */
82
+ href?: string
83
+ /** Where an `href` entry opens: default `_blank` for an absolute URL, same tab otherwise. */
84
+ target?: string
85
+ /** An `href` entry is shown only to whoever is granted this attribute. */
86
+ permission?: string
78
87
  label: string
79
88
  icon?: string
80
89
  entity?: string
@@ -209,6 +218,13 @@ const registry = {
209
218
  * @param item - { section, route, icon, label, exact? }
210
219
  */
211
220
  addNavItem(item: NavItemWithSection): void {
221
+ // An entry is a route or a link out, never both, never neither (#3675):
222
+ // an ambiguous one fails at boot rather than quietly in the menu.
223
+ if (!item.route === !item.href) {
224
+ throw new Error(
225
+ `[qdadm] nav item "${item.label}" in "${item.section}" needs exactly one of route and href`
226
+ )
227
+ }
212
228
  const { section, ...navItem } = item
213
229
  if (!navSections.has(section)) {
214
230
  navSections.set(section, [])
@@ -0,0 +1,188 @@
1
+ /**
2
+ * EmailPreview (#4113)
3
+ */
4
+
5
+ @use 'variables' as *;
6
+
7
+ .qd-email {
8
+ border: 1px solid var(--p-surface-200);
9
+ border-radius: $radius-sm;
10
+ overflow: hidden;
11
+ min-width: 0;
12
+ background: var(--p-surface-0);
13
+ }
14
+
15
+ .qd-email-head {
16
+ padding: 0.75rem 1rem 0.5rem;
17
+ border-bottom: 1px solid var(--p-surface-200);
18
+ }
19
+
20
+ .qd-email-subject {
21
+ margin: 0 0 0.5rem;
22
+ font-size: 1.0625rem;
23
+ font-weight: 600;
24
+ overflow-wrap: anywhere;
25
+ }
26
+
27
+ .qd-email-main {
28
+ display: grid;
29
+ grid-template-columns: max-content minmax(0, 1fr);
30
+ gap: 0.125rem 0.75rem;
31
+ margin: 0;
32
+ font-size: $font-size-sm;
33
+
34
+ dt {
35
+ color: var(--p-text-muted-color);
36
+ }
37
+
38
+ dd {
39
+ margin: 0;
40
+ overflow-wrap: anywhere;
41
+ }
42
+ }
43
+
44
+ .qd-email-headers {
45
+ margin-top: 0.5rem;
46
+ font-size: $font-size-xs;
47
+
48
+ summary {
49
+ cursor: pointer;
50
+ color: var(--p-text-muted-color);
51
+ }
52
+
53
+ table {
54
+ margin-top: 0.25rem;
55
+ border-collapse: collapse;
56
+ font-family: $font-family-mono;
57
+ }
58
+
59
+ th {
60
+ padding: 0.0625rem 1rem 0.0625rem 0;
61
+ text-align: left;
62
+ font-weight: 600;
63
+ white-space: nowrap;
64
+ vertical-align: top;
65
+ }
66
+
67
+ td {
68
+ overflow-wrap: anywhere;
69
+ }
70
+ }
71
+
72
+ .qd-email-warning {
73
+ padding: 0.375rem 1rem;
74
+ background: var(--p-orange-50);
75
+ color: var(--p-orange-800);
76
+ font-size: $font-size-sm;
77
+ }
78
+
79
+ .qd-email-tabs {
80
+ display: flex;
81
+ flex-wrap: wrap;
82
+ align-items: center;
83
+ gap: 0.25rem;
84
+ padding: 0 0.5rem;
85
+ border-bottom: 1px solid var(--p-surface-200);
86
+ font-size: $font-size-sm;
87
+ }
88
+
89
+ .qd-email-tab {
90
+ padding: 0.5rem 0.75rem;
91
+ border: none;
92
+ border-bottom: 2px solid transparent;
93
+ background: transparent;
94
+ color: var(--p-text-muted-color);
95
+ font: inherit;
96
+ cursor: pointer;
97
+
98
+ &--active {
99
+ border-bottom-color: var(--p-primary-color);
100
+ color: var(--p-text-color);
101
+ font-weight: 600;
102
+ }
103
+ }
104
+
105
+ .qd-email-spacer {
106
+ flex: 1;
107
+ }
108
+
109
+ .qd-email-blocked {
110
+ color: var(--p-text-muted-color);
111
+ font-size: $font-size-xs;
112
+ }
113
+
114
+ .qd-email-frame-wrap {
115
+ resize: vertical;
116
+ overflow: hidden;
117
+ min-height: 8rem;
118
+ background: #fff;
119
+ }
120
+
121
+ .qd-email-frame {
122
+ display: block;
123
+ width: 100%;
124
+ height: 100%;
125
+ border: 0;
126
+ }
127
+
128
+ .qd-email-text {
129
+ margin: 0;
130
+ padding: 0.75rem 1rem;
131
+ max-height: 32rem;
132
+ overflow: auto;
133
+ white-space: pre-wrap;
134
+ overflow-wrap: anywhere;
135
+ font-family: $font-family-mono;
136
+ font-size: 0.8125rem;
137
+ }
138
+
139
+ .qd-email-source {
140
+ white-space: pre;
141
+ }
142
+
143
+ .qd-email-empty {
144
+ padding: 0.75rem 1rem;
145
+ color: var(--p-text-muted-color);
146
+ font-style: italic;
147
+ }
148
+
149
+ .qd-email-attachments {
150
+ list-style: none;
151
+ margin: 0;
152
+ padding: 0.25rem 0.5rem;
153
+ border-top: 1px solid var(--p-surface-200);
154
+ }
155
+
156
+ .qd-email-attachment {
157
+ display: flex;
158
+ align-items: center;
159
+ gap: 0.5rem;
160
+ padding: 0.25rem;
161
+ font-size: $font-size-sm;
162
+ min-width: 0;
163
+ }
164
+
165
+ .qd-email-thumb {
166
+ width: 2.5rem;
167
+ height: 2.5rem;
168
+ object-fit: cover;
169
+ border-radius: $radius-sm;
170
+ border: 1px solid var(--p-surface-200);
171
+ }
172
+
173
+ .qd-email-clip {
174
+ width: 2.5rem;
175
+ text-align: center;
176
+ color: var(--p-text-muted-color);
177
+ }
178
+
179
+ .qd-email-att-name {
180
+ font-weight: 500;
181
+ overflow-wrap: anywhere;
182
+ }
183
+
184
+ .qd-email-att-meta {
185
+ flex: 1;
186
+ color: var(--p-text-muted-color);
187
+ font-size: $font-size-xs;
188
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * LogStream (#4110)
3
+ * A terminal-like panel: dark in both themes, as logs are read.
4
+ */
5
+
6
+ @use 'variables' as *;
7
+
8
+ .qd-log-stream {
9
+ --qd-log-bg: #1e1e1e;
10
+ --qd-log-fg: #d4d4d4;
11
+ --qd-log-muted: #858585;
12
+ --qd-log-mark: rgba(255, 213, 79, 0.45);
13
+ --qd-log-error-bg: rgba(241, 76, 76, 0.14);
14
+ --qd-log-current: rgba(59, 142, 234, 0.3);
15
+
16
+ position: relative;
17
+ display: flex;
18
+ flex-direction: column;
19
+ border: 1px solid var(--p-surface-300);
20
+ border-radius: $radius-sm;
21
+ overflow: hidden;
22
+ min-width: 0;
23
+ }
24
+
25
+ .qd-log-toolbar {
26
+ display: flex;
27
+ flex-wrap: wrap;
28
+ align-items: center;
29
+ gap: 0.25rem 0.5rem;
30
+ padding: 0.25rem 0.5rem;
31
+ background: var(--p-surface-50);
32
+ border-bottom: 1px solid var(--p-surface-200);
33
+ font-size: $font-size-sm;
34
+ }
35
+
36
+ .qd-log-spacer-x {
37
+ flex: 1;
38
+ }
39
+
40
+ .qd-log-status {
41
+ display: inline-flex;
42
+ align-items: center;
43
+ gap: 0.375rem;
44
+ font-weight: 500;
45
+ }
46
+
47
+ .qd-log-status-dot {
48
+ width: 0.5rem;
49
+ height: 0.5rem;
50
+ border-radius: 50%;
51
+ background: var(--p-surface-400);
52
+
53
+ .qd-log-status--streaming & {
54
+ background: var(--p-green-500);
55
+ animation: qd-log-pulse 1.2s ease-in-out infinite;
56
+ }
57
+ .qd-log-status--done & {
58
+ background: var(--p-blue-500);
59
+ }
60
+ .qd-log-status--error & {
61
+ background: var(--p-red-500);
62
+ }
63
+ }
64
+
65
+ @keyframes qd-log-pulse {
66
+ 50% {
67
+ opacity: 0.3;
68
+ }
69
+ }
70
+
71
+ .qd-log-count,
72
+ .qd-log-search-count {
73
+ color: var(--p-text-muted-color);
74
+ font-size: $font-size-xs;
75
+ font-variant-numeric: tabular-nums;
76
+ }
77
+
78
+ .qd-log-search {
79
+ display: inline-flex;
80
+ align-items: center;
81
+ gap: 0.125rem;
82
+
83
+ .p-inputtext {
84
+ width: 12rem;
85
+ max-width: 40vw;
86
+ }
87
+ }
88
+
89
+ .qd-log-viewport {
90
+ overflow: auto;
91
+ background: var(--qd-log-bg);
92
+ color: var(--qd-log-fg);
93
+ font-family: $font-family-mono;
94
+ font-size: 0.8125rem;
95
+ line-height: var(--qd-log-line-height, 20px);
96
+ outline: none;
97
+ }
98
+
99
+ .qd-log-empty {
100
+ padding: 0.75rem 1rem;
101
+ color: var(--qd-log-muted);
102
+ font-style: italic;
103
+ }
104
+
105
+ .qd-log-line {
106
+ display: flex;
107
+ height: var(--qd-log-line-height, 20px);
108
+ white-space: pre;
109
+ padding-right: 0.75rem;
110
+
111
+ &--error {
112
+ background: var(--qd-log-error-bg);
113
+ }
114
+ &--current {
115
+ background: var(--qd-log-current);
116
+ }
117
+
118
+ .qd-log-stream--wrap & {
119
+ height: auto;
120
+ min-height: var(--qd-log-line-height, 20px);
121
+ white-space: pre-wrap;
122
+ word-break: break-word;
123
+ content-visibility: auto;
124
+ contain-intrinsic-size: auto var(--qd-log-line-height, 20px);
125
+ }
126
+ }
127
+
128
+ .qd-log-n {
129
+ flex: none;
130
+ min-width: 3.5rem;
131
+ padding: 0 0.75rem 0 0.5rem;
132
+ text-align: right;
133
+ color: var(--qd-log-muted);
134
+ user-select: none;
135
+ }
136
+
137
+ .qd-log-text {
138
+ flex: 1;
139
+ min-width: 0;
140
+ }
141
+
142
+ .qd-log-ts {
143
+ color: var(--qd-log-muted);
144
+ }
145
+
146
+ .qd-log-mark {
147
+ background: var(--qd-log-mark);
148
+ border-radius: 2px;
149
+ }
150
+
151
+ .qd-log-resume {
152
+ position: absolute;
153
+ bottom: 0.75rem;
154
+ left: 50%;
155
+ transform: translateX(-50%);
156
+ display: inline-flex;
157
+ align-items: center;
158
+ gap: 0.375rem;
159
+ padding: 0.25rem 0.75rem;
160
+ border: none;
161
+ border-radius: 999px;
162
+ background: var(--p-primary-color);
163
+ color: var(--p-primary-contrast-color);
164
+ font-size: $font-size-xs;
165
+ cursor: pointer;
166
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
167
+ }