@globalbrain/sefirot 2.30.0 → 2.31.1

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,5 +1,15 @@
1
1
  <script setup lang="ts">
2
- import { computed, reactive, shallowRef, toRefs, watch } from 'vue'
2
+ import { useResizeObserver } from '@vueuse/core'
3
+ import {
4
+ computed,
5
+ nextTick,
6
+ reactive,
7
+ ref,
8
+ shallowRef,
9
+ unref,
10
+ watch
11
+ } from 'vue'
12
+ import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
3
13
  import type { Table } from '../composables/Table'
4
14
  import SSpinner from './SSpinner.vue'
5
15
  import STableCell from './STableCell.vue'
@@ -12,83 +22,141 @@ const props = defineProps<{
12
22
  options: Table
13
23
  }>()
14
24
 
15
- const {
16
- orders,
17
- columns,
18
- records,
19
- header,
20
- footer,
21
- summary,
22
- total,
23
- page,
24
- perPage,
25
- reset,
26
- borderless,
27
- loading,
28
- onPrev,
29
- onNext,
30
- onReset
31
- } = toRefs(props.options)
32
-
33
25
  const head = shallowRef<HTMLElement | null>(null)
34
26
  const body = shallowRef<HTMLElement | null>(null)
27
+ const block = shallowRef<HTMLElement | null>(null)
28
+ const row = shallowRef<HTMLElement | null>(null)
29
+
30
+ const colToGrowAdjusted = ref(false)
31
+
32
+ const colToGrow = computed(() => {
33
+ if (colToGrowAdjusted.value) {
34
+ return -1
35
+ }
36
+
37
+ return unref(props.options.orders).findIndex((key) => {
38
+ return unref(props.options.columns)[key]?.grow
39
+ }) ?? -1
40
+ })
41
+
42
+ const nameOfColToGrow = computed(() => {
43
+ return unref(props.options.orders)[colToGrow.value]
44
+ })
45
+
46
+ const cellOfColToGrow = computed(() => {
47
+ return row.value?.children[colToGrow.value]
48
+ })
35
49
 
36
50
  let headLock = false
37
51
  let bodyLock = false
38
52
 
39
53
  const colWidths = reactive<Record<string, string>>({})
54
+ const blockWidth = ref<number | undefined>()
40
55
 
41
56
  const showHeader = computed(() => {
42
- if (header?.value === true) {
43
- return true
44
- }
57
+ const header = unref(props.options.header)
45
58
 
46
- if (header?.value === false) {
47
- return false
59
+ if (header != null) {
60
+ return header
48
61
  }
49
62
 
50
- return total?.value !== undefined || !!reset?.value
63
+ return unref(props.options.total) != null || !!unref(props.options.reset)
51
64
  })
52
65
 
53
66
  const showFooter = computed(() => {
54
- if (loading?.value) {
67
+ if (unref(props.options.loading)) {
55
68
  return false
56
69
  }
57
70
 
58
- if (footer?.value === true) {
59
- return true
60
- }
71
+ const footer = unref(props.options.footer)
61
72
 
62
- if (footer?.value === false) {
63
- return false
73
+ if (footer != null) {
74
+ return footer
64
75
  }
65
76
 
66
- return page?.value && perPage?.value && total?.value
77
+ return (
78
+ unref(props.options.page)
79
+ && unref(props.options.perPage)
80
+ && unref(props.options.total)
81
+ )
82
+ })
83
+
84
+ const showMissing = computed(() => {
85
+ return (
86
+ !unref(props.options.loading)
87
+ && unref(props.options.records)
88
+ && !unref(props.options.records)!.length
89
+ )
67
90
  })
68
91
 
69
92
  const classes = computed(() => ({
70
93
  'has-header': showHeader.value,
71
94
  'has-footer': showFooter.value,
72
- 'borderless': borderless?.value
95
+ 'borderless': unref(props.options.borderless)
73
96
  }))
74
97
 
75
98
  const recordsWithSummary = computed(() => {
76
- return (records?.value && summary?.value)
77
- ? [...records?.value, summary?.value]
78
- : records?.value ?? []
99
+ const records = unref(props.options.records) ?? []
100
+ const summary = unref(props.options.summary)
101
+
102
+ const res = summary ? [...records, summary] : records
103
+
104
+ res.forEach((record, index) => {
105
+ record.__index = index
106
+ })
107
+
108
+ return res
79
109
  })
80
110
 
81
- watch(() => records?.value, () => {
111
+ watch(() => props.options.records, () => {
82
112
  headLock = true
83
113
  bodyLock = true
84
114
  }, { flush: 'pre' })
85
115
 
86
- watch(() => records?.value, () => {
116
+ watch(() => props.options.records, () => {
87
117
  syncScroll(head.value, body.value)
88
118
  headLock = false
89
119
  bodyLock = false
90
120
  }, { flush: 'post' })
91
121
 
122
+ useResizeObserver(block, ([entry]) => {
123
+ blockWidth.value = entry.contentRect.width
124
+ })
125
+
126
+ const resizeObserver = useResizeObserver(head, handleResize)
127
+
128
+ function stopObserving() {
129
+ const orders = unref(props.options.orders)
130
+ colWidths[orders[orders.length - 1]] = 'auto'
131
+ resizeObserver.stop()
132
+ }
133
+
134
+ async function handleResize() {
135
+ if (colToGrow.value < 0 || !cellOfColToGrow.value || !row.value) {
136
+ stopObserving()
137
+ return
138
+ }
139
+
140
+ const initialWidth = getComputedStyle(cellOfColToGrow.value)
141
+ .getPropertyValue('--table-col-width')
142
+ .trim()
143
+
144
+ updateColWidth(nameOfColToGrow.value, initialWidth)
145
+
146
+ await nextTick()
147
+
148
+ let totalWidth = 0
149
+ for (const el of row.value.children) {
150
+ totalWidth += el.getBoundingClientRect().width
151
+ }
152
+
153
+ const availableFill = row.value.getBoundingClientRect().width - totalWidth
154
+ updateColWidth(
155
+ nameOfColToGrow.value,
156
+ `calc(${availableFill}px + ${initialWidth})`
157
+ )
158
+ }
159
+
92
160
  function syncHeadScroll() {
93
161
  bodyLock || syncScroll(head.value, body.value)
94
162
  }
@@ -111,30 +179,45 @@ function lockBody(value: boolean) {
111
179
  bodyLock = value
112
180
  }
113
181
 
114
- function updateColWidth(key: string, value: string) {
182
+ function updateColWidth(key: string, value: string, triggeredByUser = false) {
115
183
  colWidths[key] = value
184
+ if (triggeredByUser && colToGrow.value >= 0) {
185
+ colToGrowAdjusted.value = true
186
+ stopObserving()
187
+ }
188
+ }
189
+
190
+ function isSummaryOrLastClass(index: number) {
191
+ if (isSummary(index)) {
192
+ return 'summary'
193
+ }
194
+
195
+ return lastRow(index) ? 'last' : ''
116
196
  }
117
197
 
118
198
  function isSummary(index: number) {
119
- return index === records?.value?.length
199
+ return index === unref(props.options.records)?.length
200
+ }
201
+
202
+ function lastRow(index: number) {
203
+ return index === recordsWithSummary.value.length - 1
120
204
  }
121
205
 
122
206
  function getCell(key: string, index: number) {
123
- return (isSummary(index) && columns.value[key]?.summaryCell)
124
- ? columns.value[key]?.summaryCell
125
- : columns.value[key]?.cell
207
+ const col = unref(props.options.columns)[key]
208
+ return (isSummary(index) && col?.summaryCell) ? col?.summaryCell : col?.cell
126
209
  }
127
210
  </script>
128
211
 
129
212
  <template>
130
- <div class="STable" :class="classes" ref="el">
213
+ <div class="STable" :class="classes">
131
214
  <div class="box">
132
215
  <STableHeader
133
216
  v-if="showHeader"
134
- :total="total"
135
- :reset="reset"
136
- :borderless="borderless"
137
- :on-reset="onReset"
217
+ :total="unref(options.total)"
218
+ :reset="unref(options.reset)"
219
+ :borderless="unref(options.borderless)"
220
+ :on-reset="options.onReset"
138
221
  />
139
222
 
140
223
  <div class="table" role="grid">
@@ -145,23 +228,23 @@ function getCell(key: string, index: number) {
145
228
  @mouseleave="lockHead(false)"
146
229
  @scroll="syncHeadScroll"
147
230
  >
148
- <div class="block">
149
- <div class="row">
231
+ <div class="block" ref="block">
232
+ <div class="row" ref="row">
150
233
  <STableItem
151
- v-for="key in orders"
234
+ v-for="key in unref(options.orders)"
152
235
  :key="key"
153
236
  :name="key"
154
- :class-name="columns[key].className"
237
+ :class-name="unref(options.columns)[key].className"
155
238
  :width="colWidths[key]"
156
239
  >
157
240
  <STableColumn
158
241
  :name="key"
159
- :label="columns[key].label"
160
- :class-name="columns[key].className"
161
- :dropdown="columns[key].dropdown"
242
+ :label="unref(options.columns)[key].label"
243
+ :class-name="unref(options.columns)[key].className"
244
+ :dropdown="unref(options.columns)[key].dropdown"
162
245
  :has-header="showHeader"
163
- :resizable="columns[key].resizable"
164
- @resize="(value) => updateColWidth(key, value)"
246
+ :resizable="unref(options.columns)[key].resizable"
247
+ @resize="(value) => updateColWidth(key, value, true)"
165
248
  />
166
249
  </STableItem>
167
250
  </div>
@@ -169,49 +252,57 @@ function getCell(key: string, index: number) {
169
252
  </div>
170
253
 
171
254
  <div
172
- v-if="!loading && records && records.length"
255
+ v-if="!unref(options.loading) && unref(options.records)?.length"
173
256
  class="container body"
174
257
  ref="body"
175
258
  @mouseenter="lockBody(true)"
176
259
  @mouseleave="lockBody(false)"
177
260
  @scroll="syncBodyScroll"
178
261
  >
179
- <div class="block">
180
- <div
181
- v-for="(record, rIndex) in recordsWithSummary"
182
- :key="rIndex"
183
- class="row"
184
- :class="isSummary(rIndex) && 'summary'"
185
- >
186
- <STableItem
187
- v-for="key in orders"
188
- :key="key"
189
- :name="key"
190
- :class-name="columns[key].className"
191
- :width="colWidths[key]"
262
+ <DynamicScroller
263
+ :items="recordsWithSummary"
264
+ :min-item-size="40"
265
+ key-field="__index"
266
+ class="block"
267
+ :style="blockWidth ? { width: `${blockWidth}px` } : undefined"
268
+ :prerender="Math.min(10, recordsWithSummary.length)"
269
+ >
270
+ <template #default="{ item: record, index: rIndex, active }">
271
+ <DynamicScrollerItem
272
+ :item="record"
273
+ :active="active"
274
+ :data-index="rIndex"
192
275
  >
193
- <STableCell
194
- :name="key"
195
- :class="isSummary(rIndex) && 'summary'"
196
- :class-name="columns[key].className"
197
- :cell="getCell(key, rIndex)"
198
- :value="record[key]"
199
- :record="record"
200
- :records="records"
201
- />
202
- </STableItem>
203
- </div>
204
- </div>
276
+ <div class="row" :class="isSummaryOrLastClass(rIndex)">
277
+ <STableItem
278
+ v-for="key in unref(options.orders)"
279
+ :key="key"
280
+ :name="key"
281
+ :class-name="unref(options.columns)[key].className"
282
+ :width="colWidths[key]"
283
+ >
284
+ <STableCell
285
+ :name="key"
286
+ :class="isSummary(rIndex) && 'summary'"
287
+ :class-name="unref(options.columns)[key].className"
288
+ :cell="getCell(key, rIndex)"
289
+ :value="record[key]"
290
+ :record="record"
291
+ :records="unref(options.records)!"
292
+ />
293
+ </STableItem>
294
+ </div>
295
+ </DynamicScrollerItem>
296
+ </template>
297
+ </DynamicScroller>
205
298
  </div>
206
299
  </div>
207
300
 
208
- <div v-if="!loading && records && !records.length" class="missing">
209
- <p class="missing-text">
210
- No results matched your search.
211
- </p>
301
+ <div v-if="showMissing" class="missing">
302
+ <p class="missing-text">No results matched your search.</p>
212
303
  </div>
213
304
 
214
- <div v-if="loading" class="loading">
305
+ <div v-if="unref(options.loading)" class="loading">
215
306
  <div class="loading-icon">
216
307
  <SSpinner class="loading-svg" />
217
308
  </div>
@@ -219,12 +310,12 @@ function getCell(key: string, index: number) {
219
310
 
220
311
  <STableFooter
221
312
  v-if="showFooter"
222
- :total="total"
223
- :page="page"
224
- :per-page="perPage"
225
- :borderless="borderless"
226
- :on-prev="onPrev"
227
- :on-next="onNext"
313
+ :total="unref(options.total)"
314
+ :page="unref(options.page)"
315
+ :per-page="unref(options.perPage)"
316
+ :borderless="unref(options.borderless)"
317
+ :on-prev="options.onPrev"
318
+ :on-next="options.onNext"
228
319
  />
229
320
  </div>
230
321
  </div>
@@ -266,22 +357,29 @@ function getCell(key: string, index: number) {
266
357
  z-index: 100;
267
358
  border-radius: var(--table-border-radius) var(--table-border-radius) 0 0;
268
359
  background-color: var(--bg-elv-2);
269
-
270
- .STable.has-header & {
271
- border-radius: 0;
272
- }
360
+ scrollbar-width: none;
273
361
 
274
362
  &::-webkit-scrollbar {
275
363
  display: none;
276
364
  }
365
+
366
+ .STable.has-header & {
367
+ border-radius: 0;
368
+ }
277
369
  }
278
370
 
279
371
  .container.body {
280
372
  border-radius: 6px 6px var(--table-border-radius) var(--table-border-radius);
373
+ line-height: 0;
281
374
 
282
375
  .STable.has-footer & {
283
376
  border-radius: 0;
284
377
  }
378
+
379
+ .block {
380
+ max-height: var(--table-max-height, 100%);
381
+ overflow-y: auto;
382
+ }
285
383
  }
286
384
 
287
385
  .block {
@@ -289,13 +387,14 @@ function getCell(key: string, index: number) {
289
387
  min-width: 100%;
290
388
  }
291
389
 
292
- .row {
390
+ :deep(.row) {
293
391
  display: flex;
294
392
  border-bottom: 1px solid var(--c-divider-2);
393
+ }
295
394
 
296
- .body &:last-child {
297
- border-bottom: 0;
298
- }
395
+ :deep(.row.last),
396
+ :deep(.row.summary) {
397
+ border-bottom: 0;
299
398
  }
300
399
 
301
400
  .missing {
@@ -7,6 +7,7 @@ import STableCellDay from './STableCellDay.vue'
7
7
  import STableCellEmpty from './STableCellEmpty.vue'
8
8
  import STableCellPill from './STableCellPill.vue'
9
9
  import STableCellPills from './STableCellPills.vue'
10
+ import STableCellState from './STableCellState.vue'
10
11
  import STableCellText from './STableCellText.vue'
11
12
 
12
13
  const props = defineProps<{
@@ -59,6 +60,11 @@ const computedCell = computed<TableCell | undefined>(() =>
59
60
  :record="record"
60
61
  :pills="computedCell.pills"
61
62
  />
63
+ <STableCellState
64
+ v-else-if="computedCell.type === 'state'"
65
+ :value="computedCell.label"
66
+ :mode="computedCell.mode"
67
+ />
62
68
  <STableCellAvatar
63
69
  v-else-if="computedCell.type === 'avatar'"
64
70
  :value="value"
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import SState, { type Mode } from './SState.vue'
3
+
4
+ defineProps<{
5
+ value?: string
6
+ mode?: Mode
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <div class="STableCellState" :class="[mode ?? 'neutral']">
12
+ <SState
13
+ v-if="value"
14
+ :mode="mode"
15
+ :label="value"
16
+ />
17
+ </div>
18
+ </template>
19
+
20
+ <style scoped lang="postcss">
21
+ .STableCellState {
22
+ display: flex;
23
+ align-items: center;
24
+ padding: 8px 16px;
25
+ min-height: 40px;
26
+ }
27
+ </style>
@@ -1,16 +1,23 @@
1
1
  <script setup lang="ts">
2
- defineProps<{
2
+ import { computed } from 'vue'
3
+
4
+ const props = defineProps<{
3
5
  name: string
4
6
  className?: string
5
7
  width?: string
6
8
  }>()
9
+
10
+ const classes = computed(() => [
11
+ 'STableItem',
12
+ props.className,
13
+ `col-${props.name}`,
14
+ { adjusted: props.width },
15
+ { auto: props.width === 'auto' }
16
+ ])
7
17
  </script>
8
18
 
9
19
  <template>
10
- <div
11
- class="STableItem"
12
- :class="[className, `col-${name}`, { adjusted: width }]"
13
- >
20
+ <div :class="classes">
14
21
  <slot />
15
22
  </div>
16
23
  </template>
@@ -24,7 +31,7 @@ defineProps<{
24
31
  flex-grow: 1;
25
32
  border-right: 1px solid var(--c-divider-light);
26
33
  min-width: var(--table-col-width);
27
- max-width: var(--table-col-max-width, var(--table-col-width));
34
+ max-width: var(--table-col-width);
28
35
 
29
36
  &:last-child {
30
37
  border-right: 0;
@@ -32,8 +39,11 @@ defineProps<{
32
39
 
33
40
  &.adjusted {
34
41
  width: v-bind(width) !important;
35
- min-width: v-bind(width) !important;
36
42
  max-width: v-bind(width) !important;
37
43
  }
44
+
45
+ &.adjusted:not(.auto) {
46
+ min-width: v-bind(width) !important;
47
+ }
38
48
  }
39
49
  </style>
@@ -1,6 +1,5 @@
1
1
  import type { MaybeRef } from '@vueuse/core'
2
2
  import type { Component } from 'vue'
3
- import { reactive } from 'vue'
4
3
  import type { Day } from '../support/Day'
5
4
  import type { DropdownSection } from './Dropdown'
6
5
 
@@ -9,18 +8,18 @@ export interface Table<
9
8
  R extends Record<string, any> = any,
10
9
  SR extends Record<string, any> = any
11
10
  > {
12
- orders: O[]
13
- columns: TableColumns<O, R, SR>
14
- records?: R[] | null
15
- header?: boolean
16
- footer?: boolean
17
- summary?: SR | null
18
- total?: number | null
19
- page?: number | null
20
- perPage?: number | null
21
- reset?: boolean
22
- borderless?: boolean
23
- loading?: boolean
11
+ orders: MaybeRef<O[]>
12
+ columns: MaybeRef<TableColumns<O, R, SR>>
13
+ records?: MaybeRef<R[] | null | undefined>
14
+ header?: MaybeRef<boolean | undefined>
15
+ footer?: MaybeRef<boolean | undefined>
16
+ summary?: MaybeRef<SR | null | undefined>
17
+ total?: MaybeRef<number | null | undefined>
18
+ page?: MaybeRef<number | null | undefined>
19
+ perPage?: MaybeRef<number | null | undefined>
20
+ reset?: MaybeRef<boolean | undefined>
21
+ borderless?: MaybeRef<boolean>
22
+ loading?: MaybeRef<boolean | undefined>
24
23
  onPrev?(): void
25
24
  onNext?(): void
26
25
  onReset?(): void
@@ -38,6 +37,7 @@ export interface TableColumn<V, R, SV, SR> {
38
37
  label?: string
39
38
  className?: string
40
39
  dropdown?: DropdownSection[]
40
+ grow?: boolean
41
41
  resizable?: boolean
42
42
  cell?: TableCell | TableColumnCellFn<V, R>
43
43
  summaryCell?: TableCell | TableColumnCellFn<SV, SR>
@@ -50,6 +50,7 @@ export type TableCell =
50
50
  | TableCellDay
51
51
  | TableCellPill
52
52
  | TableCellPills
53
+ | TableCellState
53
54
  | TableCellAvatar
54
55
  | TableCellAvatars
55
56
  | TableCellEmpty
@@ -60,6 +61,7 @@ export type TableCellType =
60
61
  | 'day'
61
62
  | 'pill'
62
63
  | 'pills'
64
+ | 'state'
63
65
  | 'avatar'
64
66
  | 'avatars'
65
67
  | 'empty'
@@ -142,34 +144,16 @@ export interface TableCellComponent extends TableCellBase {
142
144
  props?: Record<string, any>
143
145
  }
144
146
 
145
- export interface UseTableOptions<
146
- O extends string,
147
- R extends Record<string, any>,
148
- SR extends Record<string, any>
149
- > {
150
- orders: MaybeRef<O[]>
151
- columns: MaybeRef<TableColumns<O, R, SR>>
152
- records?: MaybeRef<R[] | null | undefined>
153
- header?: MaybeRef<boolean | undefined>
154
- footer?: MaybeRef<boolean | undefined>
155
- summary?: MaybeRef<SR | null | undefined>
156
- total?: MaybeRef<number | null | undefined>
157
- page?: MaybeRef<number | null | undefined>
158
- perPage?: MaybeRef<number | null | undefined>
159
- reset?: MaybeRef<boolean | undefined>
160
- borderless?: boolean
161
- loading?: MaybeRef<boolean | undefined>
162
- onPrev?(): void
163
- onNext?(): void
164
- onReset?(): void
147
+ export interface TableCellState extends TableCellBase {
148
+ type: 'state'
149
+ label: string
150
+ mode?: 'neutral' | 'mute' | 'info' | 'success' | 'warning' | 'danger'
165
151
  }
166
152
 
167
153
  export function useTable<
168
154
  O extends string,
169
155
  R extends Record<string, any>,
170
156
  SR extends Record<string, any>
171
- >(
172
- options: UseTableOptions<O, R, SR>
173
- ): Table<O, R, SR> {
174
- return reactive(options) as Table<O, R, SR>
157
+ >(options: Table<O, R, SR>): Table<O, R, SR> {
158
+ return options
175
159
  }
@@ -1,4 +1,5 @@
1
1
  @import "normalize.css";
2
2
  @import "v-calendar/dist/style.css";
3
+ @import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
3
4
  @import "./variables";
4
5
  @import "./base";