@dative-gpi/foundation-shared-components 1.0.179 → 1.0.180-utils-composable

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.
@@ -3,10 +3,45 @@
3
3
  gap="24px"
4
4
  >
5
5
  <FSPagination
6
+ v-if="$props.mode === 'pagination'"
6
7
  width="calc(100% - 16px)"
7
8
  :pages="$props.steps"
8
9
  :modelValue="currentStep - 1"
9
10
  />
11
+ <FSTabs
12
+ v-else-if="$props.mode === 'tabs'"
13
+ :tab="currentStep - 1"
14
+ :color="$props.tabsColor"
15
+ @update:tab="(val) => currentStep = val + 1"
16
+ >
17
+ <FSTab
18
+ v-for="(step, index) in $props.steps"
19
+ :key="index"
20
+ >
21
+ <slot
22
+ :name="`tab-${index + 1}`"
23
+ >
24
+ <FSRow>
25
+ <FSIcon
26
+ v-if="tabIconSlots[`tab-${index + 1}-icon`]"
27
+ >
28
+ <slot
29
+ :name="`tab-${index + 1}-icon`"
30
+ />
31
+ </FSIcon>
32
+ <FSSpan
33
+ :font="index + 1 === currentStep ? 'text-button' : 'text-body'"
34
+ >
35
+ <slot
36
+ :name="`tab-${index + 1}-label`"
37
+ >
38
+ {{ $tr('ui.tabs.step.default', 'Step {0}', step) }}
39
+ </slot>
40
+ </FSSpan>
41
+ </FSRow>
42
+ </slot>
43
+ </FSTab>
44
+ </FSTabs>
10
45
  <FSWindow
11
46
  width="100%"
12
47
  :modelValue="currentStep - 1"
@@ -78,6 +113,11 @@ import FSButton from "./FSButton.vue";
78
113
  import FSForm from "./FSForm.vue";
79
114
  import FSCol from "./FSCol.vue";
80
115
  import FSRow from "./FSRow.vue";
116
+ import FSSpan from "./FSSpan.vue";
117
+ import FSTabs from "./FSTabs.vue";
118
+ import FSTab from "./FSTab.vue";
119
+ import FSIcon from "./FSIcon.vue";
120
+ import FSWindow from "./FSWindow.vue";
81
121
 
82
122
  export default defineComponent({
83
123
  name: "FSDialogMultiFormBody",
@@ -87,7 +127,12 @@ export default defineComponent({
87
127
  FSButton,
88
128
  FSForm,
89
129
  FSCol,
90
- FSRow
130
+ FSRow,
131
+ FSSpan,
132
+ FSTabs,
133
+ FSTab,
134
+ FSIcon,
135
+ FSWindow
91
136
  },
92
137
  props: {
93
138
  subtitle: {
@@ -178,10 +223,20 @@ export default defineComponent({
178
223
  type: Boolean,
179
224
  required: false,
180
225
  default: false
181
- }
226
+ },
227
+ mode: {
228
+ type: String as PropType<"pagination" | "tabs">,
229
+ required: false,
230
+ default: "pagination"
231
+ },
232
+ tabsColor: {
233
+ type: String as PropType<ColorBase>,
234
+ required: false,
235
+ default: ColorEnum.Primary
236
+ },
182
237
  },
183
238
  emits: ["click:cancelButton", "click:submitButton"],
184
- setup(props, { emit }) {
239
+ setup(props, { emit, slots }) {
185
240
  const { isMobileSized } = useBreakpoints();
186
241
  const { $tr } = useTranslationsProvider();
187
242
 
@@ -189,6 +244,16 @@ export default defineComponent({
189
244
  const valid = ref(false);
190
245
  const valids = ref(Array.from({ length: props.steps }, () => false));
191
246
 
247
+ const hasSlot = (name: string) => !!slots[name];
248
+
249
+ const tabIconSlots = computed(() => {
250
+ const result: Record<string, boolean> = {};
251
+ for (let i = 1; i <= props.steps; i++) {
252
+ result[`tab-${i}-icon`] = hasSlot(`tab-${i}-icon`);
253
+ }
254
+ return result;
255
+ });
256
+
192
257
  const maxHeight = computed(() => {
193
258
  const other = 24 + 24 // Paddings
194
259
  + (isMobileSized.value ? 24 : 32) + 24 // Title
@@ -246,6 +311,7 @@ export default defineComponent({
246
311
  maxHeight,
247
312
  valids,
248
313
  valid,
314
+ tabIconSlots,
249
315
  onPrevious,
250
316
  onSubmit
251
317
  };
@@ -13,7 +13,7 @@ import { computed, defineComponent, type PropType } from "vue";
13
13
 
14
14
  import { IMAGE_RAW_URL } from "@dative-gpi/foundation-shared-services/config";
15
15
 
16
- import { useImage } from "@dative-gpi/foundation-shared-services/composables";
16
+ import { useImage, useAppAuthToken } from "@dative-gpi/foundation-shared-services/composables";
17
17
 
18
18
  import FSEditImageUI from "./FSEditImageUI.vue";
19
19
 
@@ -32,10 +32,11 @@ export default defineComponent({
32
32
  emits: ["update:imageId"],
33
33
  setup(props) {
34
34
  const { get: getImage, entity: image } = useImage();
35
+ const { authToken } = useAppAuthToken();
35
36
 
36
37
  const source = computed(() => {
37
- return props.imageId ? IMAGE_RAW_URL(props.imageId) : null;
38
- })
38
+ return props.imageId ? IMAGE_RAW_URL(props.imageId, authToken.value) : null;
39
+ });
39
40
 
40
41
  const onError = (): void => {
41
42
  if (props.imageId) {
@@ -18,7 +18,9 @@
18
18
  {{ $props.prependIcon }}
19
19
  </FSIcon>
20
20
  </slot>
21
- <slot>
21
+ <slot
22
+ name="label"
23
+ >
22
24
  <FSSpan
23
25
  v-if="$props.label"
24
26
  font="text-button"
@@ -84,7 +84,7 @@
84
84
  </template>
85
85
 
86
86
  <script lang="ts">
87
- import { computed, defineComponent, type PropType, ref, type StyleValue } from "vue";
87
+ import { computed, defineComponent, type PropType, ref, type StyleValue, watch } from "vue";
88
88
 
89
89
  import { type ColorBase, ColorEnum } from "@dative-gpi/foundation-shared-components/models";
90
90
  import { useAppLanguages } from "@dative-gpi/foundation-shared-services/composables";
@@ -97,6 +97,7 @@ import FSButton from "../FSButton.vue";
97
97
  import FSIcon from "../FSIcon.vue";
98
98
  import FSSpan from "../FSSpan.vue";
99
99
  import FSRow from "../FSRow.vue";
100
+ import type { Translation } from "@dative-gpi/foundation-shared-components/models";
100
101
 
101
102
  export default defineComponent({
102
103
  name: "FSTranslateField",
@@ -140,7 +141,7 @@ export default defineComponent({
140
141
  default: "label"
141
142
  },
142
143
  translations: {
143
- type: Array as PropType<{ languageCode: string; [key: string]: string }[]>,
144
+ type: Array as PropType<Translation[]>,
144
145
  required: false,
145
146
  default: () => []
146
147
  },
@@ -165,7 +166,7 @@ export default defineComponent({
165
166
 
166
167
  const dialog = ref(false);
167
168
 
168
- const innerTranslations = ref(props.translations);
169
+ const innerTranslations = ref<Translation[]>(props.translations);
169
170
 
170
171
  const lights = getColors(ColorEnum.Light);
171
172
  const darks = getColors(ColorEnum.Dark);
@@ -189,7 +190,8 @@ export default defineComponent({
189
190
  if (!translation || !translation[props.property]) {
190
191
  return "";
191
192
  }
192
- return translation[props.property];
193
+
194
+ return translation[props.property] ?? "";
193
195
  };
194
196
 
195
197
  const setTranslation = (languageCode: string, value: string): void => {
@@ -219,6 +221,10 @@ export default defineComponent({
219
221
  }
220
222
  };
221
223
 
224
+ watch(() => props.translations, (newVal) => {
225
+ innerTranslations.value = newVal;
226
+ }, { immediate: true, deep: true });
227
+
222
228
  return {
223
229
  innerTranslations,
224
230
  ColorEnum,
@@ -86,10 +86,11 @@
86
86
  </template>
87
87
 
88
88
  <script lang="ts">
89
- import { computed, defineComponent, type PropType, ref, type StyleValue } from "vue";
89
+ import { computed, defineComponent, type PropType, ref, type StyleValue, watch } from "vue";
90
90
 
91
91
  import { type ColorBase, ColorEnum } from "@dative-gpi/foundation-shared-components/models";
92
92
  import { useAppLanguages } from "@dative-gpi/foundation-shared-services/composables";
93
+ import type { Translation } from "@dative-gpi/foundation-shared-components/models";
93
94
 
94
95
  import { useColors } from "../../composables";
95
96
 
@@ -142,7 +143,7 @@ export default defineComponent({
142
143
  default: "label"
143
144
  },
144
145
  translations: {
145
- type: Array as PropType<{ languageCode: string; [key: string]: string }[]>,
146
+ type: Array as PropType<Translation[]>,
146
147
  required: false,
147
148
  default: () => []
148
149
  },
@@ -164,7 +165,7 @@ export default defineComponent({
164
165
 
165
166
  const dialog = ref(false);
166
167
 
167
- const innerTranslations = ref(props.translations);
168
+ const innerTranslations = ref<Translation[]>(props.translations);
168
169
 
169
170
  const lights = getColors(ColorEnum.Light);
170
171
  const darks = getColors(ColorEnum.Dark);
@@ -188,7 +189,7 @@ export default defineComponent({
188
189
  if (!translation || !translation[props.property]) {
189
190
  return "";
190
191
  }
191
- return translation[props.property];
192
+ return translation[props.property] ?? "";
192
193
  };
193
194
 
194
195
  const setTranslation = (languageCode: string, value: string): void => {
@@ -218,6 +219,10 @@ export default defineComponent({
218
219
  }
219
220
  };
220
221
 
222
+ watch(() => props.translations, (newVal) => {
223
+ innerTranslations.value = newVal;
224
+ }, { immediate: true, deep: true });
225
+
221
226
  return {
222
227
  innerTranslations,
223
228
  ColorEnum,
@@ -1,8 +1,11 @@
1
+ export * from "./useAccessibilityPreferences";
1
2
  export * from "./useAddress";
2
3
  export * from "./useAutocomplete";
3
4
  export * from "./useBreakpoints";
4
5
  export * from "./useColors";
6
+ export * from "./useCountUp";
5
7
  export * from "./useDebounce";
8
+ export * from "./useElementVisibility"
6
9
  export * from "./useMapLayers";
7
10
  export * from "./useRules";
8
11
  export * from "./useSlots";
@@ -0,0 +1,7 @@
1
+ export function useAccessibilityPreferences() {
2
+ const prefersReducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches || false;
3
+
4
+ return {
5
+ prefersReducedMotion
6
+ };
7
+ }
@@ -0,0 +1,101 @@
1
+ import { ref, computed, onBeforeUnmount, watch } from 'vue';
2
+ import { useAccessibilityPreferences } from './useAccessibilityPreferences';
3
+
4
+ export function useCountUp(options: {
5
+ value: number | string,
6
+ duration?: number,
7
+ countUp?: boolean,
8
+ pad?: number,
9
+ startOnVisible?: boolean,
10
+ easing?: (t: number) => number
11
+ }) {
12
+ const {
13
+ value,
14
+ duration = 800,
15
+ countUp = true,
16
+ pad = 2,
17
+ startOnVisible = true,
18
+ easing = easeOutCubic
19
+ } = options;
20
+
21
+ const { prefersReducedMotion } = useAccessibilityPreferences();
22
+ const current = ref(0);
23
+ const rafId = ref(0);
24
+ const hasAnimated = ref(false);
25
+
26
+ // Easing function
27
+ function easeOutCubic(t: number) {
28
+ return 1 - Math.pow(1 - t, 3);
29
+ }
30
+
31
+ // Target value computation
32
+ const target = computed(() => {
33
+ const n = Number(value);
34
+ return Number.isFinite(n) ? Math.trunc(n) : 0;
35
+ });
36
+
37
+ // Formatted display value
38
+ const displayText = computed(() => {
39
+ const s = String(countUp ? current.value : target.value);
40
+ return pad > 0 ? s.padStart(pad, "0") : s;
41
+ });
42
+
43
+ // Animation function
44
+ function animate(from: number, to: number, animDuration: number) {
45
+ cancelAnimationFrame(rafId.value);
46
+ const start = performance.now();
47
+
48
+ const step = (now: number) => {
49
+ const t = Math.min(1, (now - start) / animDuration);
50
+ const v = from + (to - from) * easing(t);
51
+ current.value = Math.round(v);
52
+ if (t < 1) {
53
+ rafId.value = requestAnimationFrame(step);
54
+ }
55
+ };
56
+
57
+ rafId.value = requestAnimationFrame(step);
58
+ }
59
+
60
+ // Start animation
61
+ function start() {
62
+ if (!countUp) {
63
+ current.value = target.value;
64
+ return;
65
+ }
66
+
67
+ if (prefersReducedMotion) {
68
+ current.value = target.value;
69
+ return;
70
+ }
71
+
72
+ animate(current.value, target.value, duration);
73
+ hasAnimated.value = true;
74
+ }
75
+
76
+ // Clean up
77
+ onBeforeUnmount(() => {
78
+ cancelAnimationFrame(rafId.value);
79
+ });
80
+
81
+ // The restart method can be useful when the value changes.
82
+ function restart() {
83
+ if (hasAnimated.value || !startOnVisible) {
84
+ start();
85
+ }
86
+ }
87
+
88
+ // Monitor changes in value
89
+ watch(() => value, () => {
90
+ restart();
91
+ });
92
+
93
+ return {
94
+ current,
95
+ target,
96
+ displayText,
97
+ start,
98
+ restart,
99
+ hasAnimated
100
+ };
101
+ }
@@ -0,0 +1,39 @@
1
+ import { ref, onMounted, onBeforeUnmount } from 'vue';
2
+
3
+ export function useElementVisibility(element: HTMLElement | null, options: {
4
+ threshold?: number,
5
+ onVisible?: () => void
6
+ }) {
7
+ const {
8
+ threshold = 0.3,
9
+ onVisible
10
+ } = options;
11
+
12
+ const isVisible = ref(false);
13
+ const observer = ref<IntersectionObserver | null>(null);
14
+
15
+ onMounted(() => {
16
+ if (!element) {
17
+ return;
18
+ }
19
+
20
+ observer.value = new IntersectionObserver((entries) => {
21
+ entries.forEach((entry) => {
22
+ isVisible.value = entry.isIntersecting;
23
+ if (entry.isIntersecting && onVisible) {
24
+ onVisible();
25
+ }
26
+ });
27
+ }, { threshold });
28
+
29
+ observer.value.observe(element);
30
+ });
31
+
32
+ onBeforeUnmount(() => {
33
+ observer.value?.disconnect();
34
+ });
35
+
36
+ return {
37
+ isVisible
38
+ };
39
+ }
package/models/index.ts CHANGED
@@ -11,4 +11,5 @@ export * from "./map";
11
11
  export * from "./modelStatuses";
12
12
  export * from "./rules";
13
13
  export * from "./tables";
14
- export * from "./toggleSets";
14
+ export * from "./toggleSets";
15
+ export * from "./translations";
@@ -0,0 +1,4 @@
1
+ export interface Translation {
2
+ languageCode: string;
3
+ [key: string]: string;
4
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dative-gpi/foundation-shared-components",
3
3
  "sideEffects": false,
4
- "version": "1.0.179",
4
+ "version": "1.0.180-utils-composable",
5
5
  "description": "",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -10,8 +10,8 @@
10
10
  "author": "",
11
11
  "license": "ISC",
12
12
  "dependencies": {
13
- "@dative-gpi/foundation-shared-domain": "1.0.179",
14
- "@dative-gpi/foundation-shared-services": "1.0.179"
13
+ "@dative-gpi/foundation-shared-domain": "1.0.180-utils-composable",
14
+ "@dative-gpi/foundation-shared-services": "1.0.180-utils-composable"
15
15
  },
16
16
  "peerDependencies": {
17
17
  "@dative-gpi/bones-ui": "^1.0.0",
@@ -35,5 +35,5 @@
35
35
  "sass": "1.71.1",
36
36
  "sass-loader": "13.3.2"
37
37
  },
38
- "gitHead": "6500ee86f8f47666fd31d52f02426c1551a57c1c"
38
+ "gitHead": "ec9f6b8dc92d0e7057f13f031d85436781302097"
39
39
  }