@saasicat/ui-vue 0.7.0 → 0.8.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 (40) hide show
  1. package/dist/{chunk-A3W2N26K.js → chunk-7N3CJTOA.js} +7 -227
  2. package/dist/{chunk-X5SQ5HMB.js → chunk-TBLXKSTR.js} +0 -137
  3. package/dist/{chunk-QZOSDCNP.js → chunk-TY2WYKME.js} +1 -1
  4. package/dist/client/index.cjs +112 -467
  5. package/dist/client/index.d.cts +4 -62
  6. package/dist/client/index.d.ts +4 -62
  7. package/dist/client/index.js +2 -6
  8. package/dist/index.cjs +204 -583
  9. package/dist/index.d.cts +10 -55
  10. package/dist/index.d.ts +10 -55
  11. package/dist/index.js +74 -101
  12. package/dist/{messages-7b0P8W75.d.ts → messages-BIYw32u1.d.cts} +0 -67
  13. package/dist/{messages-7b0P8W75.d.cts → messages-BIYw32u1.d.ts} +0 -67
  14. package/dist/quasar/index.cjs +0 -136
  15. package/dist/quasar/index.d.cts +2 -2
  16. package/dist/quasar/index.d.ts +2 -2
  17. package/dist/quasar/index.js +2 -2
  18. package/dist/{use-super-admin-i18n-B3v3-SWZ.d.ts → use-super-admin-i18n-CWNu_W3u.d.ts} +1 -1
  19. package/dist/{use-super-admin-i18n-DMktRVEt.d.cts → use-super-admin-i18n-DL_qI1BY.d.cts} +1 -1
  20. package/package.json +2 -2
  21. package/src/client/i18n/messages/nav.ts +0 -2
  22. package/src/client/i18n/messages/plan-versions.ts +0 -143
  23. package/src/client/index.ts +0 -2
  24. package/src/client/nav-builder.ts +9 -4
  25. package/src/index.ts +0 -1
  26. package/src/pages-standard/AdminLayout.vue +1 -1
  27. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +1 -1
  28. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +1 -1
  29. package/src/pages-standard/plan-versions/catalog-history.types.ts +37 -0
  30. package/src/pages-standard/plan-versions/format.ts +2 -38
  31. package/src/client/plan-versions-catalog.ts +0 -380
  32. package/src/pages-standard/PlanVersionsPage.vue +0 -220
  33. package/src/pages-standard/plan-versions/PlanVersionHeader.vue +0 -309
  34. package/src/pages-standard/plan-versions/PlanVersionStatusBadge.vue +0 -48
  35. package/src/pages-standard/plan-versions/PlanVersionsAudit.vue +0 -326
  36. package/src/pages-standard/plan-versions/PlanVersionsKpi.vue +0 -65
  37. package/src/pages-standard/plan-versions/PlanVersionsList.vue +0 -315
  38. package/src/pages-standard/plan-versions/PlanVersionsMatrix.vue +0 -261
  39. package/src/pages-standard/plan-versions/types.ts +0 -33
  40. package/src/vue/use-plan-versions.ts +0 -83
@@ -1,309 +0,0 @@
1
- <template>
2
- <header class="sa-pv-header">
3
- <div class="sa-pv-header__row">
4
- <div class="sa-pv-header__col">
5
- <div class="sa-pv-header__chips">
6
- <span class="sa-pv-header__label">{{ snapshot.label }}</span>
7
- <PlanVersionStatusBadge :status="snapshot.status" />
8
- <template v-if="compareSnapshot">
9
- <q-icon name="compare_arrows" size="16px" class="sa-pv-header__cmp-icon" />
10
- <span class="sa-pv-header__label sa-pv-header__label--cmp">{{
11
- compareSnapshot.label
12
- }}</span>
13
- <button
14
- type="button"
15
- class="sa-pv-header__cmp-btn"
16
- @click="emit('clearCompare')"
17
- >
18
- <q-icon name="close" size="14px" /> {{ msg.compareEnd }}
19
- </button>
20
- </template>
21
- </div>
22
- <h1 class="sa-pv-header__title">{{ snapshot.title }}</h1>
23
- <p class="sa-pv-header__desc">{{ snapshot.description }}</p>
24
-
25
- <div class="sa-pv-header__meta">
26
- <span
27
- v-if="snapshot.publishedAt"
28
- class="sa-pv-header__meta-item sa-pv-header__meta-item--ok"
29
- >
30
- <q-icon name="rocket_launch" size="14px" />
31
- {{ publishedOnLabel }}
32
- </span>
33
- <span
34
- v-if="snapshot.kind === 'drafts'"
35
- class="sa-pv-header__meta-item sa-pv-header__meta-item--warn"
36
- >
37
- <q-icon name="edit_note" size="14px" />
38
- <strong>{{ snapshot.draftCount }}</strong> {{ msg.header.openDrafts }}
39
- </span>
40
- <span
41
- v-if="snapshot.regressionCount > 0"
42
- class="sa-pv-header__meta-item sa-pv-header__meta-item--bad"
43
- >
44
- <q-icon name="trending_down" size="14px" />
45
- <strong>{{ snapshot.regressionCount }}</strong> {{ regressionLabel }}
46
- </span>
47
- </div>
48
- </div>
49
-
50
- <div class="sa-pv-header__actions">
51
- <button
52
- v-if="canPublish"
53
- type="button"
54
- class="sa-pv-header__publish"
55
- @click="emit('publish')"
56
- >
57
- <q-icon name="rocket_launch" size="16px" />
58
- {{ msg.header.startPublishFlow }}
59
- </button>
60
- </div>
61
- </div>
62
-
63
- <div class="sa-pv-header__view-row">
64
- <div
65
- class="sa-pv-header__switcher"
66
- :class="{ 'sa-pv-header__switcher--disabled': !!compareSnapshot }"
67
- >
68
- <button
69
- v-for="m in resolvedModes"
70
- :key="m.id"
71
- type="button"
72
- class="sa-pv-header__mode-btn"
73
- :class="{ 'sa-pv-header__mode-btn--active': viewMode === m.id }"
74
- @click="emit('viewChange', m.id)"
75
- >
76
- <q-icon :name="m.icon" size="14px" />
77
- {{ m.label }}
78
- </button>
79
- </div>
80
- <span v-if="compareSnapshot" class="sa-pv-header__cmp-hint">
81
- <q-icon name="compare_arrows" size="13px" />
82
- {{ msg.header.compareModeHint }}
83
- </span>
84
- </div>
85
- </header>
86
- </template>
87
-
88
- <script setup lang="ts">
89
- import { computed } from 'vue';
90
- import type { CatalogSnapshot } from '../../client/plan-versions-catalog.js';
91
- import { formatMessage } from '../../client/i18n/format.js';
92
- import { useSaMessages, useSuperAdminI18n } from '../../vue/use-super-admin-i18n.js';
93
- import { formatDate } from './format.js';
94
- import PlanVersionStatusBadge from './PlanVersionStatusBadge.vue';
95
- import type { PlanVersionViewMode } from './types.js';
96
-
97
- const props = defineProps<{
98
- snapshot: CatalogSnapshot;
99
- viewMode: PlanVersionViewMode;
100
- compareSnapshot: CatalogSnapshot | null;
101
- /** Overrides the localized default view switcher. */
102
- modes?: ReadonlyArray<{ id: PlanVersionViewMode; label: string; icon: string }>;
103
- }>();
104
-
105
- const emit = defineEmits<{
106
- (e: 'viewChange', mode: PlanVersionViewMode): void;
107
- (e: 'publish'): void;
108
- (e: 'clearCompare'): void;
109
- }>();
110
-
111
- const msg = useSaMessages('planVersions');
112
- const { locale } = useSuperAdminI18n();
113
-
114
- const canPublish = computed(
115
- () => props.snapshot.kind === 'drafts' && props.snapshot.draftCount > 0,
116
- );
117
-
118
- const publishedOnLabel = computed(() =>
119
- formatMessage(msg.value.header.publishedOn, {
120
- date: formatDate(props.snapshot.publishedAt ?? undefined, locale.value),
121
- }),
122
- );
123
-
124
- const regressionLabel = computed(() =>
125
- props.snapshot.regressionCount === 1
126
- ? msg.value.header.regressionOne
127
- : msg.value.header.regressionMany,
128
- );
129
-
130
- const resolvedModes = computed(
131
- () =>
132
- props.modes ?? [
133
- { id: 'list' as const, label: msg.value.header.modeList, icon: 'list' },
134
- { id: 'matrix' as const, label: msg.value.header.modeMatrix, icon: 'grid_view' },
135
- { id: 'audit' as const, label: msg.value.header.modeAudit, icon: 'history' },
136
- ],
137
- );
138
- </script>
139
-
140
- <style scoped>
141
- .sa-pv-header {
142
- padding: 20px 28px 18px;
143
- border-bottom: 1px solid var(--sa-border, #e2e8f0);
144
- background: var(--sa-bg-surface, #ffffff);
145
- }
146
- .sa-pv-header__row {
147
- display: flex;
148
- align-items: flex-start;
149
- gap: 20px;
150
- }
151
- .sa-pv-header__col {
152
- flex: 1;
153
- min-width: 0;
154
- }
155
- .sa-pv-header__chips {
156
- display: flex;
157
- align-items: center;
158
- gap: 10px;
159
- margin-bottom: 6px;
160
- flex-wrap: wrap;
161
- }
162
- .sa-pv-header__label {
163
- font-family: var(--sa-font-mono, ui-monospace, monospace);
164
- font-size: 13px;
165
- font-weight: 700;
166
- color: var(--sa-heading, #0f172a);
167
- background: #f1f5f9;
168
- padding: 3px 10px;
169
- border-radius: 6px;
170
- }
171
- .sa-pv-header__label--cmp {
172
- background: #fef3c7;
173
- }
174
- .sa-pv-header__cmp-icon {
175
- color: var(--sa-muted, #64748b);
176
- }
177
- .sa-pv-header__cmp-btn {
178
- background: transparent;
179
- border: none;
180
- cursor: pointer;
181
- color: var(--sa-muted, #64748b);
182
- font-size: 11px;
183
- font-weight: 600;
184
- display: inline-flex;
185
- align-items: center;
186
- gap: 4px;
187
- }
188
-
189
- .sa-pv-header__title {
190
- margin: 0;
191
- font-family: var(--sa-font-head, system-ui, sans-serif);
192
- font-weight: 700;
193
- font-size: 22px;
194
- color: var(--sa-heading, #0f172a);
195
- letter-spacing: -0.01em;
196
- }
197
- .sa-pv-header__desc {
198
- margin: 6px 0 0;
199
- color: var(--sa-muted-dark, #475569);
200
- font-size: 13.5px;
201
- line-height: 1.55;
202
- max-width: 760px;
203
- }
204
-
205
- .sa-pv-header__meta {
206
- display: flex;
207
- flex-wrap: wrap;
208
- gap: 16px;
209
- margin-top: 14px;
210
- font-size: 12px;
211
- color: var(--sa-muted, #64748b);
212
- }
213
- .sa-pv-header__meta-item {
214
- display: inline-flex;
215
- align-items: center;
216
- gap: 5px;
217
- }
218
- .sa-pv-header__meta-item strong {
219
- color: var(--sa-body, #1e293b);
220
- font-weight: 600;
221
- }
222
- .sa-pv-header__meta-item--ok :deep(.q-icon) {
223
- color: var(--sa-positive, #047857);
224
- }
225
- .sa-pv-header__meta-item--warn :deep(.q-icon) {
226
- color: var(--sa-warning, #b45309);
227
- }
228
- .sa-pv-header__meta-item--bad :deep(.q-icon) {
229
- color: var(--sa-negative, #dc2626);
230
- }
231
- .sa-pv-header__meta-item--bad strong {
232
- color: var(--sa-negative, #dc2626);
233
- }
234
-
235
- .sa-pv-header__actions {
236
- display: flex;
237
- gap: 8px;
238
- flex-shrink: 0;
239
- }
240
- .sa-pv-header__publish {
241
- background: var(--sa-primary, #3f6bff);
242
- color: #fff;
243
- border: none;
244
- border-radius: 8px;
245
- padding: 10px 18px;
246
- font-size: 13px;
247
- font-weight: 600;
248
- cursor: pointer;
249
- display: inline-flex;
250
- align-items: center;
251
- gap: 6px;
252
- box-shadow: 0 1px 3px rgba(63, 107, 255, 0.3);
253
- }
254
-
255
- .sa-pv-header__view-row {
256
- display: flex;
257
- align-items: center;
258
- gap: 12px;
259
- margin-top: 18px;
260
- flex-wrap: wrap;
261
- }
262
- .sa-pv-header__switcher {
263
- display: inline-flex;
264
- background: #f1f5f9;
265
- padding: 3px;
266
- border-radius: 9px;
267
- }
268
- .sa-pv-header__switcher--disabled {
269
- opacity: 0.5;
270
- pointer-events: none;
271
- }
272
- .sa-pv-header__mode-btn {
273
- background: transparent;
274
- color: var(--sa-body, #1e293b);
275
- border: none;
276
- border-radius: 7px;
277
- padding: 6px 12px;
278
- font-size: 12.5px;
279
- font-weight: 600;
280
- cursor: pointer;
281
- display: inline-flex;
282
- align-items: center;
283
- gap: 5px;
284
- }
285
- .sa-pv-header__mode-btn :deep(.q-icon) {
286
- color: var(--sa-muted, #64748b);
287
- }
288
- .sa-pv-header__mode-btn--active {
289
- background: #fff;
290
- color: var(--sa-primary, #3f6bff);
291
- box-shadow: 0 1px 3px rgba(15, 23, 42, 0.1);
292
- }
293
- .sa-pv-header__mode-btn--active :deep(.q-icon) {
294
- color: var(--sa-primary, #3f6bff);
295
- }
296
-
297
- .sa-pv-header__cmp-hint {
298
- font-size: 11.5px;
299
- font-weight: 600;
300
- color: var(--sa-warning, #b45309);
301
- display: inline-flex;
302
- align-items: center;
303
- gap: 4px;
304
- background: var(--sa-warning-soft, rgba(245, 158, 11, 0.1));
305
- padding: 4px 10px;
306
- border-radius: 6px;
307
- border: 1px solid var(--sa-amber-border, rgba(245, 158, 11, 0.3));
308
- }
309
- </style>
@@ -1,48 +0,0 @@
1
- <template>
2
- <span class="sa-pv-status-badge" :class="`sa-pv-status-badge--${status.toLowerCase()}`">
3
- <q-icon :name="cfg.icon" size="12px" />
4
- {{ cfg.label }}
5
- </span>
6
- </template>
7
-
8
- <script setup lang="ts">
9
- import { computed } from 'vue';
10
-
11
- type Status = 'DRAFT' | 'ACTIVE' | 'ARCHIVED';
12
- const props = defineProps<{ status: Status }>();
13
-
14
- const STATUS_CFG: Record<Status, { label: string; icon: string }> = {
15
- DRAFT: { label: 'DRAFT', icon: 'edit_note' },
16
- ACTIVE: { label: 'AKTIV', icon: 'check_circle' },
17
- ARCHIVED: { label: 'ARCHIV', icon: 'archive' },
18
- };
19
- const cfg = computed(() => STATUS_CFG[props.status]);
20
- </script>
21
-
22
- <style scoped>
23
- .sa-pv-status-badge {
24
- display: inline-flex;
25
- align-items: center;
26
- gap: 4px;
27
- padding: 3px 9px;
28
- border-radius: 6px;
29
- font-size: 10.5px;
30
- font-weight: 700;
31
- letter-spacing: 0.06em;
32
- }
33
- .sa-pv-status-badge--draft {
34
- background: #fef3c7;
35
- color: #92400e;
36
- }
37
- .sa-pv-status-badge--active {
38
- background: #d1fae5;
39
- color: #065f46;
40
- }
41
- .sa-pv-status-badge--archived {
42
- background: #f1f5f9;
43
- color: #64748b;
44
- }
45
- .sa-pv-status-badge :deep(.q-icon) {
46
- color: inherit;
47
- }
48
- </style>
@@ -1,326 +0,0 @@
1
- <template>
2
- <div class="sa-pv-audit">
3
- <div class="sa-pv-audit__card">
4
- <header class="sa-pv-audit__head">
5
- <q-icon name="history" size="18px" class="sa-pv-audit__head-icon" />
6
- <h3>{{ msg.audit.title }}</h3>
7
- <span class="sa-pv-audit__count">{{ eventCountLabel }}</span>
8
- <q-space />
9
- <button v-if="!loading" type="button" class="sa-pv-audit__refresh" @click="reload">
10
- <q-icon name="refresh" size="14px" /> {{ common.reload }}
11
- </button>
12
- </header>
13
- <div v-if="loading" class="sa-pv-audit__loading">
14
- <q-spinner size="20px" /> {{ msg.audit.loading }}
15
- </div>
16
- <div v-else-if="events.length === 0" class="sa-pv-audit__empty">
17
- {{ msg.audit.empty }}
18
- </div>
19
- <div v-else class="sa-pv-audit__list">
20
- <div
21
- v-for="(e, i) in events"
22
- :key="e.id"
23
- class="sa-pv-audit__item"
24
- :class="{ 'sa-pv-audit__item--last': i === events.length - 1 }"
25
- >
26
- <div
27
- class="sa-pv-audit__icon"
28
- :style="{ background: `${meta(e.action).color}14` }"
29
- >
30
- <q-icon
31
- :name="meta(e.action).icon"
32
- size="17px"
33
- :style="{ color: meta(e.action).color }"
34
- />
35
- </div>
36
- <div class="sa-pv-audit__body">
37
- <div class="sa-pv-audit__title-row">
38
- <span class="sa-pv-audit__title">{{ meta(e.action).label }}</span>
39
- <span class="sa-pv-audit__entity">{{ entityLabel(e) }}</span>
40
- </div>
41
- <div class="sa-pv-audit__detail">{{ detail(e) }}</div>
42
- <div class="sa-pv-audit__meta">
43
- <span
44
- ><q-icon name="schedule" size="12px" />
45
- {{ formatTimestamp(e.createdAt, locale) }}</span
46
- >
47
- <span v-if="actorEmail(e)">
48
- <q-icon name="person" size="12px" /> {{ actorEmail(e) }}
49
- </span>
50
- </div>
51
- </div>
52
- </div>
53
- </div>
54
- </div>
55
- </div>
56
- </template>
57
-
58
- <script setup lang="ts">
59
- import { computed, onMounted, ref, watch } from 'vue';
60
- import type { CatalogSnapshot } from '../../client/plan-versions-catalog.js';
61
- import { formatMessage } from '../../client/i18n/format.js';
62
- import { useSaMessages, useSuperAdminI18n } from '../../vue/use-super-admin-i18n.js';
63
- import { formatTimestamp } from './format.js';
64
-
65
- // PlanVersionsAudit — data-agnostic. Consumers pass in a `loadAudit` loader
66
- // that supplies events; the component filters them to match the current
67
- // snapshot (drafts/active = everything ≤ 50, historical = ≤ asOf).
68
- //
69
- // The action meta map can be extended per app so that additional actions
70
- // (e.g. app-specific audit keys) get their own icons and labels.
71
-
72
- interface AuditRow {
73
- id: string;
74
- createdAt: string;
75
- action: string;
76
- entity: string;
77
- entityId: string;
78
- changes: Record<string, unknown> | null;
79
- /** Legacy style: nested user object with email. */
80
- user?: { email: string; firstName?: string; lastName?: string } | null;
81
- /** Platform style: flat userEmail field. */
82
- userEmail?: string | null;
83
- }
84
-
85
- interface ActionMeta {
86
- icon: string;
87
- color: string;
88
- label: string;
89
- }
90
-
91
- const props = withDefaults(
92
- defineProps<{
93
- snapshot: CatalogSnapshot;
94
- loadAudit: () => Promise<AuditRow[]>;
95
- actionMeta?: Record<string, ActionMeta>;
96
- }>(),
97
- {
98
- actionMeta: () => ({}),
99
- },
100
- );
101
-
102
- const loading = ref(false);
103
- const events = ref<AuditRow[]>([]);
104
-
105
- const msg = useSaMessages('planVersions');
106
- const common = useSaMessages('common');
107
- const { locale } = useSuperAdminI18n();
108
-
109
- const eventCountLabel = computed(() =>
110
- formatMessage(msg.value.audit.eventCount, { count: events.value.length }),
111
- );
112
-
113
- const ACTION_ICONS: Record<string, { icon: string; color: string }> = {
114
- PLAN_VERSION_PUBLISH: { icon: 'rocket_launch', color: '#047857' },
115
- PLAN_VERSION_DRAFT_CREATE: { icon: 'edit_note', color: '#b45309' },
116
- PLAN_VERSION_DRAFT_UPDATE: { icon: 'edit', color: '#3f6bff' },
117
- PLAN_VERSION_DRAFT_DELETE: { icon: 'delete', color: '#dc2626' },
118
- PLAN_CATALOG_UPDATE: { icon: 'rocket_launch', color: '#047857' },
119
- };
120
-
121
- const defaultActionMeta = computed<Record<string, ActionMeta>>(() => ({
122
- PLAN_VERSION_PUBLISH: {
123
- ...ACTION_ICONS.PLAN_VERSION_PUBLISH,
124
- label: msg.value.audit.actionPlanPublished,
125
- },
126
- PLAN_VERSION_DRAFT_CREATE: {
127
- ...ACTION_ICONS.PLAN_VERSION_DRAFT_CREATE,
128
- label: msg.value.audit.actionPlanDraftCreated,
129
- },
130
- PLAN_VERSION_DRAFT_UPDATE: {
131
- ...ACTION_ICONS.PLAN_VERSION_DRAFT_UPDATE,
132
- label: msg.value.audit.actionPlanDraftUpdated,
133
- },
134
- PLAN_VERSION_DRAFT_DELETE: {
135
- ...ACTION_ICONS.PLAN_VERSION_DRAFT_DELETE,
136
- label: msg.value.audit.actionPlanDraftDeleted,
137
- },
138
- PLAN_CATALOG_UPDATE: {
139
- ...ACTION_ICONS.PLAN_CATALOG_UPDATE,
140
- label: msg.value.audit.actionCatalogPublished,
141
- },
142
- }));
143
-
144
- function meta(action: string): ActionMeta {
145
- return (
146
- props.actionMeta[action] ??
147
- defaultActionMeta.value[action] ?? {
148
- icon: 'circle',
149
- color: '#64748b',
150
- label: action,
151
- }
152
- );
153
- }
154
-
155
- function entityLabel(e: AuditRow): string {
156
- const c = (e.changes ?? {}) as {
157
- planId?: string;
158
- key?: string;
159
- featureKey?: string;
160
- version?: number;
161
- };
162
- const versionPart = c.version != null ? ` v${c.version}` : '';
163
- if (c.planId) return `${c.planId}${versionPart}`;
164
- if (c.key) return `${c.key}${versionPart}`;
165
- if (c.featureKey) return `${c.featureKey}${versionPart}`;
166
- return e.entity;
167
- }
168
-
169
- function detail(e: AuditRow): string {
170
- const c = (e.changes ?? {}) as {
171
- changeNote?: string;
172
- nonRegressive?: boolean;
173
- changes?: unknown;
174
- };
175
- if (c.changeNote) return c.changeNote;
176
- if (c.nonRegressive === false) return msg.value.audit.detailRegression;
177
- if (Array.isArray(c.changes)) {
178
- return formatMessage(msg.value.audit.detailFieldsChanged, { count: c.changes.length });
179
- }
180
- return e.action;
181
- }
182
-
183
- function actorEmail(e: AuditRow): string | null {
184
- return e.user?.email ?? e.userEmail ?? null;
185
- }
186
-
187
- onMounted(reload);
188
- watch(() => props.snapshot.id, reload);
189
-
190
- async function reload(): Promise<void> {
191
- loading.value = true;
192
- try {
193
- const merged = await props.loadAudit();
194
- events.value = filterForSnapshot(merged);
195
- } finally {
196
- loading.value = false;
197
- }
198
- }
199
-
200
- function filterForSnapshot(rows: AuditRow[]): AuditRow[] {
201
- if (props.snapshot.kind !== 'historical' || !props.snapshot.asOf) return rows.slice(0, 50);
202
- const asOfMs = Date.parse(props.snapshot.asOf);
203
- return rows.filter((r) => Date.parse(r.createdAt) <= asOfMs).slice(0, 50);
204
- }
205
- </script>
206
-
207
- <style scoped>
208
- .sa-pv-audit {
209
- padding: 20px 28px;
210
- }
211
- .sa-pv-audit__card {
212
- background: #fff;
213
- border: 1px solid var(--sa-border, #e2e8f0);
214
- border-radius: 12px;
215
- overflow: hidden;
216
- }
217
- .sa-pv-audit__head {
218
- padding: 14px 18px;
219
- border-bottom: 1px solid var(--sa-border, #e2e8f0);
220
- display: flex;
221
- align-items: center;
222
- gap: 8px;
223
- }
224
- .sa-pv-audit__head h3 {
225
- margin: 0;
226
- font-family: var(--sa-font-head, system-ui, sans-serif);
227
- font-size: 15px;
228
- font-weight: 700;
229
- color: var(--sa-heading, #0f172a);
230
- }
231
- .sa-pv-audit__head-icon {
232
- color: var(--sa-primary, #3f6bff);
233
- }
234
- .sa-pv-audit__count {
235
- font-size: 12px;
236
- color: var(--sa-muted, #64748b);
237
- }
238
- .sa-pv-audit__refresh {
239
- background: #fff;
240
- border: 1px solid var(--sa-border, #e2e8f0);
241
- border-radius: 6px;
242
- padding: 4px 10px;
243
- font-size: 11.5px;
244
- font-weight: 600;
245
- color: var(--sa-body, #1e293b);
246
- cursor: pointer;
247
- display: inline-flex;
248
- align-items: center;
249
- gap: 4px;
250
- }
251
-
252
- .sa-pv-audit__loading,
253
- .sa-pv-audit__empty {
254
- padding: 24px;
255
- color: var(--sa-muted, #64748b);
256
- display: flex;
257
- align-items: center;
258
- gap: 12px;
259
- }
260
-
261
- .sa-pv-audit__list {
262
- padding: 8px 0;
263
- }
264
- .sa-pv-audit__item {
265
- display: flex;
266
- align-items: flex-start;
267
- gap: 14px;
268
- padding: 12px 18px;
269
- border-bottom: 1px solid var(--sa-border-soft, #f1f5f9);
270
- }
271
- .sa-pv-audit__item--last {
272
- border-bottom: none;
273
- }
274
-
275
- .sa-pv-audit__icon {
276
- width: 32px;
277
- height: 32px;
278
- border-radius: 8px;
279
- display: flex;
280
- align-items: center;
281
- justify-content: center;
282
- flex-shrink: 0;
283
- }
284
-
285
- .sa-pv-audit__body {
286
- flex: 1;
287
- min-width: 0;
288
- }
289
- .sa-pv-audit__title-row {
290
- display: flex;
291
- align-items: center;
292
- gap: 8px;
293
- }
294
- .sa-pv-audit__title {
295
- font-size: 13px;
296
- font-weight: 600;
297
- color: var(--sa-heading, #0f172a);
298
- }
299
- .sa-pv-audit__entity {
300
- font-family: var(--sa-font-mono, ui-monospace, monospace);
301
- font-size: 10.5px;
302
- font-weight: 600;
303
- color: var(--sa-muted, #64748b);
304
- background: #f1f5f9;
305
- padding: 1px 6px;
306
- border-radius: 4px;
307
- }
308
- .sa-pv-audit__detail {
309
- font-size: 12.5px;
310
- color: var(--sa-body, #1e293b);
311
- margin-top: 3px;
312
- line-height: 1.4;
313
- }
314
- .sa-pv-audit__meta {
315
- font-size: 11px;
316
- color: var(--sa-muted, #64748b);
317
- margin-top: 4px;
318
- display: flex;
319
- gap: 10px;
320
- }
321
- .sa-pv-audit__meta span {
322
- display: inline-flex;
323
- align-items: center;
324
- gap: 4px;
325
- }
326
- </style>