@flux-ui/components 3.0.0-next.64 → 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.64",
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.64",
57
- "@flux-ui/types": "3.0.0-next.64",
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
  }>();
@@ -163,6 +163,7 @@
163
163
  border-radius: var(--radius);
164
164
  box-shadow: var(--shadow-px);
165
165
  color: var(--foreground);
166
+ outline: 0 solid transparent;
166
167
  transition: var(--transition-default);
167
168
  transition-property: background, border-color, mixin.focus-ring-transition-properties();
168
169
 
@@ -587,6 +588,7 @@
587
588
  display: inline-flex;
588
589
  flex-shrink: 0;
589
590
  gap: 12px;
591
+ line-height: 20px;
590
592
  outline: 0;
591
593
 
592
594
  &.isDisabled {
@@ -618,16 +620,16 @@
618
620
 
619
621
  .checkboxElement,
620
622
  .checkboxNative {
621
- margin: 1px 0;
622
- height: 22px;
623
- width: 22px;
623
+ margin: 1px 0 0;
624
+ height: 20px;
625
+ width: 20px;
624
626
  }
625
627
 
626
628
  .checkboxElement {
627
629
  position: relative;
628
630
  display: inline-flex;
629
- height: 22px;
630
- width: 22px;
631
+ height: 20px;
632
+ width: 20px;
631
633
  padding: 0;
632
634
  align-items: center;
633
635
  justify-content: center;
@@ -661,6 +663,7 @@
661
663
 
662
664
  .checkboxNative {
663
665
  position: absolute;
666
+ cursor: pointer;
664
667
  opacity: 0;
665
668
 
666
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