design-system-next 1.2.19 → 1.2.22

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.
@@ -1,19 +1,19 @@
1
1
  <template>
2
2
  <div :class="avatarClassses">
3
3
  <template v-if="!initial">
4
- <img v-if="!$slots.default" :src="src" :alt="alt" :class="avatarImageClassses" />
5
- <div v-else :class="avatarImageClassses">
4
+ <img v-if="!$slots.default && src" :src="src" :alt="alt" :class="avatarImageClassses" />
5
+ <div v-else :class="[avatarImageClassses, 'avatar__slot border-color-weak border border-solid']">
6
6
  <slot />
7
7
  </div>
8
8
  </template>
9
9
  <div v-else :class="initialClassses">
10
10
  {{ initial }}
11
11
  </div>
12
- <span v-if="badge" :class="avatarNotificationClassses">
12
+ <span v-if="notification" :class="avatarNotificationClassses">
13
13
  <spr-badge :text="badgeText" variant="danger" :size="getAvatarSize.notif" />
14
14
  </span>
15
- <span v-if="online" :class="onlineNotificationClassses">
16
- <spr-badge text="" variant="brand" :size="getAvatarSize.online" />
15
+ <span v-if="badge" :class="onlineNotificationClassses">
16
+ <spr-badge text="" :variant="status" :size="getAvatarSize.badge" />
17
17
  </span>
18
18
  </div>
19
19
  </template>
@@ -3,26 +3,29 @@ import type { AvatarPropTypes } from './avatar';
3
3
 
4
4
  import classNames from 'classnames';
5
5
  export const useAvatar = (props: AvatarPropTypes) => {
6
- const { size } = toRefs(props);
6
+ const { size, color } = toRefs(props);
7
7
  const avatarClassses = computed(() => {
8
- return classNames(' relative inline-block');
8
+ return classNames(' relative inline-block rounded-full', {
9
+ 'background-color-surface': color.value === 'primary',
10
+ 'background-color': color.value === 'secondary',
11
+ });
9
12
  });
10
13
 
11
14
  const avatarImageClassses = computed(() => {
12
- return classNames(' rounded-full object-cover ', {
13
- ' h-20 min-w-20': size.value === '2xl',
14
- ' h-14 min-w-14': size.value === 'xl',
15
- ' h-10 min-w-10': size.value === 'lg',
16
- ' h-9 min-w-9': size.value === 'md',
17
- ' h-6 min-w-6': size.value === 'sm',
18
- ' h-5 min-w-5': size.value === 'xs',
19
- ' h-4 min-w-4': size.value === '2xs',
15
+ return classNames('rounded-full object-cover flex items-center justify-center overflow-hidden', {
16
+ 'h-20 min-w-20': size.value === '2xl',
17
+ 'h-14 min-w-14': size.value === 'xl',
18
+ 'h-10 min-w-10': size.value === 'lg',
19
+ 'h-9 min-w-9': size.value === 'md',
20
+ 'h-6 min-w-6': size.value === 'sm',
21
+ 'h-5 min-w-5': size.value === 'xs',
22
+ 'h-4 min-w-4': size.value === '2xs',
20
23
  });
21
24
  });
22
25
 
23
26
  const initialClassses = computed(() => {
24
27
  return classNames(
25
- ' rounded-full background-color-surface border-color-weak border border-solid items-center flex justify-center heading-xs text-color-strong',
28
+ ' rounded-full border-color-weak border border-solid items-center flex justify-center heading-xs text-color-strong',
26
29
  {
27
30
  ' h-20 min-w-20': size.value === '2xl',
28
31
  ' h-14 min-w-14 body-lg-regular-medium': size.value === 'xl',
@@ -57,11 +60,11 @@ export const useAvatar = (props: AvatarPropTypes) => {
57
60
  });
58
61
 
59
62
  const getAvatarSize = computed(() => {
60
- if (['2xl'].includes(size.value)) return { notif: 'big', online: 'big' };
61
- if (['xl', 'lg'].includes(size.value)) return { notif: 'big', online: 'tiny' };
62
- if (['md', 'sm'].includes(size.value)) return { notif: 'small', online: 'tiny' };
63
+ if (['2xl'].includes(size.value)) return { notif: 'big', badge: 'big' };
64
+ if (['xl', 'lg'].includes(size.value)) return { notif: 'big', badge: 'tiny' };
65
+ if (['md', 'sm'].includes(size.value)) return { notif: 'small', badge: 'tiny' };
63
66
 
64
- return { notif: 'tiny', online: 'tiny' };
67
+ return { notif: 'tiny', badge: 'tiny' };
65
68
  });
66
69
 
67
70
  return {
@@ -1,6 +1,6 @@
1
1
  import { ref, onMounted, watch, computed } from 'vue';
2
- import classNames from 'classnames';
3
2
  import type { SetupContext } from 'vue';
3
+ import classNames from 'classnames';
4
4
 
5
5
  import type { DropdownPropTypes, DropdownEmitTypes } from './dropdown';
6
6
 
@@ -277,7 +277,7 @@
277
277
  <template v-else>
278
278
  <Tooltip
279
279
  v-if="!parentLink.hidden"
280
- aria-id="default-tooltip"
280
+ aria-id="sidenav-tooltip-wrapper"
281
281
  placement="right"
282
282
  distance="18"
283
283
  :triggers="['hover']"
@@ -460,7 +460,7 @@
460
460
  <template v-else>
461
461
  <Tooltip
462
462
  v-if="!parentLink.hidden"
463
- aria-id="default-tooltip"
463
+ aria-id="sidenav-tooltip-wrapper"
464
464
  placement="right"
465
465
  distance="18"
466
466
  :triggers="['hover']"
@@ -0,0 +1,87 @@
1
+ import type { PropType, ExtractPropTypes } from 'vue';
2
+
3
+ const SIDEPANEL_SIZE = ['sm', 'md', 'lg', 'xl'] as const;
4
+ const SIDEPANEL_POSITION = ['right'] as const;
5
+
6
+ export const sidepanelPropTypes = {
7
+ /**
8
+ * @description Controls whether the side panel is open.
9
+ * Set to `true` to display the side panel or `false` to hide it.
10
+ */
11
+ isOpen: {
12
+ type: Boolean,
13
+ default: false,
14
+ },
15
+ /**
16
+ * @description The title displayed in the side panel's header.
17
+ * If not provided, defaults to 'Sidepanel Header'.
18
+ */
19
+ headerTitle: {
20
+ type: String,
21
+ default: 'Sidepanel Header'
22
+ },
23
+ /**
24
+ * @description Specifies the size of the side panel.
25
+ * Acceptable values are: `'sm'`, `'md'`, `'lg'`, `'xl'`.
26
+ * Defaults to `'sm'`.
27
+ */
28
+ size: {
29
+ type: String as PropType<(typeof SIDEPANEL_SIZE)[number]>,
30
+ validator: (value: (typeof SIDEPANEL_SIZE)[number]) => SIDEPANEL_SIZE.includes(value),
31
+ default: 'sm',
32
+ },
33
+ /**
34
+ * @description Sets the height of the side panel.
35
+ * Accepts a string (e.g., `'500px'`, `'70vh'`) or a number (interpreted as pixels).
36
+ * Defaults to `'calc(100vh - 32px)'`.
37
+ */
38
+ height: {
39
+ type: [String, Number],
40
+ default: 'calc(100vh - 32px)',
41
+ },
42
+ /**
43
+ * @description Controls the visibility of the side panel header.
44
+ * Set to `true` to hide the header, or `false` to display it.
45
+ * Defaults to `false`.
46
+ */
47
+ hideHeader: {
48
+ type: Boolean,
49
+ default: false
50
+ },
51
+ /**
52
+ * @description Specifies the position of the side panel.
53
+ * Currently, only `'right'` is supported.
54
+ * Defaults to `'right'`.
55
+ */
56
+ position: {
57
+ type: String as PropType<(typeof SIDEPANEL_POSITION)[number]>,
58
+ validator: (value: (typeof SIDEPANEL_POSITION)[number]) => SIDEPANEL_POSITION.includes(value),
59
+ default: 'right',
60
+ },
61
+ /**
62
+ * @description Determines whether a backdrop is displayed behind the side panel.
63
+ * Set to `true` to enable the backdrop, or `false` to disable it.
64
+ * Defaults to `true`.
65
+ */
66
+ hasBackdrop: {
67
+ type: Boolean,
68
+ default: true
69
+ },
70
+ /**
71
+ * @description Controls whether clicking outside the side panel should close it.
72
+ * Set to `true` to enable closing on outside click, or `false` to disable it.
73
+ * Defaults to `false`.
74
+ */
75
+ closeOutside: {
76
+ type: Boolean,
77
+ default: false,
78
+ }
79
+ }
80
+
81
+ export const sidepanelEmitTypes = {
82
+ close: Function,
83
+ onClose: Function,
84
+ };
85
+
86
+ export type SidepanelPropTypes = ExtractPropTypes<typeof sidepanelPropTypes>;
87
+ export type SidepanelEmitTypes = typeof sidepanelEmitTypes;
@@ -0,0 +1,60 @@
1
+ <template>
2
+ <div
3
+ v-if="isOpen && hasBackdrop"
4
+ class="w-screen h-screen bg-mushroom-700/60 fixed top-0 left-0 z-[30]"
5
+ @click="handleBackdropClick"
6
+ ></div>
7
+ <Transition
8
+ :name="`sidepanel`"
9
+ enter-active-class="transition-transform duration-[0.3s] ease-[ease-out]"
10
+ leave-active-class="transition-transform duration-[0.3s] ease-[ease-out]"
11
+ :enter-from-class="sidepanelStartEndState"
12
+ :leave-to-class="sidepanelStartEndState"
13
+ :enter-to-class="sidepanelMidState"
14
+ :leave-from-class="sidepanelMidState"
15
+ appear
16
+ >
17
+ <div
18
+ v-if="isOpen"
19
+ ref="sidepanelRef"
20
+ :class="[
21
+ sidepanelSizesClasses,
22
+ 'h-[calc(100vh_-_32px)] bg-white-50 rounded-border-radius-xl fixed right-4 z-[30] min-h-[200px] flex flex-col top-1/2 translate-y-[-50%] drop-shadow'
23
+ ]"
24
+ :style="{ height: typeof height === 'number' ? `${height}px` : height }"
25
+ >
26
+ <template v-if="!hideHeader">
27
+ <div v-if="!$slots.header" class="flex justify-between tw-min-h-12 p-4 border-mushroom-200 border-b border-solid border-0 subheading-xs text-color-strong">
28
+ {{ headerTitle }}
29
+ <Icon
30
+ class="cursor-pointer"
31
+ icon="ph:x"
32
+ @click="handleClose"
33
+ />
34
+ </div>
35
+ <div v-else>
36
+ <slot name="header"></slot>
37
+ </div>
38
+ </template>
39
+ <div :class="['overflow-y-auto p-4', {'mb-[52px]': $slots.footer}]">
40
+ <slot>
41
+ Sidepanel Content
42
+ </slot>
43
+ </div>
44
+ <div v-if="$slots.footer" class="absolute bottom-0 left-0 w-full border-0 border-t border-mushroom-200 bg-white-50 border-solid py-3 rounded-b-border-radius-xl">
45
+ <slot name="footer"></slot>
46
+ </div>
47
+ </div>
48
+ </Transition>
49
+
50
+ </template>
51
+ <script lang="ts" setup>
52
+ import { useSidepanel } from './use-sidepanel';
53
+ import { sidepanelPropTypes, sidepanelEmitTypes } from './sidepanel';
54
+ import { Icon } from '@iconify/vue';
55
+
56
+ const props = defineProps(sidepanelPropTypes);
57
+ const emit = defineEmits(sidepanelEmitTypes);
58
+
59
+ const { sidepanelRef, sidepanelSizesClasses, sidepanelMidState, sidepanelStartEndState, handleClose, handleBackdropClick } = useSidepanel(props, emit);
60
+ </script>
@@ -0,0 +1,81 @@
1
+ import { ref, computed, toRefs, watch, onMounted, onUnmounted } from 'vue';
2
+
3
+ import classNames from 'classnames';
4
+ import type { SetupContext } from 'vue';
5
+ import type { SidepanelPropTypes, SidepanelEmitTypes } from './sidepanel';
6
+
7
+ export const useSidepanel = (props: SidepanelPropTypes, emit: SetupContext<SidepanelEmitTypes>['emit']) => {
8
+ const sidepanelRef = ref<HTMLDivElement | null>(null);
9
+ const { size, position } = toRefs(props);
10
+
11
+ const sidepanelSizesClasses = computed(() => {
12
+ return classNames({
13
+ 'w-[360px]': size.value === 'sm',
14
+ 'w-[420px]': size.value === 'md',
15
+ 'w-[480px]': size.value === 'lg',
16
+ });
17
+ });
18
+
19
+ const sidepanelStartEndState = computed(() => {
20
+ return classNames({
21
+ 'translate-x-full -translate-y-2/4': position.value === 'right'
22
+ })
23
+ })
24
+
25
+ const sidepanelMidState = computed(() => {
26
+ return classNames({
27
+ 'translate-x-0 -translate-y-2/4': position.value === 'right'
28
+ })
29
+ })
30
+
31
+ const handleClose = () => {
32
+ emit('close');
33
+ };
34
+
35
+ const handleBackdropClick = () => {
36
+ if (props.closeOutside) {
37
+ emit('close')
38
+ }
39
+ }
40
+
41
+ let ignoreClick = false;
42
+
43
+ const handleClickOutside = (event: MouseEvent) => {
44
+ if (ignoreClick) return;
45
+ if (sidepanelRef.value && !sidepanelRef.value.contains(event.target as Node) && props.closeOutside) {
46
+ emit('close')
47
+ }
48
+ };
49
+
50
+ watch(
51
+ () => props.isOpen,
52
+ (value) => {
53
+ if (value) {
54
+ ignoreClick = true;
55
+ setTimeout(() => {
56
+ ignoreClick = false;
57
+ });
58
+ } else {
59
+ emit('onClose');
60
+ }
61
+ },
62
+ );
63
+
64
+ onMounted(() => {
65
+ document.addEventListener('click', handleClickOutside);
66
+ });
67
+
68
+ onUnmounted(() => {
69
+ document.removeEventListener('click', handleClickOutside);
70
+ });
71
+
72
+ return {
73
+ sidepanelRef,
74
+ sidepanelSizesClasses,
75
+ sidepanelMidState,
76
+ sidepanelStartEndState,
77
+ handleClose,
78
+ handleBackdropClick,
79
+ handleClickOutside
80
+ }
81
+ }
@@ -13,8 +13,8 @@ interface Header {
13
13
  interface TableData {
14
14
  [key: string]: {
15
15
  title: string;
16
- subtext: string;
17
- image: string;
16
+ subtext?: string;
17
+ image?: string;
18
18
  };
19
19
  }
20
20
 
@@ -36,16 +36,21 @@
36
36
  <!-- for action Button -->
37
37
  <th
38
38
  v-if="action"
39
- class="'background-color-surface text-color-strong font-size-100 font-line-height-100 font-letter-spacing-normal uppercase', sticky top-0 z-10 border-none px-size-spacing-2xs py-size-spacing-3xs text-start font-medium"
40
- />
39
+ class="background-color-surface text-color-strong font-size-100 font-line-height-100 font-letter-spacing-normal sticky top-0 z-10 border-none px-size-spacing-2xs py-size-spacing-3xs text-start font-medium uppercase"
40
+ >
41
+ <slot
42
+ name="action-name"
43
+ class="background-color-surface text-color-strong font-size-100 font-line-height-100 font-letter-spacing-normal uppercase"
44
+ />
45
+ </th>
41
46
  </tr>
42
47
  </thead>
43
48
  <tbody v-if="sortedData.length > 0">
44
49
  <tr v-for="(item, keyIndex) in sortedData" :key="keyIndex" class="hover:background-color-hover min-h-[60px]">
45
50
  <td v-for="(column, headerKey) in headers" :key="headerKey" class="border-b px-6 py-3">
46
- <div class="flex flex-row items-center gap-2">
51
+ <div v-if="sortedData[keyIndex][column.field]" class="flex flex-row items-center gap-2">
47
52
  <spr-avatar
48
- v-if="column.hasAvatar"
53
+ v-if="column.hasAvatar && sortedData[keyIndex][column.field].image"
49
54
  size="lg"
50
55
  :src="sortedData[keyIndex][column.field].image"
51
56
  alt="User Avatar"
@@ -2,7 +2,7 @@ import type { PropType, ExtractPropTypes } from 'vue';
2
2
 
3
3
  export const definePropType = <T>(val: unknown): PropType<T> => val as PropType<T>;
4
4
 
5
- const TOOLTIP_POSITION = [
5
+ const PLACEMENTS = [
6
6
  'top',
7
7
  'top-start',
8
8
  'top-end',
@@ -14,7 +14,7 @@ const TOOLTIP_POSITION = [
14
14
  'left-end',
15
15
  'right',
16
16
  'right-start',
17
- 'right-end'
17
+ 'right-end',
18
18
  ] as const;
19
19
 
20
20
  export const tooltipPropTypes = {
@@ -26,12 +26,12 @@ export const tooltipPropTypes = {
26
26
  default: 'Sample tooltip',
27
27
  },
28
28
  /**
29
- * @description Tooltip Position
29
+ * @description Tooltip Placement
30
30
  */
31
- position: {
31
+ placement: {
32
32
  type: String,
33
- validator: (value: (typeof TOOLTIP_POSITION)[number]) => TOOLTIP_POSITION.includes(value),
34
- default: 'top-start',
33
+ validator: (value: (typeof PLACEMENTS)[number]) => PLACEMENTS.includes(value),
34
+ default: 'top',
35
35
  },
36
36
  };
37
37
 
@@ -1,23 +1,23 @@
1
1
  <template>
2
- <div v-if="$slots.default" ref="componentRef" class="relative h-min w-min whitespace-nowrap">
2
+ <Tooltip class="w-fit" aria-id="tooltip-wrapper" :placement="placement">
3
+ <template #popper>
4
+ <p>{{ text }}</p>
5
+
6
+ <slot name="popper-content" />
7
+ </template>
8
+
3
9
  <slot />
4
- <div
5
- :class="[
6
- tooltipClasses,
7
- 'background-color-inverted absolute z-50 w-max min-w-10 max-w-72 text-wrap rounded-md px-size-spacing-4xs py-size-spacing-3xs',
8
- ]"
9
- >
10
- <div class="text-color-inverted-strong body-xs-regular">
11
- {{ text }}
12
- </div>
13
- </div>
14
- </div>
10
+ </Tooltip>
15
11
  </template>
16
12
 
17
13
  <script lang="ts" setup>
14
+ import { Tooltip } from 'floating-vue';
15
+ import 'floating-vue/dist/style.css';
16
+
18
17
  import { tooltipPropTypes } from './tooltip';
19
18
  import { useTooltip } from './use-tooltip';
20
19
 
21
20
  const props = defineProps(tooltipPropTypes);
22
- const { componentRef, tooltipClasses, text } = useTooltip(props);
21
+
22
+ const { placement } = useTooltip(props);
23
23
  </script>
@@ -1,37 +1,13 @@
1
- import { computed, ref, toRefs } from 'vue';
2
- import { useElementHover } from '@vueuse/core';
3
- import { TooltipPropTypes } from './tooltip';
4
-
5
- import classNames from 'classnames';
1
+ import { ref } from 'vue';
6
2
 
7
- export const useTooltip = (props: TooltipPropTypes) => {
8
- const { position, text } = toRefs(props);
3
+ import { Placement } from 'floating-vue';
9
4
 
10
- const componentRef = ref<HTMLDivElement | null>(null);
11
- const isHovered = useElementHover(componentRef);
5
+ import { TooltipPropTypes } from './tooltip';
12
6
 
13
- const tooltipClasses = computed(() => {
14
- // Tooltip Gap: 6px
15
- return classNames({
16
- hidden: !isHovered.value,
17
- 'bottom-[calc(100%+6px)] left-[50%] translate-x-[-50%]': position.value === 'top',
18
- 'bottom-[calc(100%+6px)] left-0': position.value === 'top-start',
19
- 'bottom-[calc(100%+6px)] right-0': position.value === 'top-end',
20
- 'top-[calc(100%+6px)] left-[50%] translate-x-[-50%]': position.value === 'bottom',
21
- 'top-[calc(100%+6px)] left-0': position.value === 'bottom-start',
22
- 'top-[calc(100%+6px)] right-0': position.value === 'bottom-end',
23
- 'left-[calc(100%+6px)] top-[50%] translate-y-[-50%]': position.value === 'right',
24
- 'left-[calc(100%+6px)] top-0': position.value === 'right-start',
25
- 'left-[calc(100%+6px)] bottom-0': position.value === 'right-end',
26
- 'right-[calc(100%+6px)] top-[50%] translate-y-[-50%]': position.value === 'left',
27
- 'right-[calc(100%+6px)] top-0': position.value === 'left-start',
28
- 'right-[calc(100%+6px)] bottom-0': position.value === 'left-end',
29
- });
30
- });
7
+ export const useTooltip = (props: TooltipPropTypes) => {
8
+ const placement = ref(props.placement as Placement);
31
9
 
32
10
  return {
33
- componentRef,
34
- tooltipClasses,
35
- text,
11
+ placement,
36
12
  };
37
13
  };