@globalbrain/sefirot 2.46.0 → 2.47.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.
@@ -1,6 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { type MaybeRef } from '@vueuse/core'
3
- import { computed, unref, useSlots } from 'vue'
2
+ import { type MaybeRef, computed, unref, useSlots } from 'vue'
4
3
  import { type Position } from '../composables/Tooltip'
5
4
  import SFragment from './SFragment.vue'
6
5
  import SIcon from './SIcon.vue'
@@ -24,7 +23,7 @@ export type Mode =
24
23
 
25
24
  export interface Tooltip {
26
25
  tag?: string
27
- text?: MaybeRef<string>
26
+ text?: MaybeRef<string | null>
28
27
  position?: Position
29
28
  trigger?: 'hover' | 'focus' | 'both'
30
29
  timeout?: number
@@ -5,7 +5,7 @@
5
5
  </template>
6
6
 
7
7
  <style scoped lang="postcss">
8
- .SButtonGroup :slotted(.SButton) {
8
+ .SButtonGroup :deep(.SButton) {
9
9
  border-left-width: 0;
10
10
  border-radius: 0;
11
11
 
@@ -28,6 +28,7 @@ const classes = computed(() => [
28
28
  <style scoped lang="postcss">
29
29
  .SCard {
30
30
  display: grid;
31
+ grid-template-columns: minmax(0, 1fr);
31
32
  gap: 1px;
32
33
  border: 1px solid transparent;
33
34
  border-radius: 6px;
@@ -1,8 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import IconCheck from '@iconify-icons/ph/check'
3
- import { type MaybeRef } from '@vueuse/core'
4
3
  import Fuse from 'fuse.js'
5
- import { computed, onMounted, ref, unref } from 'vue'
4
+ import { type MaybeRef, computed, onMounted, ref, unref } from 'vue'
6
5
  import { type DropdownSectionFilterOption, type DropdownSectionFilterSelectedValue } from '../composables/Dropdown'
7
6
  import { isArray } from '../support/Utils'
8
7
  import SDropdownSectionFilterItem from './SDropdownSectionFilterItem.vue'
@@ -57,16 +57,12 @@ function getErrorMsg(validation: Validatable) {
57
57
  <template>
58
58
  <div class="SInputBase" :class="{ 'has-error': error?.has }">
59
59
  <label v-if="label" class="label" :for="name">
60
- <STooltip v-if="hasInfo" :text="info">
61
- <div class="label-text">
62
- <span class="label-text-value">{{ label }}</span>
63
- <SIcon class="label-text-info" :icon="IconQuestion" />
64
- </div>
60
+ <span class="label-text">{{ label }}</span>
61
+
62
+ <STooltip v-if="hasInfo" :text="info" trigger="focus" @click.prevent>
63
+ <SIcon class="label-info" :icon="IconQuestion" />
65
64
  <template v-if="$slots.info" #text><slot name="info" /></template>
66
65
  </STooltip>
67
- <div v-else class="label-text">
68
- <span class="label-text-value">{{ label }}</span>
69
- </div>
70
66
 
71
67
  <span class="label-note">{{ note }}</span>
72
68
 
@@ -119,26 +115,29 @@ function getErrorMsg(validation: Validatable) {
119
115
  }
120
116
 
121
117
  .label-text {
122
- display: flex;
123
- align-items: center;
124
- gap: 4px;
125
- transition: color 0.25s;
126
- }
127
-
128
- .label-text-value {
129
118
  font-weight: 500;
130
119
  color: var(--input-label-color);
131
120
  transition: color 0.25s;
132
121
  }
133
122
 
134
- .label-text-info {
135
- width: 16px;
136
- height: 16px;
137
- color: var(--c-text-2);
138
- transition: color 0.25s;
123
+ :deep(.STooltip) {
124
+ .label-info {
125
+ display: inline-block;
126
+ margin-left: 4px;
127
+ width: 16px;
128
+ height: 16px;
129
+ color: var(--c-text-2);
130
+ transition: color 0.25s;
131
+ }
132
+
133
+ &:hover, &:focus, &:focus-within {
134
+ .label-info {
135
+ color: var(--c-info-text);
136
+ }
137
+ }
139
138
 
140
- .label-text:hover & {
141
- color: var(--c-info-text);
139
+ .content {
140
+ vertical-align: middle;
142
141
  }
143
142
  }
144
143
 
@@ -15,18 +15,20 @@ const props = defineProps<{
15
15
  help?: string
16
16
  text?: string
17
17
  placeholder?: string
18
+ accept?: string
19
+ multiple?: boolean
18
20
  checkIcon?: IconifyIcon | DefineComponent
19
21
  checkText?: string
20
22
  checkColor?: Color
21
- value?: File | null
22
- modelValue?: File | null
23
+ value?: File | File[] | null
24
+ modelValue?: File | File[] | null
23
25
  hideError?: boolean
24
26
  validation?: Validatable
25
27
  }>()
26
28
 
27
29
  const emit = defineEmits<{
28
- (e: 'update:model-value', file: File | null): void
29
- (e: 'change', file: File | null): void
30
+ (e: 'update:model-value', file: File | File[] | null): void
31
+ (e: 'change', file: File | File[] | null): void
30
32
  }>()
31
33
 
32
34
  const _value = computed(() => {
@@ -39,15 +41,19 @@ const input = ref<HTMLInputElement | null>(null)
39
41
 
40
42
  const classes = computed(() => [props.size ?? 'small'])
41
43
 
42
- const fileName = computed(() => _value.value?.name ?? null)
44
+ const fileName = computed(() => Array.isArray(_value.value)
45
+ ? _value.value.map((file) => file.name).join(', ')
46
+ : _value.value?.name ?? ''
47
+ )
43
48
 
44
- /* c8 ignore next 4 */
45
49
  function open() {
46
50
  input.value!.click()
47
51
  }
48
52
 
49
53
  function onChange(e: Event) {
50
- const file = (e.target as any).files[0]
54
+ const file = props.multiple
55
+ ? Array.from((e.target as HTMLInputElement).files ?? [])
56
+ : ((e.target as HTMLInputElement).files ?? [])[0]
51
57
 
52
58
  emit('update:model-value', file ?? null)
53
59
  emit('change', file ?? null)
@@ -74,6 +80,8 @@ function onChange(e: Event) {
74
80
  ref="input"
75
81
  class="input"
76
82
  type="file"
83
+ :accept="accept"
84
+ :multiple="multiple"
77
85
  @change="onChange"
78
86
  >
79
87
 
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { useVirtualizer } from '@tanstack/vue-virtual'
3
3
  import { useResizeObserver } from '@vueuse/core'
4
+ import xor from 'lodash-es/xor'
4
5
  import {
5
6
  computed,
6
7
  nextTick,
@@ -11,6 +12,7 @@ import {
11
12
  watch
12
13
  } from 'vue'
13
14
  import { type Table } from '../composables/Table'
15
+ import SInputCheckbox from './SInputCheckbox.vue'
14
16
  import SSpinner from './SSpinner.vue'
15
17
  import STableCell from './STableCell.vue'
16
18
  import STableColumn from './STableColumn.vue'
@@ -20,6 +22,11 @@ import STableItem from './STableItem.vue'
20
22
 
21
23
  const props = defineProps<{
22
24
  options: Table
25
+ selected?: unknown[]
26
+ }>()
27
+
28
+ const emit = defineEmits<{
29
+ (e: 'update:selected', value: unknown[]): void
23
30
  }>()
24
31
 
25
32
  const head = shallowRef<HTMLElement | null>(null)
@@ -28,9 +35,13 @@ const block = shallowRef<HTMLElement | null>(null)
28
35
  const row = shallowRef<HTMLElement | null>(null)
29
36
 
30
37
  const ordersToShow = computed(() => {
31
- return unref(props.options.orders).filter((key) => {
38
+ const orders = unref(props.options.orders).filter((key) => {
32
39
  return unref(props.options.columns)[key]?.show !== false
33
40
  })
41
+ if (!props.selected) {
42
+ return orders
43
+ }
44
+ return ['__select', ...orders]
34
45
  })
35
46
 
36
47
  watch(() => ordersToShow.value, handleResize)
@@ -55,9 +66,6 @@ const cellOfColToGrow = computed(() => {
55
66
  return row.value?.children[colToGrow.value]
56
67
  })
57
68
 
58
- let headLock = false
59
- let bodyLock = false
60
-
61
69
  const colWidths = reactive<Record<string, string>>({})
62
70
  const blockWidth = ref<number | undefined>()
63
71
 
@@ -114,25 +122,80 @@ const recordsWithSummary = computed(() => {
114
122
  return summary ? [...records, summary] : records
115
123
  })
116
124
 
125
+ const indexes = computed(() => {
126
+ if (!props.selected) {
127
+ return []
128
+ }
129
+ const records = unref(props.options.records) ?? []
130
+ const indexField = unref(props.options.indexField)
131
+
132
+ return records.map((record, i) => indexField ? record[indexField] : i)
133
+ })
134
+
135
+ const selectedIndexes = reactive(new Set())
136
+
137
+ const control = computed({
138
+ get() {
139
+ if (!props.selected) {
140
+ return false
141
+ }
142
+ const selected = indexes.value.filter((index) => {
143
+ return selectedIndexes.has(index)
144
+ })
145
+
146
+ updateSelected(selected)
147
+
148
+ return selected.length === indexes.value.length
149
+ ? true
150
+ : selected.length ? 'indeterminate' : false
151
+ },
152
+
153
+ set(newValue) {
154
+ if (newValue === false) {
155
+ selectedIndexes.clear()
156
+ } else if (newValue === true) {
157
+ indexes.value.forEach((index) => {
158
+ selectedIndexes.add(index)
159
+ })
160
+ }
161
+ }
162
+ })
163
+
164
+ watch(indexes, (newValue, oldValue) => {
165
+ if (!props.selected) {
166
+ return
167
+ }
168
+ xor(newValue, oldValue).forEach((index) => {
169
+ selectedIndexes.delete(index)
170
+ })
171
+ })
172
+
117
173
  const virtualizerOptions = computed(() => ({
118
174
  count: recordsWithSummary.value.length,
119
175
  getScrollElement: () => body.value,
120
- estimateSize: () => unref(props.options.rowSize) ?? 41,
176
+ estimateSize: (index: number) => {
177
+ const rowSize = unref(props.options.rowSize) ?? 40
178
+ const borderSize = unref(props.options.borderSize) ?? 1
179
+ return lastRow(index) ? rowSize : rowSize + borderSize
180
+ },
121
181
  overscan: 10
122
182
  }))
123
183
 
124
184
  const rowVirtualizer = useVirtualizer(virtualizerOptions)
125
185
  const virtualItems = computed(() => rowVirtualizer.value.getVirtualItems())
126
186
 
127
- watch(() => props.options.records, () => {
128
- headLock = true
129
- bodyLock = true
187
+ let isSyncingHead = false
188
+ let isSyncingBody = false
189
+
190
+ watch(() => unref(props.options.records), () => {
191
+ isSyncingHead = true
192
+ isSyncingBody = true
130
193
  }, { flush: 'pre' })
131
194
 
132
- watch(() => props.options.records, () => {
195
+ watch(() => unref(props.options.records), () => {
133
196
  syncScroll(head.value, body.value)
134
- headLock = false
135
- bodyLock = false
197
+ isSyncingHead = false
198
+ isSyncingBody = false
136
199
  }, { flush: 'post' })
137
200
 
138
201
  useResizeObserver(block, ([entry]) => {
@@ -174,11 +237,19 @@ async function handleResize() {
174
237
  }
175
238
 
176
239
  function syncHeadScroll() {
177
- bodyLock || syncScroll(head.value, body.value)
240
+ if (!isSyncingHead) {
241
+ isSyncingBody = true
242
+ syncScroll(head.value, body.value)
243
+ }
244
+ isSyncingHead = false
178
245
  }
179
246
 
180
247
  function syncBodyScroll() {
181
- headLock || syncScroll(body.value, head.value)
248
+ if (!isSyncingBody) {
249
+ isSyncingHead = true
250
+ syncScroll(body.value, head.value)
251
+ }
252
+ isSyncingBody = false
182
253
  }
183
254
 
184
255
  function syncScroll(from: HTMLElement | null, to: HTMLElement | null) {
@@ -187,14 +258,6 @@ function syncScroll(from: HTMLElement | null, to: HTMLElement | null) {
187
258
  }
188
259
  }
189
260
 
190
- function lockHead(value: boolean) {
191
- headLock = value
192
- }
193
-
194
- function lockBody(value: boolean) {
195
- bodyLock = value
196
- }
197
-
198
261
  function updateColWidth(key: string, value: string, triggeredByUser = false) {
199
262
  colWidths[key] = value
200
263
  if (triggeredByUser && colToGrow.value >= 0) {
@@ -220,9 +283,18 @@ function lastRow(index: number) {
220
283
  }
221
284
 
222
285
  function getCell(key: string, index: number) {
286
+ if (key === '__select') {
287
+ return { type: 'custom' }
288
+ }
223
289
  const col = unref(props.options.columns)[key]
224
290
  return (isSummary(index) && col?.summaryCell) ? col?.summaryCell : col?.cell
225
291
  }
292
+
293
+ function updateSelected(selected: unknown[]) {
294
+ if (xor(selected, props.selected ?? []).length) {
295
+ emit('update:selected', selected)
296
+ }
297
+ }
226
298
  </script>
227
299
 
228
300
  <template>
@@ -233,16 +305,16 @@ function getCell(key: string, index: number) {
233
305
  :total="unref(options.total)"
234
306
  :reset="unref(options.reset)"
235
307
  :menu="unref(options.menu)"
308
+ :actions="unref(options.actions)"
236
309
  :borderless="unref(options.borderless)"
237
310
  :on-reset="options.onReset"
311
+ :selected="selected"
238
312
  />
239
313
 
240
314
  <div class="table" role="grid">
241
315
  <div
242
316
  class="container head"
243
317
  ref="head"
244
- @mouseenter="lockHead(true)"
245
- @mouseleave="lockHead(false)"
246
318
  @scroll="syncHeadScroll"
247
319
  >
248
320
  <div class="block" ref="block">
@@ -251,18 +323,23 @@ function getCell(key: string, index: number) {
251
323
  v-for="key in ordersToShow"
252
324
  :key="key"
253
325
  :name="key"
254
- :class-name="unref(options.columns)[key].className"
326
+ :class-name="unref(options.columns)[key]?.className"
255
327
  :width="colWidths[key]"
256
328
  >
257
329
  <STableColumn
258
330
  :name="key"
259
- :label="unref(options.columns)[key].label"
260
- :class-name="unref(options.columns)[key].className"
261
- :dropdown="unref(options.columns)[key].dropdown"
331
+ :label="unref(options.columns)[key]?.label"
332
+ :class-name="unref(options.columns)[key]?.className"
333
+ :dropdown="unref(options.columns)[key]?.dropdown"
262
334
  :has-header="showHeader"
263
- :resizable="unref(options.columns)[key].resizable"
335
+ :resizable="unref(options.columns)[key]?.resizable"
264
336
  @resize="(value) => updateColWidth(key, value, true)"
265
- />
337
+ >
338
+ <SInputCheckbox
339
+ v-if="key === '__select' && unref(options.records)?.length"
340
+ v-model="control"
341
+ />
342
+ </STableColumn>
266
343
  </STableItem>
267
344
  </div>
268
345
  </div>
@@ -272,8 +349,6 @@ function getCell(key: string, index: number) {
272
349
  v-if="!unref(options.loading) && unref(options.records)?.length"
273
350
  class="container body"
274
351
  ref="body"
275
- @mouseenter="lockBody(true)"
276
- @mouseleave="lockBody(false)"
277
352
  @scroll="syncBodyScroll"
278
353
  >
279
354
  <div
@@ -304,18 +379,24 @@ function getCell(key: string, index: number) {
304
379
  v-for="key in ordersToShow"
305
380
  :key="key"
306
381
  :name="key"
307
- :class-name="unref(options.columns)[key].className"
382
+ :class-name="unref(options.columns)[key]?.className"
308
383
  :width="colWidths[key]"
309
384
  >
310
385
  <STableCell
311
386
  :name="key"
312
387
  :class="isSummary(index) && 'summary'"
313
- :class-name="unref(options.columns)[key].className"
388
+ :class-name="unref(options.columns)[key]?.className"
314
389
  :cell="getCell(key, index)"
315
390
  :value="recordsWithSummary[index][key]"
316
391
  :record="recordsWithSummary[index]"
317
392
  :records="unref(options.records)!"
318
- />
393
+ >
394
+ <SInputCheckbox
395
+ v-if="key === '__select' && !isSummary(index)"
396
+ :value="selectedIndexes.has(indexes[index])"
397
+ @change="c => selectedIndexes[c ? 'add' : 'delete'](indexes[index])"
398
+ />
399
+ </STableCell>
319
400
  </STableItem>
320
401
  </div>
321
402
  </div>
@@ -380,9 +461,10 @@ function getCell(key: string, index: number) {
380
461
  position: var(--table-head-position, static);
381
462
  top: var(--table-head-top, auto);
382
463
  z-index: 100;
383
- border-radius: var(--table-border-radius) var(--table-border-radius) 0 0;
464
+ border-radius: calc(var(--table-border-radius) - 1px) calc(var(--table-border-radius) - 1px) 0 0;
384
465
  background-color: var(--bg-elv-2);
385
466
  scrollbar-width: none;
467
+ line-height: 0;
386
468
 
387
469
  &::-webkit-scrollbar {
388
470
  display: none;
@@ -394,7 +476,7 @@ function getCell(key: string, index: number) {
394
476
  }
395
477
 
396
478
  .container.body {
397
- border-radius: 6px 6px var(--table-border-radius) var(--table-border-radius);
479
+ border-radius: 0 0 calc(var(--table-border-radius) - 1px) calc(var(--table-border-radius) - 1px);
398
480
  line-height: 0;
399
481
  max-height: var(--table-max-height, 100%);
400
482
 
@@ -419,7 +501,7 @@ function getCell(key: string, index: number) {
419
501
  }
420
502
 
421
503
  .missing {
422
- border-radius: 0 0 6px 6px;
504
+ border-radius: 0 0 calc(var(--table-border-radius) - 1px) calc(var(--table-border-radius) - 1px);
423
505
  padding: 48px 32px;
424
506
  text-align: center;
425
507
  background-color: var(--c-bg-elv-3);
@@ -427,10 +509,14 @@ function getCell(key: string, index: number) {
427
509
  font-size: 14px;
428
510
  font-weight: 500;
429
511
  color: var(--c-text-3);
512
+
513
+ .has-footer & {
514
+ border-radius: 0;
515
+ }
430
516
  }
431
517
 
432
518
  .loading {
433
- border-radius: 0 0 6px 6px;
519
+ border-radius: 0 0 calc(var(--table-border-radius) - 1px) calc(var(--table-border-radius) - 1px);
434
520
  padding: 64px 32px;
435
521
  background-color: var(--c-bg-elv-3);
436
522
  }
@@ -446,4 +532,19 @@ function getCell(key: string, index: number) {
446
532
  height: 48px;
447
533
  color: var(--c-text-1);
448
534
  }
535
+
536
+ .STable .col-__select {
537
+ --table-padding-left: 0;
538
+ --table-col-width: 48px;
539
+
540
+ :deep(.input) {
541
+ align-items: center;
542
+ padding: 0 16px;
543
+ min-height: 40px;
544
+ }
545
+
546
+ :deep(.container) {
547
+ padding: 0;
548
+ }
549
+ }
449
550
  </style>
@@ -3,6 +3,7 @@ import { computed } from 'vue'
3
3
  import { type TableCell } from '../composables/Table'
4
4
  import STableCellAvatar from './STableCellAvatar.vue'
5
5
  import STableCellAvatars from './STableCellAvatars.vue'
6
+ import STableCellCustom from './STableCellCustom.vue'
6
7
  import STableCellDay from './STableCellDay.vue'
7
8
  import STableCellEmpty from './STableCellEmpty.vue'
8
9
  import STableCellNumber from './STableCellNumber.vue'
@@ -98,6 +99,11 @@ const computedCell = computed<TableCell | undefined>(() =>
98
99
  :avatars="computedCell.avatars"
99
100
  :color="computedCell.color"
100
101
  />
102
+ <STableCellCustom
103
+ v-else-if="computedCell.type === 'custom'"
104
+ >
105
+ <slot />
106
+ </STableCellCustom>
101
107
  <STableCellEmpty
102
108
  v-else-if="computedCell.type === 'empty'"
103
109
  />
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <div class="STableCellCustom">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <style scoped lang="postcss">
8
+ .STableCellCustom {
9
+ min-height: 40px;
10
+ }
11
+ </style>
@@ -125,21 +125,23 @@ function stopDialogPositionListener() {
125
125
  <template>
126
126
  <div class="STableColumn STableCell" :class="classes" ref="column">
127
127
  <div class="container">
128
- <p class="label">{{ label }}</p>
129
-
130
- <div v-if="dropdown" class="action" ref="container">
131
- <button class="button" :class="{ active: buttonActive }" @click="toggle">
132
- <SIcon :icon="IconDotsThree" class="icon" />
133
- </button>
134
-
135
- <transition name="fade">
136
- <div v-if="isOpen" class="dialog" :style="{ top, left }" ref="dialog">
137
- <SDropdown :sections="dropdown" />
138
- </div>
139
- </transition>
140
- </div>
141
-
142
- <div v-if="resizable" class="grip" @mousedown="grip" />
128
+ <slot>
129
+ <p class="label">{{ label }}</p>
130
+
131
+ <div v-if="dropdown" class="action" ref="container">
132
+ <button class="button" :class="{ active: buttonActive }" @click="toggle">
133
+ <SIcon :icon="IconDotsThree" class="icon" />
134
+ </button>
135
+
136
+ <transition name="fade">
137
+ <div v-if="isOpen" class="dialog" :style="{ top, left }" ref="dialog">
138
+ <SDropdown :sections="dropdown" />
139
+ </div>
140
+ </transition>
141
+ </div>
142
+
143
+ <div v-if="resizable" class="grip" @mousedown="grip" />
144
+ </slot>
143
145
  </div>
144
146
  </div>
145
147
  </template>
@@ -239,13 +241,12 @@ function stopDialogPositionListener() {
239
241
  }
240
242
 
241
243
  .grip {
242
- position: relative;
244
+ position: absolute;
243
245
  right: -8px;
244
246
  top: 0px;
245
247
  bottom: 0px;
246
248
  width: 16px;
247
249
  z-index: 1;
248
- position: absolute;
249
250
  cursor: col-resize;
250
251
 
251
252
  &::before {
@@ -57,7 +57,7 @@ const hasNext = computed(() => {
57
57
  <style scoped lang="postcss">
58
58
  .STableFooter {
59
59
  border-top: 1px solid var(--c-divider-2);
60
- border-radius: 0 0 6px 6px;
60
+ border-radius: 0 0 calc(var(--table-border-radius) - 1px) calc(var(--table-border-radius) - 1px);
61
61
  padding-right: var(--table-padding-right);
62
62
  padding-left: var(--table-padding-left);
63
63
  background-color: var(--c-bg-elv-3);
@@ -1,29 +1,51 @@
1
1
  <script setup lang="ts">
2
- import { type TableMenu } from '../composables/Table'
2
+ import { computed } from 'vue'
3
+ import { type TableAction, type TableMenu } from '../composables/Table'
3
4
  import { format } from '../support/Num'
4
5
  import { isNullish } from '../support/Utils'
6
+ import STableHeaderActions from './STableHeaderActions.vue'
5
7
  import STableHeaderMenu from './STableHeaderMenu.vue'
6
8
 
7
- defineProps<{
9
+ const props = defineProps<{
8
10
  total?: number | null
9
11
  reset?: boolean
10
12
  menu?: TableMenu[] | TableMenu[][]
13
+ actions?: TableAction[]
11
14
  borderless?: boolean
12
15
  onReset?(): void
16
+ selected?: unknown[]
13
17
  }>()
18
+
19
+ const stats = computed(() => {
20
+ if (isNullish(props.total)) {
21
+ return null
22
+ }
23
+
24
+ return props.selected?.length
25
+ ? `${format(props.selected.length)} of ${props.total} selected`
26
+ : `${format(props.total)} ${props.total > 1 ? 'records' : 'record'}`
27
+ })
28
+
29
+ // deprecated `reset` prop in favor of `actions`, remove this in next major version
30
+ const resetAction = computed(() => {
31
+ return props.reset
32
+ ? [{ label: 'Reset filters', onClick: props.onReset, type: 'info' }]
33
+ : []
34
+ })
14
35
  </script>
15
36
 
16
37
  <template>
17
38
  <div class="STableHeader" :class="{ borderless }">
18
39
  <div class="container">
19
- <div class="stat">
20
- <p v-if="!isNullish(total)" class="total">
21
- {{ format(total) }} {{ (total) > 1 ? 'records' : 'record' }}
22
- </p>
23
- <div v-if="reset" class="reset">
24
- <button class="button" @click="onReset">
25
- Reset filters
26
- </button>
40
+ <div class="primary">
41
+ <div v-if="stats" class="stats">
42
+ {{ stats }}
43
+ </div>
44
+ <div v-if="actions?.length" class="actions">
45
+ <STableHeaderActions :actions="actions" />
46
+ </div>
47
+ <div v-else-if="resetAction.length">
48
+ <STableHeaderActions :actions="resetAction" />
27
49
  </div>
28
50
  </div>
29
51
  <div v-if="menu && menu.length" class="menu">
@@ -35,10 +57,14 @@ defineProps<{
35
57
 
36
58
  <style scoped lang="postcss">
37
59
  .STableHeader {
38
- border-radius: 6px 6px 0 0;
60
+ border-radius: calc(var(--table-border-radius) - 1px) calc(var(--table-border-radius) - 1px) 0 0;
39
61
  padding-right: var(--table-padding-right);
40
62
  padding-left: var(--table-padding-left);
41
63
  background-color: var(--c-bg-soft);
64
+
65
+ &.borderless {
66
+ border-radius: 0;
67
+ }
42
68
  }
43
69
 
44
70
  .container {
@@ -46,14 +72,15 @@ defineProps<{
46
72
  min-height: 48px;
47
73
  }
48
74
 
49
- .stat {
75
+ .primary {
50
76
  display: flex;
77
+ gap: 16px;
51
78
  flex-grow: 1;
52
79
  padding: 0 16px;
80
+ min-height: 48px;
53
81
  }
54
82
 
55
- .total {
56
- margin: 0;
83
+ .stats {
57
84
  padding: 12px 0;
58
85
  line-height: 24px;
59
86
  font-size: 12px;
@@ -61,37 +88,6 @@ defineProps<{
61
88
  color: var(--c-text-2);
62
89
  }
63
90
 
64
- .reset {
65
- position: relative;
66
-
67
- .total + & {
68
- margin-left: 16px;
69
- }
70
-
71
- .total + &::before {
72
- display: inline-block;
73
- margin-right: 16px;
74
- width: 1px;
75
- height: 16px;
76
- background-color: var(--c-divider-2);
77
- content: "";
78
- transform: translateY(4px);
79
- }
80
- }
81
-
82
- .button {
83
- padding: 12px 0;
84
- line-height: 24px;
85
- font-size: 12px;
86
- font-weight: 500;
87
- color: var(--c-info-text);
88
- transition: color 0.25s;
89
-
90
- &:hover {
91
- color: var(--c-info-text-dark);
92
- }
93
- }
94
-
95
91
  .menu {
96
92
  flex-shrink: 0;
97
93
  padding-right: 8px;
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import { type TableAction } from '../composables/Table'
3
+ import SButton from './SButton.vue'
4
+
5
+ withDefaults(defineProps<TableAction>(), {
6
+ show: true,
7
+ mode: 'mute',
8
+ labelMode: 'neutral'
9
+ })
10
+ </script>
11
+
12
+ <template>
13
+ <div v-if="show" class="STableHeaderActionItem">
14
+ <SButton
15
+ size="mini"
16
+ :mode="mode"
17
+ :label="label"
18
+ :label-mode="labelMode"
19
+ @click="onClick"
20
+ />
21
+ </div>
22
+ </template>
23
+
24
+ <style scoped lang="postcss">
25
+ .STableHeaderActionItem {
26
+ padding: 10px 0;
27
+ }
28
+ </style>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ import { type TableAction } from '../composables/Table'
3
+ import STableHeaderActionItem from './STableHeaderActionItem.vue'
4
+
5
+ defineProps<{ actions: TableAction[] }>()
6
+ </script>
7
+
8
+ <template>
9
+ <div class="STableHeaderActions">
10
+ <STableHeaderActionItem v-for="action in actions" :key="action.label" v-bind="action" />
11
+ </div>
12
+ </template>
13
+
14
+ <style scoped lang="postcss">
15
+ .STableHeaderActions {
16
+ display: flex;
17
+ flex-grow: 1;
18
+ gap: 8px;
19
+ }
20
+ </style>
@@ -25,7 +25,7 @@ const classes = computed(() => [
25
25
  <style scoped lang="postcss">
26
26
  .STableItem {
27
27
  position: var(--table-col-position, relative);
28
- left: 0;
28
+ left: var(--table-col-left, 0);
29
29
  z-index: var(--table-col-z-index, auto);
30
30
  flex-shrink: 0;
31
31
  flex-grow: 1;
@@ -1,5 +1,5 @@
1
- import { type MaybeRef, useElementBounding, useWindowSize } from '@vueuse/core'
2
- import { type Component, type Ref, ref, unref } from 'vue'
1
+ import { useElementBounding, useWindowSize } from '@vueuse/core'
2
+ import { type Component, type MaybeRef, type Ref, ref, unref } from 'vue'
3
3
 
4
4
  export type DropdownSection =
5
5
  | DropdownSectionMenu
@@ -1,16 +1,16 @@
1
- import { type MaybeRef } from '@vueuse/core'
2
- import { type Component } from 'vue'
1
+ import { type Component, type MaybeRef } from 'vue'
3
2
  import { type Day } from '../support/Day'
4
3
  import { type DropdownSection } from './Dropdown'
5
4
 
6
5
  export interface Table<
7
- O extends string = any,
6
+ O extends string = string,
8
7
  R extends Record<string, any> = any,
9
8
  SR extends Record<string, any> = any
10
9
  > {
11
10
  orders: MaybeRef<O[]>
12
11
  columns: MaybeRef<TableColumns<O, R, SR>>
13
12
  menu?: MaybeRef<TableMenu[] | TableMenu[][]>
13
+ actions?: MaybeRef<TableAction[]>
14
14
  records?: MaybeRef<R[] | null | undefined>
15
15
  header?: MaybeRef<boolean | undefined>
16
16
  footer?: MaybeRef<boolean | undefined>
@@ -18,12 +18,16 @@ export interface Table<
18
18
  total?: MaybeRef<number | null | undefined>
19
19
  page?: MaybeRef<number | null | undefined>
20
20
  perPage?: MaybeRef<number | null | undefined>
21
+ /** @deprecated use `actions` instead */
21
22
  reset?: MaybeRef<boolean | undefined>
22
23
  borderless?: MaybeRef<boolean>
23
24
  loading?: MaybeRef<boolean | undefined>
24
25
  rowSize?: MaybeRef<number | undefined>
26
+ borderSize?: MaybeRef<number | undefined>
27
+ indexField?: keyof R
25
28
  onPrev?(): void
26
29
  onNext?(): void
30
+ /** @deprecated use `actions` instead */
27
31
  onReset?(): void
28
32
  }
29
33
 
@@ -57,6 +61,7 @@ export type TableCell =
57
61
  | TableCellState
58
62
  | TableCellAvatar
59
63
  | TableCellAvatars
64
+ | TableCellCustom
60
65
  | TableCellEmpty
61
66
  | TableCellComponent
62
67
 
@@ -69,6 +74,7 @@ export type TableCellType =
69
74
  | 'state'
70
75
  | 'avatar'
71
76
  | 'avatars'
77
+ | 'custom'
72
78
  | 'empty'
73
79
  | 'component'
74
80
 
@@ -154,6 +160,10 @@ export interface TableCellAvatarsOption {
154
160
  name?: string | null
155
161
  }
156
162
 
163
+ export interface TableCellCustom extends TableCellBase {
164
+ type: 'custom'
165
+ }
166
+
157
167
  export interface TableCellEmpty extends TableCellBase {
158
168
  type: 'empty'
159
169
  }
@@ -176,6 +186,14 @@ export interface TableMenu {
176
186
  dropdown: DropdownSection[]
177
187
  }
178
188
 
189
+ export interface TableAction {
190
+ show?: boolean
191
+ mode?: 'neutral' | 'mute' | 'info' | 'success' | 'warning' | 'danger'
192
+ label: string
193
+ labelMode?: 'neutral' | 'mute' | 'info' | 'success' | 'warning' | 'danger'
194
+ onClick?(): void
195
+ }
196
+
179
197
  export function useTable<
180
198
  O extends string,
181
199
  R extends Record<string, any>,
@@ -1,5 +1,4 @@
1
- import { type MaybeRefOrGetter, resolveUnref } from '@vueuse/core'
2
- import { type ComputedRef, computed, useSlots } from 'vue'
1
+ import { type ComputedRef, type MaybeRefOrGetter, computed, toValue, useSlots } from 'vue'
3
2
  import { isArray, isString } from '../support/Utils'
4
3
 
5
4
  export type WhenCondition<T> = MaybeRefOrGetter<T>
@@ -21,7 +20,7 @@ export function computedWhen<T, C, D>(
21
20
  whenFalse?: D
22
21
  ): ComputedRef<T | D> {
23
22
  return computed(() => {
24
- const c = resolveUnref(condition)
23
+ const c = toValue(condition)
25
24
 
26
25
  return c ? fn(c) : whenFalse as D
27
26
  })
@@ -1,5 +1,4 @@
1
1
  @import "normalize.css";
2
2
  @import "v-calendar/dist/style.css";
3
- @import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
4
3
  @import "./variables";
5
4
  @import "./base";
@@ -1,9 +1,3 @@
1
1
  declare module 'v-calendar' {
2
2
  export const DatePicker: any
3
3
  }
4
-
5
- declare module 'vue-virtual-scroller' {
6
- export const RecycleScroller: any
7
- export const DynamicScroller: any
8
- export const DynamicScrollerItem: any
9
- }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@globalbrain/sefirot",
3
- "version": "2.46.0",
4
- "packageManager": "pnpm@8.7.4",
3
+ "version": "2.47.0",
4
+ "packageManager": "pnpm@8.7.5",
5
5
  "description": "Vue Components for Global Brain Design System.",
6
6
  "author": "Kia Ishii <ka.ishii@globalbrains.com>",
7
7
  "license": "MIT",