@asd20/ui-next 2.0.29 → 2.1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ # [2.1.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.0.29...ui-next-v2.1.0) (2026-04-06)
4
+
5
+
6
+ ### Features
7
+
8
+ * revise file list template and componets to add search ([661b500](https://github.com/academydistrict20/asd20-ui-next/commit/661b500259a263285b37adf497156417fe983430))
9
+
3
10
  ## [2.0.29](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.0.28...ui-next-v2.0.29) (2026-04-06)
4
11
 
5
12
  ## [2.0.28](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.0.27...ui-next-v2.0.28) (2026-04-03)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.0.29",
3
+ "version": "2.1.0",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -10,6 +10,20 @@
10
10
  :icon="icon"
11
11
  :column-width="640"
12
12
  >
13
+ <template #header>
14
+ <div
15
+ v-if="searchable"
16
+ class="asd20-file-list__toolbar"
17
+ >
18
+ <asd20-search-field
19
+ v-model="searchQuery"
20
+ class="asd20-file-list__search"
21
+ :placeholder="searchPlaceholder"
22
+ :extra="resultSummary"
23
+ :id-tag="searchIdTag"
24
+ />
25
+ </div>
26
+ </template>
13
27
  <div
14
28
  v-for="category in categorizedFileItems"
15
29
  :key="category.name"
@@ -22,6 +36,36 @@
22
36
  v-bind="item"
23
37
  />
24
38
  </div>
39
+ <div
40
+ v-if="!categorizedFileItems.length"
41
+ class="asd20-file-list__empty"
42
+ >
43
+ {{ emptyStateMessage }}
44
+ </div>
45
+ <template #footer>
46
+ <div
47
+ v-if="shouldShowFooter"
48
+ class="asd20-file-list__footer"
49
+ >
50
+ <div class="asd20-file-list__footer-summary">
51
+ {{ footerSummary }}
52
+ </div>
53
+ <div class="asd20-file-list__footer-actions">
54
+ <asd20-button
55
+ v-if="canShowMore"
56
+ :label="showMoreLabel"
57
+ text-only
58
+ @click="showMore"
59
+ />
60
+ <asd20-button
61
+ v-if="canShowAll"
62
+ label="Show all"
63
+ text-only
64
+ @click="showAll"
65
+ />
66
+ </div>
67
+ </div>
68
+ </template>
25
69
  </asd20-list>
26
70
  </template>
27
71
 
@@ -30,10 +74,25 @@ import Asd20List from '../../../components/organisms/Asd20WidgetList'
30
74
  import mapFilesToListItems from '../../../helpers/mapFilesToListItems'
31
75
  import Asd20ListItem from '../../../components/molecules/Asd20ListItem'
32
76
  import Asd20ListCategory from '../../../components/molecules/Asd20ListCategory'
77
+ import Asd20SearchField from '../../../components/molecules/Asd20SearchField'
78
+
79
+ function normalizeSearchText(value = '') {
80
+ return String(value || '')
81
+ .toLowerCase()
82
+ .replace(/\.[a-z0-9]+$/i, '')
83
+ .replace(/[^a-z0-9]+/g, ' ')
84
+ .trim()
85
+ }
86
+
33
87
  export default {
34
88
  name: 'Asd20FileList',
35
89
 
36
- components: { Asd20List, Asd20ListItem, Asd20ListCategory },
90
+ components: {
91
+ Asd20List,
92
+ Asd20ListItem,
93
+ Asd20ListCategory,
94
+ Asd20SearchField,
95
+ },
37
96
 
38
97
  props: {
39
98
  files: { type: Array, default: () => [] },
@@ -46,10 +105,16 @@ export default {
46
105
  groupByTag: { type: Boolean, default: false },
47
106
  groupByDate: { type: Boolean, default: false },
48
107
  maxHeight: { type: String, default: '320px' },
108
+ searchable: { type: Boolean, default: false },
109
+ searchPlaceholder: { type: String, default: 'Search files' },
110
+ initialVisibleCount: { type: Number, default: 0 },
111
+ visibleCountStep: { type: Number, default: 50 },
49
112
  },
50
113
 
51
114
  data: () => ({
52
115
  loadedFiles: [],
116
+ searchQuery: '',
117
+ visibleCount: 0,
53
118
  }),
54
119
 
55
120
  computed: {
@@ -57,6 +122,125 @@ export default {
57
122
  return (this.files || []).concat(this.loadedFiles)
58
123
  },
59
124
 
125
+ normalizedSearchQuery() {
126
+ return normalizeSearchText(this.searchQuery)
127
+ },
128
+
129
+ searchTerms() {
130
+ return this.normalizedSearchQuery
131
+ ? this.normalizedSearchQuery.split(/\s+/).filter(Boolean)
132
+ : []
133
+ },
134
+
135
+ filteredFiles() {
136
+ if (!this.searchTerms.length) return this.computedFiles
137
+
138
+ return this.computedFiles.filter(file => this.matchesSearch(file))
139
+ },
140
+
141
+ totalFilesCount() {
142
+ return this.computedFiles.length
143
+ },
144
+
145
+ filteredFilesCount() {
146
+ return this.filteredFiles.length
147
+ },
148
+
149
+ hasActiveSearch() {
150
+ return this.searchTerms.length > 0
151
+ },
152
+
153
+ effectiveVisibleCount() {
154
+ if (!this.initialVisibleCount) return this.filteredFilesCount
155
+ if (!this.visibleCount) return this.initialVisibleCount
156
+
157
+ return this.visibleCount
158
+ },
159
+
160
+ visibleFiles() {
161
+ if (!this.initialVisibleCount) return this.filteredFiles
162
+
163
+ return this.filteredFiles.slice(0, this.effectiveVisibleCount)
164
+ },
165
+
166
+ canShowMore() {
167
+ return (
168
+ !!this.initialVisibleCount &&
169
+ this.visibleFiles.length < this.filteredFilesCount
170
+ )
171
+ },
172
+
173
+ canShowAll() {
174
+ return this.canShowMore && this.filteredFilesCount > this.nextVisibleCount
175
+ },
176
+
177
+ nextVisibleCount() {
178
+ return Math.min(
179
+ this.filteredFilesCount,
180
+ this.effectiveVisibleCount + this.resolvedVisibleCountStep
181
+ )
182
+ },
183
+
184
+ resolvedVisibleCountStep() {
185
+ return this.visibleCountStep > 0
186
+ ? this.visibleCountStep
187
+ : this.initialVisibleCount || 50
188
+ },
189
+
190
+ resultSummary() {
191
+ if (!this.searchable) return ''
192
+ if (!this.totalFilesCount) return '0 files'
193
+ if (this.hasActiveSearch) {
194
+ return `${this.filteredFilesCount} of ${this.totalFilesCount}`
195
+ }
196
+
197
+ return `${this.totalFilesCount} files`
198
+ },
199
+
200
+ footerSummary() {
201
+ if (!this.totalFilesCount) return ''
202
+
203
+ if (this.canShowMore) {
204
+ return `Showing ${this.visibleFiles.length} of ${this.filteredFilesCount} files`
205
+ }
206
+
207
+ if (this.hasActiveSearch) {
208
+ return `${this.filteredFilesCount} matching files`
209
+ }
210
+
211
+ return `Showing all ${this.filteredFilesCount} files`
212
+ },
213
+
214
+ hasLargeResultSet() {
215
+ return (
216
+ !!this.initialVisibleCount &&
217
+ this.filteredFilesCount > this.initialVisibleCount
218
+ )
219
+ },
220
+
221
+ shouldShowFooter() {
222
+ return (
223
+ this.totalFilesCount > 0 &&
224
+ (this.canShowMore || this.hasActiveSearch || this.hasLargeResultSet)
225
+ )
226
+ },
227
+
228
+ showMoreLabel() {
229
+ const increment = this.nextVisibleCount - this.visibleFiles.length
230
+ return `Show ${increment} more`
231
+ },
232
+
233
+ searchIdTag() {
234
+ return normalizeSearchText(this.title).replace(/\s+/g, '-')
235
+ },
236
+
237
+ emptyStateMessage() {
238
+ if (!this.totalFilesCount) return 'No files available.'
239
+ if (this.hasActiveSearch) return 'No files match your search.'
240
+
241
+ return 'No files available.'
242
+ },
243
+
60
244
  categorizedFileItems() {
61
245
  if (this.groupByDate) {
62
246
  return this.fileItemsGroupedByDate
@@ -71,7 +255,7 @@ export default {
71
255
  },
72
256
 
73
257
  fileItemsGroupedByCategory() {
74
- return this.computedFiles
258
+ return this.visibleFiles
75
259
  .reduce((a, c) => {
76
260
  let categories =
77
261
  c.categories && c.categories.length > 0
@@ -87,7 +271,7 @@ export default {
87
271
  return {
88
272
  name: c,
89
273
  items: mapFilesToListItems(
90
- this.computedFiles
274
+ this.visibleFiles
91
275
  .map(f => ({
92
276
  ...f,
93
277
  categories:
@@ -112,7 +296,7 @@ export default {
112
296
  },
113
297
 
114
298
  fileItemsGroupedByCategoryDescending() {
115
- return this.computedFiles
299
+ return this.visibleFiles
116
300
  .reduce((a, c) => {
117
301
  let categories =
118
302
  c.categories && c.categories.length > 0
@@ -128,7 +312,7 @@ export default {
128
312
  return {
129
313
  name: c,
130
314
  items: mapFilesToListItems(
131
- this.computedFiles
315
+ this.visibleFiles
132
316
  .map(f => ({
133
317
  ...f,
134
318
  categories:
@@ -153,7 +337,7 @@ export default {
153
337
  },
154
338
 
155
339
  fileItemsGroupedByTag() {
156
- return this.computedFiles
340
+ return this.visibleFiles
157
341
  .reduce((a, t) => {
158
342
  let tags = t.tags && t.tags.length > 0 ? t.tags : ['Untagged']
159
343
  for (const tag of tags) {
@@ -166,7 +350,7 @@ export default {
166
350
  return {
167
351
  name: t,
168
352
  items: mapFilesToListItems(
169
- this.computedFiles
353
+ this.visibleFiles
170
354
  .map(f => ({
171
355
  ...f,
172
356
  tags: f.tags && f.tags.length > 0 ? f.tags : ['Untagged'],
@@ -188,7 +372,7 @@ export default {
188
372
  },
189
373
 
190
374
  fileItemsGroupedByDate() {
191
- return this.computedFiles
375
+ return this.visibleFiles
192
376
  .reduce((a, c) => {
193
377
  let date = new Date(c.lastModifiedDateTime).toLocaleDateString()
194
378
  if (a.indexOf(date) === -1) a.push(date)
@@ -199,7 +383,7 @@ export default {
199
383
  name: d,
200
384
  unix: new Date(d).valueOf(),
201
385
  items: mapFilesToListItems(
202
- this.computedFiles.filter(
386
+ this.visibleFiles.filter(
203
387
  f => new Date(f.lastModifiedDateTime).toLocaleDateString() === d
204
388
  )
205
389
  )
@@ -221,7 +405,7 @@ export default {
221
405
  fileItemsGroupedByOwner() {
222
406
  return [
223
407
  {
224
- items: mapFilesToListItems(this.computedFiles).map(t => ({
408
+ items: mapFilesToListItems(this.visibleFiles).map(t => ({
225
409
  ...t,
226
410
  bordered: false,
227
411
  alignTop: false,
@@ -236,6 +420,7 @@ export default {
236
420
  watch: {
237
421
  files: {
238
422
  handler() {
423
+ this.resetVisibleCount()
239
424
  this.$nextTick(() => {
240
425
  // console.log('Change in Files detected:', this.files)
241
426
  this.checkForOverflow() // Ensure overflow check happens after file changes
@@ -245,13 +430,48 @@ export default {
245
430
  deep: true,
246
431
  immediate: true,
247
432
  },
433
+ searchQuery() {
434
+ this.resetVisibleCount()
435
+ },
248
436
  },
249
437
 
250
438
  mounted() {
439
+ this.resetVisibleCount()
251
440
  if (this.url) this.loadFiles()
252
441
  },
253
442
 
254
443
  methods: {
444
+ resetVisibleCount() {
445
+ this.visibleCount = this.initialVisibleCount > 0
446
+ ? this.initialVisibleCount
447
+ : 0
448
+ },
449
+ showMore() {
450
+ this.visibleCount = this.nextVisibleCount
451
+ },
452
+ showAll() {
453
+ this.visibleCount = this.filteredFilesCount
454
+ },
455
+ getFileSearchTokens(file = {}) {
456
+ return [
457
+ file.name,
458
+ file.filename,
459
+ file.description,
460
+ file.slug,
461
+ file.createdBy,
462
+ file.lastModifiedBy,
463
+ ...(Array.isArray(file.categories) ? file.categories : []),
464
+ ...(Array.isArray(file.owners) ? file.owners : []),
465
+ ...(Array.isArray(file.tags) ? file.tags : []),
466
+ ]
467
+ .map(normalizeSearchText)
468
+ .filter(Boolean)
469
+ .join(' ')
470
+ },
471
+ matchesSearch(file) {
472
+ const haystack = this.getFileSearchTokens(file)
473
+ return this.searchTerms.every(term => haystack.includes(term))
474
+ },
255
475
  // Expose the checkForOverflow method from the asd20viewport component
256
476
  // Expose the handleResize method from the asd20list component
257
477
  checkForOverflow() {
@@ -280,6 +500,33 @@ export default {
280
500
  <style lang="scss" scoped>
281
501
  @use '../../../design/component-stack' as *;
282
502
 
503
+ .asd20-file-list__toolbar {
504
+ display: flex;
505
+ align-items: center;
506
+ gap: space(0.25);
507
+ }
508
+
509
+ .asd20-file-list__search {
510
+ min-width: min(20rem, 100%);
511
+ margin-left: auto;
512
+
513
+ :deep(.asd20-icon) {
514
+ margin-left: space(0.5);
515
+ }
516
+
517
+ :deep(input) {
518
+ border: 2px solid var(--color__accent);
519
+ border-radius: var(--website-shape__radius-s);
520
+ font-family: var(--website-typography__font-family-headlines);
521
+ font-size: 1rem;
522
+ min-height: 40px;
523
+ }
524
+
525
+ :deep(.asd20-search-field__extra) {
526
+ padding: 0 space(0.5) 0 space(0.75);
527
+ }
528
+ }
529
+
283
530
  .asd20-file-list__category-group {
284
531
  display: contents;
285
532
  }
@@ -289,4 +536,53 @@ export default {
289
536
  margin-top: space(0.5);
290
537
  }
291
538
  }
539
+
540
+ .asd20-file-list__empty {
541
+ width: 100%;
542
+ padding: space(0.75) 0;
543
+ color: var(--color__primary);
544
+ }
545
+
546
+ .asd20-file-list__footer {
547
+ display: flex;
548
+ flex-wrap: wrap;
549
+ align-items: center;
550
+ justify-content: space-between;
551
+ gap: space(0.5);
552
+ padding-top: space(0.75);
553
+ }
554
+
555
+ .asd20-file-list__footer-summary {
556
+ color: var(--color__primary);
557
+ font-size: 0.875rem;
558
+ }
559
+
560
+ .asd20-file-list__footer-actions {
561
+ display: flex;
562
+ flex-wrap: wrap;
563
+ gap: space(0.5);
564
+ }
565
+
566
+ @media (max-width: 767px) {
567
+ .asd20-file-list__toolbar {
568
+ display: block;
569
+ flex-wrap: wrap;
570
+ width: 100%;
571
+ }
572
+
573
+ .asd20-file-list__search {
574
+ min-width: 100%;
575
+ margin-left: 0;
576
+ }
577
+ }
578
+
579
+ @media (min-width: 1024px) {
580
+ .asd20-file-list__search {
581
+ margin: space(0.5) space(0.5);
582
+
583
+ :deep(.asd20-icon) {
584
+ margin-left: 0;
585
+ }
586
+ }
587
+ }
292
588
  </style>
@@ -8,16 +8,21 @@
8
8
  v-if="$slots.header || headline"
9
9
  class="asd20-list__header"
10
10
  >
11
- <asd20-icon
12
- v-if="icon"
13
- :name="icon"
14
- :size="iconSize"
15
- />
16
- <component
17
- :is="headlineTag"
18
- class="asd20-list__headline"
19
- v-html="headline"
20
- ></component>
11
+ <div
12
+ v-if="icon || headline"
13
+ class="asd20-list__headline-group"
14
+ >
15
+ <asd20-icon
16
+ v-if="icon"
17
+ :name="icon"
18
+ :size="iconSize"
19
+ />
20
+ <component
21
+ :is="headlineTag"
22
+ class="asd20-list__headline"
23
+ v-html="headline"
24
+ ></component>
25
+ </div>
21
26
  <slot name="header" />
22
27
  </div>
23
28
  <asd20-viewport
@@ -123,7 +128,17 @@ export default {
123
128
  display: flex;
124
129
  flex-direction: row;
125
130
  align-items: center;
131
+ justify-content: space-between;
126
132
  margin-bottom: space(0.5);
133
+ gap: space(0.5);
134
+ }
135
+
136
+ .asd20-list__headline-group {
137
+ display: flex;
138
+ flex-direction: row;
139
+ align-items: center;
140
+ min-width: 0;
141
+
127
142
  .asd20-icon {
128
143
  margin-right: space(0.5);
129
144
  --line-color: var(--website-icon__line-color);
@@ -154,6 +169,18 @@ export default {
154
169
  }
155
170
  }
156
171
 
172
+ @media (max-width: 767px) {
173
+ .asd20-list__header {
174
+ flex-direction: column;
175
+ justify-content: flex-start;
176
+ align-items: stretch;
177
+ }
178
+
179
+ .asd20-list__headline-group {
180
+ width: 100%;
181
+ }
182
+ }
183
+
157
184
  /* @media (min-width: 767px) {*/
158
185
  /* .asd20-list--multi-column {*/
159
186
  /* &::v-deep .asd20-list-item {*/
@@ -8,16 +8,21 @@
8
8
  v-if="$slots.header || headline"
9
9
  class="asd20-list__header"
10
10
  >
11
- <asd20-icon
12
- v-if="icon"
13
- :name="icon"
14
- :size="iconSize"
15
- />
16
- <component
17
- :is="headlineTag"
18
- class="asd20-list__headline"
19
- v-html="headline"
20
- ></component>
11
+ <div
12
+ v-if="icon || headline"
13
+ class="asd20-list__headline-group"
14
+ >
15
+ <asd20-icon
16
+ v-if="icon"
17
+ :name="icon"
18
+ :size="iconSize"
19
+ />
20
+ <component
21
+ :is="headlineTag"
22
+ class="asd20-list__headline"
23
+ v-html="headline"
24
+ ></component>
25
+ </div>
21
26
  <slot name="header" />
22
27
  </div>
23
28
  <asd20-viewport
@@ -128,7 +133,17 @@ export default {
128
133
  display: flex;
129
134
  flex-direction: row;
130
135
  align-items: center;
136
+ justify-content: space-between;
131
137
  margin-bottom: space(0.5);
138
+ gap: space(0.5);
139
+ }
140
+
141
+ .asd20-list__headline-group {
142
+ display: flex;
143
+ flex-direction: row;
144
+ align-items: center;
145
+ min-width: 0;
146
+
132
147
  .asd20-icon {
133
148
  margin-right: space(0.5);
134
149
  --line-color: var(--website-icon__line-color);
@@ -159,6 +174,18 @@ export default {
159
174
  }
160
175
  }
161
176
 
177
+ @media (max-width: 767px) {
178
+ .asd20-list__header {
179
+ flex-direction: column;
180
+ justify-content: flex-start;
181
+ align-items: stretch;
182
+ }
183
+
184
+ .asd20-list__headline-group {
185
+ width: 100%;
186
+ }
187
+ }
188
+
162
189
  /* @media (min-width: 767px) {*/
163
190
  /* .asd20-list--multi-column {*/
164
191
  /* &::v-deep .asd20-list-item {*/
@@ -79,6 +79,10 @@
79
79
  :files="files"
80
80
  v-bind="filesFeedProps"
81
81
  :multi-column="true"
82
+ searchable
83
+ :initial-visible-count="50"
84
+ :visible-count-step="50"
85
+ search-placeholder="Search files"
82
86
  max-height="auto"
83
87
  @files-in-view="$emit('files-in-view')"
84
88
  />