nuxt-devtools-observatory 0.1.28 → 0.1.31
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/README.md +93 -11
- package/client/.env +2 -1
- package/client/.env.example +2 -1
- package/client/dist/assets/index-BuMXDBO9.js +17 -0
- package/client/dist/assets/index-CwcspZ6w.css +1 -0
- package/client/dist/index.html +2 -2
- package/client/src/App.vue +4 -0
- package/client/src/components/Flamegraph.vue +443 -0
- package/client/src/components/SpanInspector.vue +446 -0
- package/client/src/components/TraceFilter.vue +344 -0
- package/client/src/components/WaterfallView.vue +443 -0
- package/client/src/composables/useResizablePane.ts +65 -0
- package/client/src/composables/useTraceFilter.ts +164 -0
- package/client/src/stores/observatory.ts +16 -2
- package/client/src/style.css +203 -28
- package/client/src/views/ComposableTracker.vue +324 -259
- package/client/src/views/FetchDashboard.vue +104 -133
- package/client/src/views/ProvideInjectGraph.vue +99 -109
- package/client/src/views/RenderHeatmap.vue +191 -147
- package/client/src/views/TraceViewer.vue +599 -0
- package/client/src/views/TransitionTimeline.vue +167 -137
- package/client/tsconfig.json +3 -1
- package/client/vite.config.ts +8 -0
- package/dist/module.d.mts +5 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +186 -200
- package/dist/runtime/composables/render-registry.js +66 -110
- package/dist/runtime/composables/transition-registry.js +103 -28
- package/dist/runtime/instrumentation/asyncData.d.ts +9 -0
- package/dist/runtime/instrumentation/asyncData.js +49 -0
- package/dist/runtime/instrumentation/component.d.ts +2 -0
- package/dist/runtime/instrumentation/component.js +126 -0
- package/dist/runtime/instrumentation/fetch.d.ts +2 -0
- package/dist/runtime/instrumentation/fetch.js +89 -0
- package/dist/runtime/instrumentation/route.d.ts +6 -0
- package/dist/runtime/instrumentation/route.js +41 -0
- package/dist/runtime/plugin.js +39 -3
- package/dist/runtime/tracing/context.d.ts +9 -0
- package/dist/runtime/tracing/context.js +15 -0
- package/dist/runtime/tracing/trace.d.ts +25 -0
- package/dist/runtime/tracing/trace.js +0 -0
- package/dist/runtime/tracing/traceStore.d.ts +39 -0
- package/dist/runtime/tracing/traceStore.js +101 -0
- package/dist/runtime/tracing/tracing.d.ts +27 -0
- package/dist/runtime/tracing/tracing.js +48 -0
- package/package.json +9 -6
- package/client/dist/assets/index-DXCGQOSF.js +0 -17
- package/client/dist/assets/index-htI4WwBU.css +0 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed } from 'vue'
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import { useResizablePane } from '@observatory-client/composables/useResizablePane'
|
|
4
|
+
import { useObservatoryData } from '@observatory-client/stores/observatory'
|
|
5
|
+
import type { FetchEntry } from '@observatory/types/snapshot'
|
|
5
6
|
|
|
6
7
|
type FetchViewEntry = FetchEntry & { startOffset: number }
|
|
7
8
|
|
|
8
9
|
const { fetch, connected } = useObservatoryData()
|
|
10
|
+
const { paneWidth: detailWidth, onHandleMouseDown } = useResizablePane(280, 'observatory:fetch:detailWidth')
|
|
9
11
|
|
|
10
12
|
const filter = ref<string>('all')
|
|
11
13
|
const search = ref('')
|
|
@@ -94,6 +96,7 @@ function barWidth(entry: FetchViewEntry) {
|
|
|
94
96
|
// collapse all bars to a dot while waiting.
|
|
95
97
|
const completedMs = entries.value.filter((e) => e.ms != null).map((e) => e.ms!)
|
|
96
98
|
const maxMs = completedMs.length > 0 ? Math.max(...completedMs, 1) : 1
|
|
99
|
+
|
|
97
100
|
return entry.ms != null ? Math.max(4, Math.round((entry.ms / maxMs) * 100)) : 4
|
|
98
101
|
}
|
|
99
102
|
|
|
@@ -103,6 +106,7 @@ function barWidth(entry: FetchViewEntry) {
|
|
|
103
106
|
function waterfallScale() {
|
|
104
107
|
const completed = entries.value.filter((e) => e.ms != null)
|
|
105
108
|
const maxEnd = completed.length > 0 ? Math.max(...completed.map((e) => e.startOffset + e.ms!), 1) : 1
|
|
109
|
+
|
|
106
110
|
return maxEnd
|
|
107
111
|
}
|
|
108
112
|
|
|
@@ -117,8 +121,10 @@ function wfWidth(entry: FetchViewEntry) {
|
|
|
117
121
|
// a zero-width invisible bar.
|
|
118
122
|
return 2
|
|
119
123
|
}
|
|
124
|
+
|
|
120
125
|
const scale = waterfallScale()
|
|
121
126
|
const left = wfLeft(entry)
|
|
127
|
+
|
|
122
128
|
// Clamp so bar + left never exceeds 100%
|
|
123
129
|
return Math.min(100 - left, Math.max(2, Math.round((entry.ms / scale) * 100)))
|
|
124
130
|
}
|
|
@@ -133,36 +139,41 @@ function formatSize(bytes: number) {
|
|
|
133
139
|
</script>
|
|
134
140
|
|
|
135
141
|
<template>
|
|
136
|
-
<div class="view">
|
|
137
|
-
<div class="stats-row">
|
|
142
|
+
<div class="fetch-dashboard tracker-view">
|
|
143
|
+
<div class="fetch-dashboard__stats tracker-stats-row">
|
|
138
144
|
<div class="stat-card">
|
|
139
145
|
<div class="stat-label">total</div>
|
|
140
146
|
<div class="stat-val">{{ entries.length }}</div>
|
|
141
147
|
</div>
|
|
142
148
|
<div class="stat-card">
|
|
143
149
|
<div class="stat-label">success</div>
|
|
144
|
-
<div class="stat-val
|
|
150
|
+
<div class="stat-val stat-val--ok">{{ counts.ok }}</div>
|
|
145
151
|
</div>
|
|
146
152
|
<div class="stat-card">
|
|
147
153
|
<div class="stat-label">pending</div>
|
|
148
|
-
<div class="stat-val
|
|
154
|
+
<div class="stat-val stat-val--pending">{{ counts.pending }}</div>
|
|
149
155
|
</div>
|
|
150
156
|
<div class="stat-card">
|
|
151
157
|
<div class="stat-label">error</div>
|
|
152
|
-
<div class="stat-val
|
|
158
|
+
<div class="stat-val stat-val--error">{{ counts.error }}</div>
|
|
153
159
|
</div>
|
|
154
160
|
</div>
|
|
155
161
|
|
|
156
|
-
<div class="toolbar">
|
|
162
|
+
<div class="fetch-dashboard__toolbar tracker-toolbar">
|
|
157
163
|
<button :class="{ active: filter === 'all' }" @click="filter = 'all'">all</button>
|
|
158
164
|
<button :class="{ 'danger-active': filter === 'error' }" @click="filter = 'error'">errors</button>
|
|
159
165
|
<button :class="{ active: filter === 'pending' }" @click="filter = 'pending'">pending</button>
|
|
160
166
|
<button :class="{ active: filter === 'cached' }" @click="filter = 'cached'">cached</button>
|
|
161
|
-
<input
|
|
167
|
+
<input
|
|
168
|
+
v-model="search"
|
|
169
|
+
type="search"
|
|
170
|
+
class="fetch-dashboard__search tracker-toolbar__spacer"
|
|
171
|
+
placeholder="search key or url…"
|
|
172
|
+
/>
|
|
162
173
|
</div>
|
|
163
174
|
|
|
164
|
-
<div class="split">
|
|
165
|
-
<div class="table-wrap">
|
|
175
|
+
<div class="fetch-dashboard__split tracker-split">
|
|
176
|
+
<div class="fetch-dashboard__table tracker-table-wrap">
|
|
166
177
|
<table class="data-table">
|
|
167
178
|
<thead>
|
|
168
179
|
<tr>
|
|
@@ -172,7 +183,7 @@ function formatSize(bytes: number) {
|
|
|
172
183
|
<th>origin</th>
|
|
173
184
|
<th>size</th>
|
|
174
185
|
<th>time</th>
|
|
175
|
-
<th
|
|
186
|
+
<th class="fetch-dashboard__bar-column">bar</th>
|
|
176
187
|
</tr>
|
|
177
188
|
</thead>
|
|
178
189
|
<tbody>
|
|
@@ -183,21 +194,10 @@ function formatSize(bytes: number) {
|
|
|
183
194
|
@click="selectedId = entry.id"
|
|
184
195
|
>
|
|
185
196
|
<td>
|
|
186
|
-
<span class="mono
|
|
197
|
+
<span class="fetch-dashboard__key mono tracker-mono-secondary">{{ entry.key }}</span>
|
|
187
198
|
</td>
|
|
188
199
|
<td>
|
|
189
|
-
<span
|
|
190
|
-
class="mono"
|
|
191
|
-
style="
|
|
192
|
-
font-size: 11px;
|
|
193
|
-
max-width: 200px;
|
|
194
|
-
display: block;
|
|
195
|
-
overflow: hidden;
|
|
196
|
-
text-overflow: ellipsis;
|
|
197
|
-
white-space: nowrap;
|
|
198
|
-
"
|
|
199
|
-
:title="entry.url"
|
|
200
|
-
>
|
|
200
|
+
<span class="fetch-dashboard__url mono tracker-mono-secondary tracker-truncate" :title="entry.url">
|
|
201
201
|
{{ entry.url }}
|
|
202
202
|
</span>
|
|
203
203
|
</td>
|
|
@@ -210,20 +210,19 @@ function formatSize(bytes: number) {
|
|
|
210
210
|
<td class="muted text-sm">{{ entry.size ? formatSize(entry.size) : '—' }}</td>
|
|
211
211
|
<td class="mono text-sm">{{ entry.ms != null ? `${entry.ms}ms` : '—' }}</td>
|
|
212
212
|
<td>
|
|
213
|
-
<div
|
|
213
|
+
<div class="fetch-dashboard__bar-track tracker-progress-bar">
|
|
214
214
|
<div
|
|
215
|
+
class="fetch-dashboard__bar-fill tracker-progress-bar__fill"
|
|
215
216
|
:style="{
|
|
216
217
|
width: `${barWidth(entry)}%`,
|
|
217
218
|
background: barColor(entry.status),
|
|
218
|
-
height: '100%',
|
|
219
|
-
borderRadius: '2px',
|
|
220
219
|
}"
|
|
221
220
|
></div>
|
|
222
221
|
</div>
|
|
223
222
|
</td>
|
|
224
223
|
</tr>
|
|
225
224
|
<tr v-if="!filtered.length">
|
|
226
|
-
<td colspan="7"
|
|
225
|
+
<td colspan="7" class="tracker-empty-cell">
|
|
227
226
|
{{ connected ? 'No fetches recorded yet.' : 'Waiting for connection to the Nuxt app…' }}
|
|
228
227
|
</td>
|
|
229
228
|
</tr>
|
|
@@ -231,49 +230,48 @@ function formatSize(bytes: number) {
|
|
|
231
230
|
</table>
|
|
232
231
|
</div>
|
|
233
232
|
|
|
234
|
-
<div v-if="selected" class="
|
|
235
|
-
|
|
236
|
-
|
|
233
|
+
<div v-if="selected" class="tracker-resize-handle" @mousedown="onHandleMouseDown" />
|
|
234
|
+
|
|
235
|
+
<div v-if="selected" class="fetch-dashboard__detail tracker-detail-panel" :style="{ width: detailWidth + 'px' }">
|
|
236
|
+
<div class="fetch-dashboard__detail-header">
|
|
237
|
+
<span class="fetch-dashboard__detail-title mono bold">{{ selected.key }}</span>
|
|
237
238
|
<div class="flex gap-2">
|
|
238
239
|
<button @click="selectedId = null">×</button>
|
|
239
240
|
</div>
|
|
240
241
|
</div>
|
|
241
242
|
|
|
242
|
-
<div class="
|
|
243
|
+
<div class="fetch-dashboard__meta-grid">
|
|
243
244
|
<template v-for="[key, value] in metaRows" :key="key">
|
|
244
245
|
<span class="muted text-sm">{{ key }}</span>
|
|
245
|
-
<span class="mono text-sm"
|
|
246
|
+
<span class="fetch-dashboard__meta-value mono text-sm">{{ value }}</span>
|
|
246
247
|
</template>
|
|
247
248
|
</div>
|
|
248
249
|
|
|
249
|
-
<div class="section-label">payload</div>
|
|
250
|
-
<pre class="
|
|
250
|
+
<div class="tracker-section-label fetch-dashboard__section-label">payload</div>
|
|
251
|
+
<pre class="fetch-dashboard__payload-box">{{ payloadStr }}</pre>
|
|
251
252
|
|
|
252
|
-
<div class="section-label
|
|
253
|
+
<div class="tracker-section-label fetch-dashboard__section-label fetch-dashboard__section-label--source">source</div>
|
|
253
254
|
<div class="mono text-sm muted">{{ selected.file }}:{{ selected.line }}</div>
|
|
254
255
|
</div>
|
|
255
|
-
<div v-else class="detail-empty">select a call to inspect</div>
|
|
256
|
+
<div v-else class="tracker-detail-empty">select a call to inspect</div>
|
|
256
257
|
</div>
|
|
257
258
|
|
|
258
|
-
<div class="
|
|
259
|
-
<div class="
|
|
260
|
-
<div class="section-label
|
|
259
|
+
<div class="fetch-dashboard__waterfall">
|
|
260
|
+
<div class="fetch-dashboard__waterfall-header">
|
|
261
|
+
<div class="tracker-section-label fetch-dashboard__waterfall-label">waterfall</div>
|
|
261
262
|
<button :class="{ active: waterfallOpen }" @click="waterfallOpen = !waterfallOpen">
|
|
262
263
|
{{ waterfallOpen ? 'hide' : 'show' }}
|
|
263
264
|
</button>
|
|
264
265
|
</div>
|
|
265
266
|
|
|
266
|
-
<div v-if="waterfallOpen" class="
|
|
267
|
-
<div v-for="entry in entries" :key="entry.id" class="
|
|
268
|
-
<span
|
|
269
|
-
class="mono muted text-sm"
|
|
270
|
-
style="width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 0"
|
|
271
|
-
>
|
|
267
|
+
<div v-if="waterfallOpen" class="fetch-dashboard__waterfall-body">
|
|
268
|
+
<div v-for="entry in entries" :key="entry.id" class="fetch-dashboard__waterfall-row">
|
|
269
|
+
<span class="fetch-dashboard__waterfall-key mono muted text-sm">
|
|
272
270
|
{{ entry.key }}
|
|
273
271
|
</span>
|
|
274
|
-
<div class="
|
|
272
|
+
<div class="fetch-dashboard__waterfall-track">
|
|
275
273
|
<div
|
|
276
|
-
class="
|
|
274
|
+
class="fetch-dashboard__waterfall-bar"
|
|
277
275
|
:style="{
|
|
278
276
|
left: `${wfLeft(entry)}%`,
|
|
279
277
|
width: `${Math.max(2, wfWidth(entry))}%`,
|
|
@@ -281,7 +279,7 @@ function formatSize(bytes: number) {
|
|
|
281
279
|
}"
|
|
282
280
|
></div>
|
|
283
281
|
</div>
|
|
284
|
-
<span class="mono muted text-sm"
|
|
282
|
+
<span class="fetch-dashboard__waterfall-time mono muted text-sm">
|
|
285
283
|
{{ entry.ms != null ? `${entry.ms}ms` : '—' }}
|
|
286
284
|
</span>
|
|
287
285
|
</div>
|
|
@@ -291,145 +289,118 @@ function formatSize(bytes: number) {
|
|
|
291
289
|
</template>
|
|
292
290
|
|
|
293
291
|
<style scoped>
|
|
294
|
-
.
|
|
295
|
-
|
|
296
|
-
flex-direction: column;
|
|
297
|
-
height: 100%;
|
|
298
|
-
overflow: hidden;
|
|
299
|
-
padding: 12px;
|
|
300
|
-
gap: 10px;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
.stats-row {
|
|
304
|
-
display: grid;
|
|
305
|
-
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
306
|
-
gap: 8px;
|
|
307
|
-
flex-shrink: 0;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
.toolbar {
|
|
311
|
-
display: flex;
|
|
312
|
-
align-items: center;
|
|
313
|
-
gap: 6px;
|
|
314
|
-
flex-shrink: 0;
|
|
315
|
-
flex-wrap: wrap;
|
|
292
|
+
.fetch-dashboard__search {
|
|
293
|
+
max-width: 240px;
|
|
316
294
|
}
|
|
317
295
|
|
|
318
|
-
.
|
|
319
|
-
|
|
320
|
-
gap: 12px;
|
|
321
|
-
flex: 1;
|
|
322
|
-
overflow: hidden;
|
|
323
|
-
min-height: 0;
|
|
296
|
+
.fetch-dashboard__bar-column {
|
|
297
|
+
min-width: 80px;
|
|
324
298
|
}
|
|
325
299
|
|
|
326
|
-
.
|
|
327
|
-
|
|
328
|
-
overflow: auto;
|
|
329
|
-
border: 0.5px solid var(--border);
|
|
330
|
-
border-radius: var(--radius-lg);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
.detail-panel {
|
|
334
|
-
width: 280px;
|
|
335
|
-
flex-shrink: 0;
|
|
336
|
-
display: flex;
|
|
337
|
-
flex-direction: column;
|
|
338
|
-
gap: 8px;
|
|
339
|
-
overflow: auto;
|
|
340
|
-
border: 0.5px solid var(--border);
|
|
341
|
-
border-radius: var(--radius-lg);
|
|
342
|
-
padding: 12px;
|
|
343
|
-
background: var(--bg3);
|
|
300
|
+
.fetch-dashboard__url {
|
|
301
|
+
max-width: 200px;
|
|
344
302
|
}
|
|
345
303
|
|
|
346
|
-
.
|
|
347
|
-
width: 280px;
|
|
348
|
-
flex-shrink: 0;
|
|
304
|
+
.fetch-dashboard__detail-header {
|
|
349
305
|
display: flex;
|
|
350
306
|
align-items: center;
|
|
351
|
-
justify-content:
|
|
352
|
-
color: var(--text3);
|
|
353
|
-
font-size: 12px;
|
|
354
|
-
border: 0.5px dashed var(--border);
|
|
355
|
-
border-radius: var(--radius-lg);
|
|
307
|
+
justify-content: space-between;
|
|
356
308
|
}
|
|
357
309
|
|
|
358
|
-
.
|
|
359
|
-
|
|
360
|
-
align-items: center;
|
|
361
|
-
justify-content: space-between;
|
|
310
|
+
.fetch-dashboard__detail-title {
|
|
311
|
+
font-size: var(--tracker-font-size-md);
|
|
362
312
|
}
|
|
363
313
|
|
|
364
|
-
.
|
|
314
|
+
.fetch-dashboard__meta-grid {
|
|
365
315
|
display: grid;
|
|
366
316
|
grid-template-columns: auto 1fr;
|
|
367
|
-
gap:
|
|
368
|
-
font-size:
|
|
317
|
+
gap: var(--tracker-space-1) var(--tracker-space-3);
|
|
318
|
+
font-size: var(--tracker-font-size-sm);
|
|
369
319
|
}
|
|
370
320
|
|
|
371
|
-
.
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
color: var(--text3);
|
|
321
|
+
.fetch-dashboard__meta-value {
|
|
322
|
+
word-break: break-all;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.fetch-dashboard__section-label {
|
|
377
326
|
margin-top: 6px;
|
|
378
327
|
min-height: fit-content;
|
|
379
328
|
}
|
|
380
329
|
|
|
381
|
-
.
|
|
330
|
+
.fetch-dashboard__section-label--source {
|
|
331
|
+
margin-top: 10px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.fetch-dashboard__payload-box {
|
|
382
335
|
font-family: var(--mono);
|
|
383
|
-
font-size:
|
|
336
|
+
font-size: var(--tracker-font-size-sm);
|
|
384
337
|
color: var(--text2);
|
|
385
338
|
background: var(--bg2);
|
|
386
339
|
border-radius: var(--radius);
|
|
387
|
-
padding:
|
|
340
|
+
padding: var(--tracker-space-2) 10px;
|
|
388
341
|
overflow: auto;
|
|
389
342
|
white-space: pre;
|
|
390
343
|
max-height: 160px;
|
|
391
344
|
}
|
|
392
345
|
|
|
393
|
-
.
|
|
346
|
+
.fetch-dashboard__waterfall {
|
|
394
347
|
flex-shrink: 0;
|
|
395
348
|
background: var(--bg3);
|
|
396
|
-
border:
|
|
349
|
+
border: var(--tracker-border-width) solid var(--border);
|
|
397
350
|
border-radius: var(--radius-lg);
|
|
398
|
-
padding: 10px
|
|
351
|
+
padding: 10px var(--tracker-space-3);
|
|
399
352
|
}
|
|
400
353
|
|
|
401
|
-
.
|
|
354
|
+
.fetch-dashboard__waterfall-header {
|
|
402
355
|
display: flex;
|
|
403
356
|
align-items: center;
|
|
404
357
|
justify-content: space-between;
|
|
405
|
-
gap:
|
|
358
|
+
gap: var(--tracker-space-2);
|
|
406
359
|
}
|
|
407
360
|
|
|
408
|
-
.
|
|
409
|
-
margin
|
|
361
|
+
.fetch-dashboard__waterfall-label {
|
|
362
|
+
margin: 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.fetch-dashboard__waterfall-body {
|
|
366
|
+
margin-top: var(--tracker-gap-toolbar);
|
|
410
367
|
}
|
|
411
368
|
|
|
412
|
-
.
|
|
369
|
+
.fetch-dashboard__waterfall-row {
|
|
413
370
|
display: flex;
|
|
414
371
|
align-items: center;
|
|
415
|
-
gap:
|
|
416
|
-
margin-bottom:
|
|
372
|
+
gap: var(--tracker-space-2);
|
|
373
|
+
margin-bottom: var(--tracker-space-1);
|
|
417
374
|
}
|
|
418
375
|
|
|
419
|
-
.
|
|
420
|
-
|
|
376
|
+
.fetch-dashboard__waterfall-key {
|
|
377
|
+
width: 140px;
|
|
378
|
+
overflow: hidden;
|
|
379
|
+
text-overflow: ellipsis;
|
|
380
|
+
white-space: nowrap;
|
|
381
|
+
flex-shrink: 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.fetch-dashboard__waterfall-track {
|
|
421
385
|
position: relative;
|
|
386
|
+
flex: 1;
|
|
422
387
|
height: 8px;
|
|
423
388
|
background: var(--bg2);
|
|
424
389
|
border-radius: 2px;
|
|
425
390
|
overflow: hidden;
|
|
426
391
|
}
|
|
427
392
|
|
|
428
|
-
.
|
|
393
|
+
.fetch-dashboard__waterfall-bar {
|
|
429
394
|
position: absolute;
|
|
430
395
|
top: 0;
|
|
431
396
|
height: 100%;
|
|
432
397
|
border-radius: 2px;
|
|
433
398
|
opacity: 0.8;
|
|
434
399
|
}
|
|
400
|
+
|
|
401
|
+
.fetch-dashboard__waterfall-time {
|
|
402
|
+
width: 44px;
|
|
403
|
+
text-align: right;
|
|
404
|
+
flex-shrink: 0;
|
|
405
|
+
}
|
|
435
406
|
</style>
|