sprintify-ui 0.12.13 → 0.12.15

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.
@@ -2,30 +2,31 @@
2
2
  <BaseButton
3
3
  :icon="section.icon"
4
4
  :size="size"
5
+ :color="hasCount ? 'primary' : ''"
5
6
  type="button"
6
7
  @click="open()"
7
8
  >
8
9
  <span v-if="section.title && width > 600">
9
10
  {{ section.title }}
10
11
  </span>
11
- <BaseBadge
12
- v-if="section.count"
13
- class="ml-2"
14
- color="blue"
15
- :size="size == 'sm' ? 'sm' : 'md'"
12
+ <span
13
+ v-if="hasCount"
14
+ class="ml-2 inline-flex items-center whitespace-nowrap rounded bg-white/25 px-1.5 py-0.5 font-semibold leading-tight"
15
+ :class="size == 'sm' ? 'text-[10px]' : 'text-xs'"
16
16
  >
17
- {{ section.count }}
18
- </BaseBadge>
17
+ <span aria-hidden="true">{{ section.count }}</span>
18
+ <span class="sr-only">{{ activeFiltersLabel }}</span>
19
+ </span>
19
20
  </BaseButton>
20
21
  </template>
21
22
 
22
23
  <script lang="ts" setup>
23
24
  import { DataIteratorSection } from '@/types';
24
25
  import BaseButton from './BaseButton.vue';
25
- import BaseBadge from './BaseBadge.vue';
26
26
  import { Size } from '@/utils/sizes';
27
+ import { useSectionCount } from '@/composables/sectionCount';
27
28
 
28
- defineProps<{
29
+ const props = defineProps<{
29
30
  section: DataIteratorSection;
30
31
  size: Size;
31
32
  }>();
@@ -36,6 +37,8 @@ const emit = defineEmits<{
36
37
 
37
38
  const width = inject('dataIterator:width', ref(0));
38
39
 
40
+ const { hasCount, activeFiltersLabel } = useSectionCount(() => props.section);
41
+
39
42
  function open() {
40
43
  emit('open');
41
44
  }
@@ -0,0 +1,18 @@
1
+ import { DataIteratorSection } from '@/types';
2
+ import { t } from '@/i18n';
3
+
4
+ /**
5
+ * Shared active-filter badge state for the data-iterator section chrome (the
6
+ * box header and the compact funnel button). Each renders the count with its
7
+ * own markup, but the "is there a count to show?" test and the accessible
8
+ * plural label are identical — so they live here.
9
+ */
10
+ export function useSectionCount(getSection: () => DataIteratorSection) {
11
+ const hasCount = computed<boolean>(() => (getSection().count ?? 0) > 0);
12
+
13
+ const activeFiltersLabel = computed<string>(() =>
14
+ t('sui.x_active_filters', { count: getSection().count })
15
+ );
16
+
17
+ return { hasCount, activeFiltersLabel };
18
+ }
package/src/lang/en.json CHANGED
@@ -93,6 +93,7 @@
93
93
  "up_to_x": "Up to {x}",
94
94
  "upload_failed": "Upload failed",
95
95
  "whoops": "Whoops",
96
+ "x_active_filters": "1 active filter | {count} active filters",
96
97
  "x_ago": "{duration} ago",
97
98
  "x_rows_selected": "1 item selected | {count} items selected",
98
99
  "year": "Year",
package/src/lang/fr.json CHANGED
@@ -93,6 +93,7 @@
93
93
  "up_to_x": "Jusqu'à {x}",
94
94
  "upload_failed": "Le téléchargement a échoué",
95
95
  "whoops": "Oups",
96
+ "x_active_filters": "1 filtre actif | {count} filtres actifs",
96
97
  "x_ago": "il y a {duration}",
97
98
  "x_rows_selected": "1 item sélectionné | \n{count} items sélectionnés",
98
99
  "year": "Année",
@@ -6,6 +6,7 @@ import { disableScroll, enableScroll } from './scrollPreventer';
6
6
  import { blobToBase64, validateBase64, base64ToBlob } from './blob';
7
7
  import { getColorConfig } from './colors';
8
8
  import { getInitials, getAvatarColor } from './avatar';
9
+ import { isEmpty } from './isEmpty';
9
10
 
10
11
  export {
11
12
  toHumanList,
@@ -20,4 +21,5 @@ export {
20
21
  getColorConfig,
21
22
  getInitials,
22
23
  getAvatarColor,
24
+ isEmpty,
23
25
  };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Deep emptiness predicate — the default "is this filter active?" rule.
3
+ *
4
+ * A value counts as *empty* (i.e. not an active filter) when it carries no
5
+ * user intent. This is deliberately NOT lodash's `isEmpty`: here `0` and
6
+ * `false` are meaningful, exposed choices and therefore count as active.
7
+ *
8
+ * Reference table (WIT-159):
9
+ *
10
+ * | Value | Empty (does not count) |
11
+ * |------------------------------------|------------------------|
12
+ * | `undefined`, `null` | yes |
13
+ * | `''`, `' '` (trimmed) | yes |
14
+ * | `[]`, `[null]`, `['']` (deep) | yes |
15
+ * | `{}`, `{from:null,to:null}` (deep) | yes |
16
+ * | `0` | **no → counts** |
17
+ * | `false` | **no → counts** |
18
+ * | `'jedi'`, `5`, `['a']`, `Date` | no → counts |
19
+ *
20
+ * Arrays and plain objects are inspected recursively: a container is empty
21
+ * only when every leaf it holds is itself empty. This keeps a grouped filter
22
+ * (`{ from, to }`) counting as a single active filter when any leaf is set.
23
+ */
24
+ export function isEmpty(value: unknown): boolean {
25
+ if (value === undefined || value === null) {
26
+ return true;
27
+ }
28
+
29
+ if (typeof value === 'string') {
30
+ return value.trim().length === 0;
31
+ }
32
+
33
+ // 0 and false are explicit, exposed choices → they count as active.
34
+ if (typeof value === 'number' || typeof value === 'boolean') {
35
+ return false;
36
+ }
37
+
38
+ if (value instanceof Date) {
39
+ return false;
40
+ }
41
+
42
+ if (Array.isArray(value)) {
43
+ return value.every((item) => isEmpty(item));
44
+ }
45
+
46
+ if (typeof value === 'object') {
47
+ return Object.values(value as Record<string, unknown>).every((item) =>
48
+ isEmpty(item)
49
+ );
50
+ }
51
+
52
+ // Any other primitive (bigint, symbol, function, …) carries intent.
53
+ return false;
54
+ }