picocrank 1.31.1 → 1.32.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.
@@ -0,0 +1,267 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <nav
4
+ v-if="open"
5
+ ref="menuRef"
6
+ class="menu context-menu table-row-context-menu"
7
+ role="menu"
8
+ tabindex="-1"
9
+ :style="style"
10
+ aria-label="Row actions"
11
+ @click.stop
12
+ @contextmenu.prevent
13
+ @keydown="onMenuKeyDown"
14
+ >
15
+ <ul class="noListStyle">
16
+ <li>
17
+ <div>
18
+ <ul>
19
+ <template v-for="(item, index) in items" :key="itemKey(item, index)">
20
+ <li
21
+ v-if="item.divider"
22
+ role="separator"
23
+ />
24
+ <li v-else>
25
+ <button
26
+ type="button"
27
+ role="menuitem"
28
+ :class="{
29
+ 'inline-icon': Boolean(item.icon),
30
+ bad: item.danger,
31
+ }"
32
+ :disabled="item.disabled"
33
+ @click="onSelect(item)"
34
+ >
35
+ <HugeiconsIcon
36
+ v-if="item.icon"
37
+ :icon="item.icon"
38
+ width="1em"
39
+ height="1em"
40
+ :strokeWidth="2.5"
41
+ aria-hidden="true"
42
+ />
43
+ <span>{{ itemLabel(item) }}</span>
44
+ </button>
45
+ </li>
46
+ </template>
47
+ </ul>
48
+ </div>
49
+ </li>
50
+ </ul>
51
+ </nav>
52
+ </Teleport>
53
+ </template>
54
+
55
+ <script setup>
56
+ import { nextTick, onBeforeUnmount, ref, watch } from 'vue'
57
+ import { HugeiconsIcon } from '@hugeicons/vue'
58
+
59
+ const props = defineProps({
60
+ open: {
61
+ type: Boolean,
62
+ default: false,
63
+ },
64
+ items: {
65
+ type: Array,
66
+ default: () => [],
67
+ },
68
+ clientX: {
69
+ type: Number,
70
+ default: 0,
71
+ },
72
+ clientY: {
73
+ type: Number,
74
+ default: 0,
75
+ },
76
+ targetCount: {
77
+ type: Number,
78
+ default: 1,
79
+ },
80
+ })
81
+
82
+ const emit = defineEmits(['update:open', 'select', 'cancel'])
83
+
84
+ const MENU_MARGIN_PX = 8
85
+ const menuRef = ref(null)
86
+ const style = ref({ top: '0px', left: '0px' })
87
+
88
+ function itemKey(item, index) {
89
+ if (item.divider) {
90
+ return `divider-${index}`
91
+ }
92
+ return item.id ?? item.label ?? index
93
+ }
94
+
95
+ function itemLabel(item) {
96
+ if (typeof item.label === 'function') {
97
+ return item.label({ count: props.targetCount })
98
+ }
99
+ const label = item.label ?? ''
100
+ if (props.targetCount > 1 && item.showCount !== false) {
101
+ return `${label} (${props.targetCount})`
102
+ }
103
+ return label
104
+ }
105
+
106
+ function positionMenu() {
107
+ const menu = menuRef.value
108
+ if (!menu) {
109
+ return
110
+ }
111
+
112
+ const rect = menu.getBoundingClientRect()
113
+ const margin = MENU_MARGIN_PX
114
+ let top = props.clientY
115
+ let left = props.clientX
116
+
117
+ if (left + rect.width > window.innerWidth - margin) {
118
+ left = window.innerWidth - rect.width - margin
119
+ }
120
+ if (top + rect.height > window.innerHeight - margin) {
121
+ top = window.innerHeight - rect.height - margin
122
+ }
123
+ if (left < margin) {
124
+ left = margin
125
+ }
126
+ if (top < margin) {
127
+ top = margin
128
+ }
129
+
130
+ style.value = {
131
+ top: `${top}px`,
132
+ left: `${left}px`,
133
+ }
134
+ }
135
+
136
+ function close() {
137
+ emit('update:open', false)
138
+ emit('cancel')
139
+ }
140
+
141
+ function onSelect(item) {
142
+ if (item.disabled) {
143
+ return
144
+ }
145
+ emit('select', item)
146
+ emit('update:open', false)
147
+ }
148
+
149
+ function enabledMenuItems() {
150
+ if (!menuRef.value) {
151
+ return []
152
+ }
153
+ return [...menuRef.value.querySelectorAll('[role="menuitem"]:not([disabled])')]
154
+ }
155
+
156
+ function onMenuKeyDown(event) {
157
+ const items = enabledMenuItems()
158
+ const active = document.activeElement
159
+ const currentIndex = items.indexOf(active)
160
+ const focusIsOnMenuShell = active === menuRef.value
161
+
162
+ if (event.key === 'Escape') {
163
+ event.preventDefault()
164
+ close()
165
+ return
166
+ }
167
+
168
+ if (event.key === 'ArrowDown') {
169
+ event.preventDefault()
170
+ const nextIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % items.length
171
+ items[nextIndex]?.focus()
172
+ return
173
+ }
174
+
175
+ if (event.key === 'ArrowUp') {
176
+ event.preventDefault()
177
+ const nextIndex = currentIndex < 0
178
+ ? items.length - 1
179
+ : (currentIndex - 1 + items.length) % items.length
180
+ items[nextIndex]?.focus()
181
+ return
182
+ }
183
+
184
+ if (event.key === 'Home') {
185
+ event.preventDefault()
186
+ items[0]?.focus()
187
+ return
188
+ }
189
+
190
+ if (event.key === 'End') {
191
+ event.preventDefault()
192
+ items[items.length - 1]?.focus()
193
+ return
194
+ }
195
+
196
+ if (event.key !== 'Tab' || items.length === 0) {
197
+ return
198
+ }
199
+
200
+ // Tab through items; leave (and close) rather than jumping to the rest of the page.
201
+ if (event.shiftKey) {
202
+ if (focusIsOnMenuShell || currentIndex <= 0) {
203
+ event.preventDefault()
204
+ close()
205
+ }
206
+ return
207
+ }
208
+
209
+ if (currentIndex === items.length - 1) {
210
+ event.preventDefault()
211
+ close()
212
+ }
213
+ }
214
+
215
+ function onDocumentPointerDown(event) {
216
+ if (!props.open) {
217
+ return
218
+ }
219
+ if (menuRef.value?.contains(event.target)) {
220
+ return
221
+ }
222
+ close()
223
+ }
224
+
225
+ function onDocumentKeyDown(event) {
226
+ if (!props.open) {
227
+ return
228
+ }
229
+ if (event.key === 'Escape') {
230
+ close()
231
+ }
232
+ }
233
+
234
+ function attachListeners() {
235
+ window.addEventListener('pointerdown', onDocumentPointerDown, true)
236
+ window.addEventListener('keydown', onDocumentKeyDown)
237
+ }
238
+
239
+ function detachListeners() {
240
+ window.removeEventListener('pointerdown', onDocumentPointerDown, true)
241
+ window.removeEventListener('keydown', onDocumentKeyDown)
242
+ }
243
+
244
+ watch(
245
+ () => props.open,
246
+ async (isOpen) => {
247
+ detachListeners()
248
+ if (!isOpen) {
249
+ return
250
+ }
251
+ style.value = {
252
+ top: `${props.clientY}px`,
253
+ left: `${props.clientX}px`,
254
+ }
255
+ await nextTick()
256
+ positionMenu()
257
+ // Focus the menu shell so keyboard users can Tab/arrow into items,
258
+ // without visually selecting the first action on open.
259
+ menuRef.value?.focus({ preventScroll: true })
260
+ attachListeners()
261
+ },
262
+ )
263
+
264
+ onBeforeUnmount(() => {
265
+ detachListeners()
266
+ })
267
+ </script>
@@ -112,6 +112,11 @@ export const navSectionHubs = {
112
112
  title: 'Dialog',
113
113
  options: { description: 'Modal dialogs and confirmations' },
114
114
  },
115
+ {
116
+ name: 'LoadingAreaExample',
117
+ title: 'Loading area',
118
+ options: { description: 'Async placeholder with spinner' },
119
+ },
115
120
  ],
116
121
  },
117
122
  NavPatternsHub: {
@@ -0,0 +1,175 @@
1
+ <template>
2
+ <Section
3
+ title="Loading area"
4
+ :icon="Loading03Icon"
5
+ subtitle="Placeholder for async regions — wrap content or use as a standalone status block."
6
+ >
7
+ <p>
8
+ Bind <code>:loading</code> around a default slot for the usual Vue pattern, or
9
+ render <code>&lt;LoadingArea /&gt;</code> on its own while data is pending.
10
+ The spinner uses Hugeicons <code>Loading03Icon</code> with a CSS rotation
11
+ (respects <code>prefers-reduced-motion</code>).
12
+ </p>
13
+ </Section>
14
+
15
+ <Section title="Wrap content" subtitle="Toggle loading to swap between placeholder and content">
16
+ <template #toolbar>
17
+ <button type="button" class="neutral" @click="contentLoading = !contentLoading">
18
+ {{ contentLoading ? 'Show content' : 'Show loading' }}
19
+ </button>
20
+ </template>
21
+
22
+ <LoadingArea :loading="contentLoading" message="Fetching table rows…" min-height="10rem">
23
+ <div class="demo-panel">
24
+ <p>Content is ready. This block is the default slot.</p>
25
+ <ul>
26
+ <li>Ada — London</li>
27
+ <li>Grace — New York</li>
28
+ <li>Katherine — Newport News</li>
29
+ </ul>
30
+ </div>
31
+ </LoadingArea>
32
+ </Section>
33
+
34
+ <Section title="Overlay mode" subtitle="Keep layout size while covering content">
35
+ <template #toolbar>
36
+ <button type="button" class="neutral" @click="runOverlayFetch">
37
+ {{ overlayLoading ? 'Loading…' : 'Simulate refresh' }}
38
+ </button>
39
+ </template>
40
+
41
+ <LoadingArea
42
+ :loading="overlayLoading"
43
+ overlay
44
+ message="Refreshing…"
45
+ min-height="8rem"
46
+ >
47
+ <div class="demo-panel">
48
+ <p>
49
+ With <code>overlay</code>, the default slot stays mounted so the area does
50
+ not collapse. Useful for refresh-in-place.
51
+ </p>
52
+ <p class="subtle">Last refreshed: {{ lastRefreshedLabel }}</p>
53
+ </div>
54
+ </LoadingArea>
55
+ </Section>
56
+
57
+ <Section title="Standalone placeholder" subtitle="No default slot — just the loading state">
58
+ <LoadingArea message="Connecting to server…" min-height="8rem" />
59
+ </Section>
60
+
61
+ <Section title="Custom placeholder slot" subtitle="Replace the spinner with your own markup">
62
+ <LoadingArea :loading="true" min-height="8rem">
63
+ <template #placeholder>
64
+ <p class="custom-placeholder">Preparing preview…</p>
65
+ </template>
66
+ </LoadingArea>
67
+ </Section>
68
+
69
+ <Section title="Idiomatic async example" subtitle="ref + async function with try/finally">
70
+ <template #toolbar>
71
+ <button type="button" class="good" :disabled="peopleLoading" @click="loadPeople">
72
+ {{ peopleLoading ? 'Loading…' : 'Load people' }}
73
+ </button>
74
+ <button type="button" class="neutral" :disabled="!people.length" @click="people = []">
75
+ Clear
76
+ </button>
77
+ </template>
78
+
79
+ <LoadingArea :loading="peopleLoading" message="Loading people…" min-height="10rem">
80
+ <div v-if="peopleError" class="demo-panel">
81
+ <p class="annotation bad">{{ peopleError }}</p>
82
+ </div>
83
+ <div v-else-if="people.length" class="demo-panel">
84
+ <ul>
85
+ <li v-for="person in people" :key="person.name">
86
+ {{ person.name }} — {{ person.city }}
87
+ </li>
88
+ </ul>
89
+ </div>
90
+ <p v-else class="subtle">No people loaded yet. Click “Load people”.</p>
91
+ </LoadingArea>
92
+ </Section>
93
+ </template>
94
+
95
+ <script setup>
96
+ import { computed, onBeforeUnmount, ref } from 'vue'
97
+ import { Loading03Icon } from '@hugeicons/core-free-icons'
98
+ import { getExampleTableRows } from '../data/examplePeople.js'
99
+
100
+ const contentLoading = ref(true)
101
+ const overlayLoading = ref(false)
102
+ const lastRefreshedAt = ref(Date.now())
103
+ const people = ref([])
104
+ const peopleLoading = ref(false)
105
+ const peopleError = ref('')
106
+
107
+ let overlayTimer = null
108
+ let peopleTimer = null
109
+
110
+ const lastRefreshedLabel = computed(() =>
111
+ new Date(lastRefreshedAt.value).toLocaleTimeString(),
112
+ )
113
+
114
+ function runOverlayFetch() {
115
+ if (overlayLoading.value) {
116
+ return
117
+ }
118
+ overlayLoading.value = true
119
+ clearTimeout(overlayTimer)
120
+ overlayTimer = setTimeout(() => {
121
+ lastRefreshedAt.value = Date.now()
122
+ overlayLoading.value = false
123
+ overlayTimer = null
124
+ }, 1500)
125
+ }
126
+
127
+ async function loadPeople() {
128
+ peopleLoading.value = true
129
+ peopleError.value = ''
130
+ try {
131
+ people.value = await fetchPeople()
132
+ } catch (error) {
133
+ peopleError.value = error?.message || 'Failed to load people.'
134
+ people.value = []
135
+ } finally {
136
+ peopleLoading.value = false
137
+ }
138
+ }
139
+
140
+ function fetchPeople() {
141
+ return new Promise((resolve, reject) => {
142
+ clearTimeout(peopleTimer)
143
+ peopleTimer = setTimeout(() => {
144
+ peopleTimer = null
145
+ if (Math.random() < 0.15) {
146
+ reject(new Error('Simulated network failure. Try again.'))
147
+ return
148
+ }
149
+ resolve(getExampleTableRows().slice(0, 5))
150
+ }, 1200)
151
+ })
152
+ }
153
+
154
+ onBeforeUnmount(() => {
155
+ clearTimeout(overlayTimer)
156
+ clearTimeout(peopleTimer)
157
+ })
158
+ </script>
159
+
160
+ <style scoped>
161
+ .demo-panel {
162
+ padding: 1rem;
163
+ }
164
+
165
+ .demo-panel ul {
166
+ margin: 0.5rem 0 0;
167
+ padding-left: 1.25rem;
168
+ }
169
+
170
+ .custom-placeholder {
171
+ margin: 0;
172
+ font-style: italic;
173
+ color: var(--muted-text-color);
174
+ }
175
+ </style>
@@ -2,9 +2,53 @@
2
2
  <Section
3
3
  :title="tableSectionTitle"
4
4
  :icon="TableIcon"
5
- subtitle="Sortable columns, pagination, and column filters. Right-click or long-press a column header to filter."
5
+ subtitle="Sortable columns, pagination, and column filters. Right-click or long-press a column header to filter. Enable “Row context menu” to right-click rows."
6
6
  :padding="false"
7
7
  >
8
+ <template v-if="multiSelectEnabled" #toolbar>
9
+ <span v-if="selectedKeys.length" class="subtle selection-count">
10
+ {{ selectedKeys.length }} selected
11
+ </span>
12
+ <button
13
+ type="button"
14
+ class="inline-icon neutral"
15
+ :disabled="!selectedKeys.length"
16
+ @click="bulkExportSelected"
17
+ >
18
+ <HugeiconsIcon
19
+ :icon="Download01Icon"
20
+ width="1em"
21
+ height="1em"
22
+ :strokeWidth="iconStrokeWidth"
23
+ aria-hidden="true"
24
+ />
25
+ <span>Export</span>
26
+ </button>
27
+ <button
28
+ type="button"
29
+ class="inline-icon bad"
30
+ :disabled="!selectedKeys.length"
31
+ @click="bulkDeleteSelected"
32
+ >
33
+ <HugeiconsIcon
34
+ :icon="Delete02Icon"
35
+ width="1em"
36
+ height="1em"
37
+ :strokeWidth="iconStrokeWidth"
38
+ aria-hidden="true"
39
+ />
40
+ <span>Delete</span>
41
+ </button>
42
+ <button
43
+ type="button"
44
+ class="neutral"
45
+ :disabled="!selectedKeys.length"
46
+ @click="clearSelection"
47
+ >
48
+ Clear selection
49
+ </button>
50
+ </template>
51
+
8
52
  <Table
9
53
  table-id="table-example"
10
54
  row-key="name"
@@ -14,6 +58,9 @@
14
58
  :filterable="enabledFeatures.includes('filters')"
15
59
  :column-options="enabledFeatures.includes('columnOptions')"
16
60
  :loading="enabledFeatures.includes('loading')"
61
+ :selectable="multiSelectEnabled"
62
+ v-model:selected-keys="selectedKeys"
63
+ :row-context-menu="rowContextMenuEnabled ? rowContextMenuItems : null"
17
64
  :horizontal-scroll="layout === 'scroll'"
18
65
  :responsive-columns="layout === 'priorities'"
19
66
  :sticky-cols="stickyCols"
@@ -127,6 +174,25 @@
127
174
  <p v-else-if="enabledFeatures.includes('columnOptions')" class="event-line subtle">
128
175
  Layout: using developer defaults (change columns, order, or filters to see a preset or “Custom” label).
129
176
  </p>
177
+
178
+ <p v-if="multiSelectEnabled && selectedKeys.length" class="event-line">
179
+ <strong>Selection:</strong>
180
+ {{ selectedKeys.join(', ') }}
181
+ </p>
182
+ <p v-else-if="multiSelectEnabled" class="event-line subtle">
183
+ Selection: enable “Multi-select”, then click rows or checkboxes. Use the toolbar Export / Delete actions on the selected set.
184
+ </p>
185
+
186
+ <p v-if="lastContextMenuAction" class="event-line">
187
+ <strong>Context menu:</strong>
188
+ {{ lastContextMenuAction }}
189
+ </p>
190
+ <p v-else-if="rowContextMenuEnabled" class="event-line subtle">
191
+ Context menu: right-click a row. With multi-select, right-click inside the selection to act on all selected rows.
192
+ </p>
193
+ <p v-else-if="lastBulkAction" class="event-line subtle">
194
+ {{ lastBulkAction }}
195
+ </p>
130
196
  </div>
131
197
  </Section>
132
198
 
@@ -165,11 +231,13 @@
165
231
 
166
232
  <script setup>
167
233
  import { ref, computed, onMounted } from 'vue'
168
- import { TableIcon, Settings01Icon } from '@hugeicons/core-free-icons'
234
+ import { HugeiconsIcon } from '@hugeicons/vue'
235
+ import { TableIcon, Settings01Icon, Download01Icon, Delete02Icon, ViewIcon } from '@hugeicons/core-free-icons'
169
236
  import { getExampleTableRows } from '../data/examplePeople.js'
170
237
  import { countActiveFilters } from '../composables/tableFilters.js'
171
238
 
172
- const defaultFeatures = ['pagination', 'filters', 'columnOptions', 'rowClick']
239
+ const iconStrokeWidth = 2.5
240
+ const defaultFeatures = ['pagination', 'filters', 'columnOptions', 'rowClick', 'rowContextMenu']
173
241
 
174
242
  const defaultColumnVisibility = {
175
243
  birthYear: false,
@@ -184,9 +252,15 @@ const dataConnected = ref(true)
184
252
  const data = ref([])
185
253
  const lastRowClick = ref(null)
186
254
  const lastQuery = ref(null)
255
+ const lastBulkAction = ref(null)
256
+ const lastContextMenuAction = ref(null)
257
+ const selectedKeys = ref([])
187
258
  const tableLayoutLabel = ref('')
188
259
  const rowDialogRef = ref(null)
189
260
 
261
+ const multiSelectEnabled = computed(() => enabledFeatures.value.includes('multiSelect'))
262
+ const rowContextMenuEnabled = computed(() => enabledFeatures.value.includes('rowContextMenu'))
263
+
190
264
  const tableSectionTitle = computed(() => {
191
265
  if (tableLayoutLabel.value) {
192
266
  return `Table Example (${tableLayoutLabel.value})`
@@ -204,6 +278,8 @@ const featureOptions = [
204
278
  { value: 'filters', label: 'Column filters' },
205
279
  { value: 'columnOptions', label: 'Column options' },
206
280
  { value: 'rowClick', label: 'Row click' },
281
+ { value: 'multiSelect', label: 'Multi-select' },
282
+ { value: 'rowContextMenu', label: 'Row context menu' },
207
283
  { value: 'loading', label: 'Loading state' },
208
284
  ]
209
285
 
@@ -285,12 +361,56 @@ const tableListeners = computed(() => {
285
361
  const listeners = {
286
362
  onQueryChange: handleQueryChange,
287
363
  }
288
- if (enabledFeatures.value.includes('rowClick')) {
364
+ // Row click opens the detail dialog; multi-select uses row clicks for selection instead.
365
+ if (enabledFeatures.value.includes('rowClick') && !multiSelectEnabled.value) {
289
366
  listeners.onRowClick = handleRowClick
290
367
  }
291
368
  return listeners
292
369
  })
293
370
 
371
+ const rowContextMenuItems = computed(() => [
372
+ {
373
+ id: 'view',
374
+ label: 'View details',
375
+ icon: ViewIcon,
376
+ action: ({ row, rows }) => {
377
+ const target = rows[0] ?? row
378
+ if (!target) {
379
+ return
380
+ }
381
+ lastContextMenuAction.value = `View “${target.name}”`
382
+ lastRowClick.value = {
383
+ row: target,
384
+ index: tableData.value.findIndex((item) => item.name === target.name),
385
+ }
386
+ rowDialogRef.value?.showModal()
387
+ },
388
+ },
389
+ {
390
+ id: 'export',
391
+ label: 'Export',
392
+ icon: Download01Icon,
393
+ action: ({ keys }) => {
394
+ lastContextMenuAction.value = `Exported ${keys.length} row(s): ${keys.join(', ')}`
395
+ lastBulkAction.value = lastContextMenuAction.value
396
+ },
397
+ },
398
+ { divider: true },
399
+ {
400
+ id: 'delete',
401
+ label: 'Delete',
402
+ icon: Delete02Icon,
403
+ danger: true,
404
+ action: ({ keys }) => {
405
+ const removeSet = new Set(keys)
406
+ data.value = data.value.filter((row) => !removeSet.has(row.name))
407
+ selectedKeys.value = selectedKeys.value.filter((key) => !removeSet.has(key))
408
+ lastContextMenuAction.value = `Deleted ${keys.length} row(s): ${keys.join(', ')}`
409
+ lastBulkAction.value = lastContextMenuAction.value
410
+ },
411
+ },
412
+ ])
413
+
294
414
  function handleRowClick(payload) {
295
415
  lastRowClick.value = payload
296
416
  rowDialogRef.value?.showModal()
@@ -306,6 +426,29 @@ function handleQueryChange(query) {
306
426
  lastQuery.value = query
307
427
  }
308
428
 
429
+ function clearSelection() {
430
+ selectedKeys.value = []
431
+ lastBulkAction.value = 'Selection cleared.'
432
+ }
433
+
434
+ function bulkExportSelected() {
435
+ if (!selectedKeys.value.length) {
436
+ return
437
+ }
438
+ lastBulkAction.value = `Exported ${selectedKeys.value.length} row(s): ${selectedKeys.value.join(', ')}`
439
+ }
440
+
441
+ function bulkDeleteSelected() {
442
+ if (!selectedKeys.value.length) {
443
+ return
444
+ }
445
+ const removed = [...selectedKeys.value]
446
+ const removeSet = new Set(removed)
447
+ data.value = data.value.filter((row) => !removeSet.has(row.name))
448
+ selectedKeys.value = []
449
+ lastBulkAction.value = `Deleted ${removed.length} row(s): ${removed.join(', ')}`
450
+ }
451
+
309
452
  function statusTagClass(value) {
310
453
  if (value === 'Active') {
311
454
  return 'good'
@@ -323,7 +466,11 @@ function resetControls() {
323
466
  stickyColsOption.value = '2'
324
467
  wideTable.value = true
325
468
  dataConnected.value = true
469
+ data.value = getExampleTableRows()
326
470
  lastRowClick.value = null
471
+ lastBulkAction.value = null
472
+ lastContextMenuAction.value = null
473
+ selectedKeys.value = []
327
474
  if (rowDialogRef.value?.open) {
328
475
  rowDialogRef.value.close()
329
476
  }
@@ -347,6 +494,11 @@ onMounted(() => {
347
494
  margin: 0.35rem 0;
348
495
  }
349
496
 
497
+ .selection-count {
498
+ align-self: center;
499
+ margin-right: 0.5rem;
500
+ }
501
+
350
502
  .row-detail-dialog .dialog-panel h3 {
351
503
  margin: 0 0 0.5rem;
352
504
  }