@flux-ui/components 3.0.0-next.65 → 3.0.0-next.66

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@flux-ui/components",
3
3
  "description": "A set of opiniated UI components.",
4
- "version": "3.0.0-next.65",
4
+ "version": "3.0.0-next.66",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "funding": "https://github.com/sponsors/basmilius",
@@ -50,11 +50,15 @@
50
50
  "main": "./dist/index.js",
51
51
  "module": "./dist/index.js",
52
52
  "types": "./dist/index.d.ts",
53
+ "sideEffects": [
54
+ "**/css/index.scss",
55
+ "**/dist/index.css"
56
+ ],
53
57
  "dependencies": {
54
58
  "@basmilius/common": "^3.25.0",
55
59
  "@basmilius/utils": "^3.25.0",
56
- "@flux-ui/internals": "3.0.0-next.65",
57
- "@flux-ui/types": "3.0.0-next.65",
60
+ "@flux-ui/internals": "3.0.0-next.66",
61
+ "@flux-ui/types": "3.0.0-next.66",
58
62
  "@fortawesome/fontawesome-common-types": "^7.2.0",
59
63
  "clsx": "^2.1.1",
60
64
  "imask": "^7.6.1",
@@ -26,12 +26,12 @@
26
26
  <FluxIcon
27
27
  v-if="isIndeterminate"
28
28
  name="minus"
29
- :size="16"/>
29
+ :size="12"/>
30
30
 
31
31
  <FluxIcon
32
32
  v-else
33
33
  name="check"
34
- :size="16"/>
34
+ :size="12"/>
35
35
  </button>
36
36
 
37
37
  <span
@@ -13,13 +13,23 @@
13
13
  </template>
14
14
 
15
15
  <template
16
- v-if="'header' in slots"
16
+ v-if="'header' in slots || selectionMode"
17
17
  #header>
18
18
  <slot
19
19
  name="filter"
20
20
  v-bind="{page, perPage, items: limitedItems, total}"/>
21
21
 
22
22
  <FluxTableRow>
23
+ <FluxTableHeader
24
+ v-if="selectionMode"
25
+ is-shrinking
26
+ :class="$style.tableCellSelection">
27
+ <FluxCheckbox
28
+ v-if="selectionMode === 'multiple'"
29
+ :model-value="selectAllState"
30
+ @update:model-value="onSelectAll"/>
31
+ </FluxTableHeader>
32
+
23
33
  <slot
24
34
  name="header"
25
35
  v-bind="{page, perPage, items: limitedItems, total}"/>
@@ -54,11 +64,22 @@
54
64
 
55
65
  <FluxTableRow
56
66
  v-for="(item, index) of limitedItems"
57
- :key="uniqueKey ? item[uniqueKey] : index">
67
+ :key="uniqueKey ? item[uniqueKey] : index"
68
+ :class="selectionMode && $style.isSelectableRow"
69
+ :is-selected="selectionMode ? isItemSelected(item) : false"
70
+ @click="onRowClick(item, $event)">
71
+ <FluxTableCell
72
+ v-if="selectionMode"
73
+ :class="$style.tableCellSelection">
74
+ <FluxCheckbox
75
+ :model-value="isItemSelected(item)"
76
+ @update:model-value="onSelectRow(item)"/>
77
+ </FluxTableCell>
78
+
58
79
  <template v-for="(_, name) of slots">
59
80
  <slot
60
81
  v-if="!IGNORED_SLOTS.includes(name as string)"
61
- v-bind="{index, item, items: limitedItems, page, perPage, total}"
82
+ v-bind="{index, item, items: limitedItems, page, perPage, total, isSelected: isItemSelected(item)}"
62
83
  :name="name"/>
63
84
  </template>
64
85
  </FluxTableRow>
@@ -69,10 +90,17 @@
69
90
  lang="ts"
70
91
  setup
71
92
  generic="T extends Record<string, any>">
72
- import { computed, type VNode } from 'vue';
93
+ import { computed, unref, type VNode } from 'vue';
94
+ import FluxCheckbox from './FluxCheckbox.vue';
73
95
  import FluxPaginationBar from './FluxPaginationBar.vue';
74
96
  import FluxTable from './FluxTable.vue';
97
+ import FluxTableCell from './FluxTableCell.vue';
98
+ import FluxTableHeader from './FluxTableHeader.vue';
75
99
  import FluxTableRow from './FluxTableRow.vue';
100
+ import $style from '~flux/components/css/component/Table.module.scss';
101
+
102
+ type SelectionId = string | number;
103
+ type SelectionValue = SelectionId | null | SelectionId[];
76
104
 
77
105
  const IGNORED_SLOTS: string[] = ['filter', 'header', 'footer', 'colgroups', 'pagination'];
78
106
 
@@ -81,6 +109,8 @@
81
109
  navigate: [number];
82
110
  }>();
83
111
 
112
+ const selected = defineModel<SelectionValue>('selected');
113
+
84
114
  const {
85
115
  isBordered = true,
86
116
  isHoverable = false,
@@ -88,7 +118,9 @@
88
118
  isSeparated = true,
89
119
  isStriped = false,
90
120
  items,
91
- perPage
121
+ perPage,
122
+ selectionMode,
123
+ uniqueKey
92
124
  } = defineProps<{
93
125
  readonly fillColumns?: number;
94
126
  readonly isBordered?: boolean;
@@ -100,6 +132,7 @@
100
132
  readonly limits: number[];
101
133
  readonly page: number;
102
134
  readonly perPage: number;
135
+ readonly selectionMode?: 'single' | 'multiple';
103
136
  readonly total: number;
104
137
  readonly uniqueKey?: string;
105
138
  }>();
@@ -112,6 +145,7 @@
112
145
  readonly item: T;
113
146
  readonly items: T[];
114
147
  readonly total: number;
148
+ readonly isSelected: boolean;
115
149
  }) => VNode;
116
150
 
117
151
  filter(props: {
@@ -146,4 +180,116 @@
146
180
  }>();
147
181
 
148
182
  const limitedItems = computed(() => items.slice(0, perPage));
183
+
184
+ const currentPageIds = computed<SelectionId[]>(() => {
185
+ if (!uniqueKey) {
186
+ return [];
187
+ }
188
+
189
+ return unref(limitedItems).map(item => item[uniqueKey] as SelectionId);
190
+ });
191
+
192
+ const selectAllState = computed<boolean | null>(() => {
193
+ const ids = unref(currentPageIds);
194
+ const value = unref(selected);
195
+
196
+ if (ids.length === 0 || !Array.isArray(value)) {
197
+ return false;
198
+ }
199
+
200
+ const selectedOnPage = ids.filter(id => value.includes(id)).length;
201
+
202
+ if (selectedOnPage === 0) {
203
+ return false;
204
+ }
205
+
206
+ if (selectedOnPage === ids.length) {
207
+ return true;
208
+ }
209
+
210
+ return null;
211
+ });
212
+
213
+ function getItemId(item: T): SelectionId | undefined {
214
+ if (!uniqueKey) {
215
+ return undefined;
216
+ }
217
+
218
+ return item[uniqueKey] as SelectionId;
219
+ }
220
+
221
+ function isItemSelected(item: T): boolean {
222
+ if (!selectionMode) {
223
+ return false;
224
+ }
225
+
226
+ const id = getItemId(item);
227
+
228
+ if (id === undefined) {
229
+ return false;
230
+ }
231
+
232
+ const value = unref(selected);
233
+
234
+ if (Array.isArray(value)) {
235
+ return value.includes(id);
236
+ }
237
+
238
+ return value === id;
239
+ }
240
+
241
+ function onRowClick(item: T, event: MouseEvent): void {
242
+ if (!selectionMode) {
243
+ return;
244
+ }
245
+
246
+ const target = event.target as HTMLElement | null;
247
+
248
+ if (target?.closest('a, button, input, label, select, textarea, [role="button"]')) {
249
+ return;
250
+ }
251
+
252
+ onSelectRow(item);
253
+ }
254
+
255
+ function onSelectRow(item: T): void {
256
+ const id = getItemId(item);
257
+
258
+ if (id === undefined) {
259
+ return;
260
+ }
261
+
262
+ if (selectionMode === 'multiple') {
263
+ const current = Array.isArray(unref(selected)) ? unref(selected) as SelectionId[] : [];
264
+ selected.value = current.includes(id)
265
+ ? current.filter(v => v !== id)
266
+ : [...current, id];
267
+ return;
268
+ }
269
+
270
+ if (selectionMode === 'single') {
271
+ selected.value = isItemSelected(item) ? null : id;
272
+ }
273
+ }
274
+
275
+ function onSelectAll(value: boolean | null): void {
276
+ if (selectionMode !== 'multiple') {
277
+ return;
278
+ }
279
+
280
+ const ids = unref(currentPageIds);
281
+ const current = Array.isArray(unref(selected)) ? unref(selected) as SelectionId[] : [];
282
+
283
+ if (value) {
284
+ const additions = ids.filter(id => !current.includes(id));
285
+ selected.value = [...current, ...additions];
286
+ return;
287
+ }
288
+
289
+ selected.value = current.filter(id => !ids.includes(id));
290
+ }
291
+
292
+ if (import.meta.env.DEV && selectionMode && !uniqueKey) {
293
+ console.warn('[FluxDataTable] `uniqueKey` is required when `selectionMode` is set, otherwise rows cannot be tracked across renders.');
294
+ }
149
295
  </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :class="$style.table">
2
+ <div :class="[$style.table, isBordered && $style.isBordered]">
3
3
  <table :class="$style.tableBase">
4
4
  <slot name="colgroups"/>
5
5
 
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <tr :class="$style.tableRow">
2
+ <tr :class="clsx($style.tableRow, isSelected && $style.isSelected)">
3
3
  <slot/>
4
4
  </tr>
5
5
  </template>
@@ -7,9 +7,14 @@
7
7
  <script
8
8
  lang="ts"
9
9
  setup>
10
+ import { clsx } from 'clsx';
10
11
  import type { VNode } from 'vue';
11
12
  import $style from '~flux/components/css/component/Table.module.scss';
12
13
 
14
+ defineProps<{
15
+ readonly isSelected?: boolean;
16
+ }>();
17
+
13
18
  defineSlots<{
14
19
  default(): VNode[];
15
20
  }>();
@@ -588,6 +588,7 @@
588
588
  display: inline-flex;
589
589
  flex-shrink: 0;
590
590
  gap: 12px;
591
+ line-height: 20px;
591
592
  outline: 0;
592
593
 
593
594
  &.isDisabled {
@@ -619,16 +620,16 @@
619
620
 
620
621
  .checkboxElement,
621
622
  .checkboxNative {
622
- margin: 1px 0;
623
- height: 22px;
624
- width: 22px;
623
+ margin: 1px 0 0;
624
+ height: 20px;
625
+ width: 20px;
625
626
  }
626
627
 
627
628
  .checkboxElement {
628
629
  position: relative;
629
630
  display: inline-flex;
630
- height: 22px;
631
- width: 22px;
631
+ height: 20px;
632
+ width: 20px;
632
633
  padding: 0;
633
634
  align-items: center;
634
635
  justify-content: center;
@@ -662,6 +663,7 @@
662
663
 
663
664
  .checkboxNative {
664
665
  position: absolute;
666
+ cursor: pointer;
665
667
  opacity: 0;
666
668
 
667
669
  @include mixin.hover {
@@ -36,6 +36,10 @@
36
36
  border: 0;
37
37
  }
38
38
 
39
+ .layerPane > .paneHeader .button {
40
+ margin: -9px -9px -9px 0;
41
+ }
42
+
39
43
  .layerPaneSecondary {
40
44
  display: flex;
41
45
  align-items: center;
@@ -138,7 +138,7 @@
138
138
 
139
139
  > .paneHeader + .tabBarDefault {
140
140
  position: sticky;
141
- top: 45px;
141
+ top: 60px;
142
142
  z-index: 100;
143
143
  }
144
144
 
@@ -31,7 +31,7 @@
31
31
  .tabBarArrow {
32
32
  position: absolute;
33
33
  display: flex;
34
- top: 3px;
34
+ top: 2px;
35
35
  height: calc(100% - 6px);
36
36
  width: 30px;
37
37
  align-items: center;
@@ -53,13 +53,13 @@
53
53
  .tabBarArrowStart {
54
54
  composes: tabBarArrow;
55
55
 
56
- left: -6px;
56
+ left: 3px;
57
57
  }
58
58
 
59
59
  .tabBarArrowEnd {
60
60
  composes: tabBarArrow;
61
61
 
62
- right: -6px;
62
+ right: 3px;
63
63
  }
64
64
 
65
65
  .baseTabBarTabs {
@@ -39,6 +39,10 @@
39
39
  height: 0;
40
40
  margin: 0;
41
41
  padding: 0;
42
+
43
+ &.isSelectableRow {
44
+ cursor: pointer;
45
+ }
42
46
  }
43
47
 
44
48
  .tableCell {
@@ -88,11 +92,25 @@ tbody .tableRow:nth-child(even) .tableCell.isStriped {
88
92
  background: rgb(from var(--gray-50) r g b / .5);
89
93
  }
90
94
 
95
+ tbody .tableRow.isSelected .tableCell {
96
+ background: var(--primary-50);
97
+ border-color: var(--primary-100);
98
+ }
99
+
100
+ tbody .tableRow.isSelected + .tableRow .tableCell {
101
+ border-top-color: var(--primary-100);
102
+ }
103
+
91
104
  @media (hover: hover) {
92
105
  tbody .tableRow:hover .tableCell.isHoverable,
93
106
  tbody .tableRow:has(:focus-visible) .tableCell.isHoverable {
94
107
  background: var(--gray-50);
95
108
  }
109
+
110
+ tbody .tableRow.isSelected:hover .tableCell.isHoverable,
111
+ tbody .tableRow.isSelected:has(:focus-visible) .tableCell.isHoverable {
112
+ background: var(--primary-50);
113
+ }
96
114
  }
97
115
 
98
116
  tfoot .tableCell {
@@ -111,6 +129,10 @@ tfoot .tableCell {
111
129
  margin: -4px 0 -4px -3px;
112
130
  }
113
131
 
132
+ .tableCellSelection {
133
+ width: 0;
134
+ }
135
+
114
136
  .tableFill {
115
137
  pointer-events: none;
116
138
 
@@ -180,12 +202,20 @@ tfoot .tableCell {
180
202
  }
181
203
  }
182
204
 
205
+ .basePaneStructure:has(.table) {
206
+ --table-spacing: 18px;
207
+ }
208
+
209
+ .basePaneStructure .basePaneStructure {
210
+ --table-spacing: 15px;
211
+ }
212
+
183
213
  .basePaneStructure > .table .tableCell:first-child .tableCellContent {
184
- padding-left: 18px;
214
+ padding-left: var(--table-spacing);
185
215
  }
186
216
 
187
217
  .basePaneStructure > .table .tableCell:last-child .tableCellContent {
188
- padding-right: 18px;
218
+ padding-right: var(--table-spacing);
189
219
  }
190
220
 
191
221
  .basePaneStructure > .table .tableActions {
@@ -193,7 +223,7 @@ tfoot .tableCell {
193
223
  }
194
224
 
195
225
  .basePaneStructure > .table :is(caption) {
196
- padding: 12px 18px;
226
+ padding: 12px var(--table-spacing);
197
227
  border-top: 1px solid var(--gray-100);
198
228
  }
199
229