design-system-next 1.2.18 → 1.2.21

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.
@@ -0,0 +1,122 @@
1
+ import { ref, onMounted, watch, computed } from 'vue';
2
+ import type { SetupContext } from 'vue';
3
+ import classNames from 'classnames';
4
+
5
+ import type { DropdownPropTypes, DropdownEmitTypes } from './dropdown';
6
+
7
+ interface SelectedItem {
8
+ text: string;
9
+ value: string | number;
10
+ }
11
+
12
+ export const useDropdown = (props: DropdownPropTypes, emit: SetupContext<DropdownEmitTypes>['emit']) => {
13
+ const dropdownItemBaseClasses = computed(() => {
14
+ return classNames(
15
+ 'flex cursor-pointer items-center justify-between gap-1.5 rounded-lg p-2',
16
+ 'transition duration-150 ease-in-out',
17
+ 'hover:background-color-hover',
18
+ 'active:background-color-single-active active:scale-95',
19
+ );
20
+ });
21
+
22
+ const menuOpened = ref(false);
23
+
24
+ watch(menuOpened, () => {
25
+ handlePopperState();
26
+ });
27
+
28
+ const isSingleSelect = computed(() => props.dropdownType === 'single-select');
29
+ const isMultiSelect = computed(() => props.dropdownType === 'multi-select');
30
+
31
+ const selectedItems = ref<SelectedItem[]>([]);
32
+
33
+ const handleSelectedItem = (item: SelectedItem) => {
34
+ if (isSingleSelect.value) {
35
+ menuOpened.value = false;
36
+
37
+ selectedItems.value = [item];
38
+
39
+ emit('get-selected-item', selectedItems.value[0]);
40
+ }
41
+
42
+ if (isMultiSelect.value) {
43
+ const index = selectedItems.value.findIndex((selectedItem: SelectedItem) => selectedItem.value === item.value);
44
+
45
+ if (index === -1) {
46
+ checkboxModels.value[item.text] = true;
47
+ selectedItems.value.push(item);
48
+ } else {
49
+ checkboxModels.value[item.text] = false;
50
+ selectedItems.value.splice(index, 1);
51
+ }
52
+
53
+ emit('get-selected-item', selectedItems.value);
54
+ }
55
+ };
56
+
57
+ const isItemSelected = (item: SelectedItem) => {
58
+ if (Array.isArray(selectedItems.value)) {
59
+ return selectedItems.value.some((selectedItem) => selectedItem.text === item.text);
60
+ }
61
+
62
+ return (selectedItems.value[0] as SelectedItem).text === item.text;
63
+ };
64
+
65
+ const checkboxModels = ref<Record<string, boolean>>({});
66
+
67
+ const setCheckboxModels = () => {
68
+ if (props.menu && props.menu.length > 0) {
69
+ props.menu.forEach((item) => {
70
+ checkboxModels.value = Object.assign({}, checkboxModels.value, {
71
+ [item.text]: false,
72
+ });
73
+ });
74
+ }
75
+ };
76
+
77
+ const setPreSelectedItems = () => {
78
+ if (props.menu && props.menu.length > 0 && props.preSelectedItems && props.preSelectedItems.length > 0) {
79
+ (props.preSelectedItems as string[]).forEach((preSelectedItem: string) => {
80
+ const item = props.menu?.find((menuItem) => menuItem.text === preSelectedItem);
81
+
82
+ if (item) {
83
+ if (isMultiSelect.value) {
84
+ checkboxModels.value[item.text] = true;
85
+ }
86
+
87
+ selectedItems.value.push(item);
88
+ }
89
+ });
90
+
91
+ if (isSingleSelect.value) {
92
+ if (selectedItems.value.length > 0) {
93
+ emit('get-selected-item', selectedItems.value[0]);
94
+ }
95
+ }
96
+
97
+ if (isMultiSelect.value) {
98
+ emit('get-selected-item', selectedItems.value);
99
+ }
100
+ }
101
+ };
102
+
103
+ const handlePopperState = () => {
104
+ emit('get-popper-state', menuOpened.value);
105
+ };
106
+
107
+ onMounted(() => {
108
+ menuOpened.value = props.menuOpened;
109
+
110
+ setCheckboxModels();
111
+ setPreSelectedItems();
112
+ handlePopperState();
113
+ });
114
+
115
+ return {
116
+ dropdownItemBaseClasses,
117
+ menuOpened,
118
+ handleSelectedItem,
119
+ isItemSelected,
120
+ checkboxModels,
121
+ };
122
+ };
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div :class="wrapperClasses">
3
- <label v-if="label" :for="id" :class="labelClasses">
4
- {{ label }}
3
+ <label v-if="props.label" :for="id" :class="labelClasses">
4
+ {{ props.label }}
5
5
  </label>
6
6
  <div class="relative">
7
7
  <div v-if="$slots.prefix" :class="prefixSlotClasses">
@@ -9,9 +9,10 @@
9
9
  </div>
10
10
  <input
11
11
  :class="[inputClasses, { 'number-input': type === 'number' }]"
12
- :placeholder="placeholder"
13
- :disabled="disabled"
14
- :readonly="readonly"
12
+ :placeholder="props.placeholder"
13
+ :disabled="props.disabled"
14
+ :readonly="props.readonly"
15
+ :value="props.modelValue"
15
16
  @input="onInput"
16
17
  />
17
18
  <div v-if="$slots.trailing" :class="trailingSlotClasses">
@@ -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']"
@@ -504,7 +504,7 @@
504
504
  @click="emit('notifications', 'notifications-triggered')"
505
505
  >
506
506
  <Icon icon="ph:bell" class="h-[1.25em] w-[1.25em]" />
507
- <badge
507
+ <spr-badge
508
508
  class="absolute -top-0.5 right-2.5"
509
509
  :text="String(props.notificationCount)"
510
510
  variant="danger"
@@ -523,7 +523,12 @@
523
523
  @click="emit('requests', 'requests-triggered')"
524
524
  >
525
525
  <Icon icon="ph:check-square" class="h-[1.25em] w-[1.25em]" />
526
- <badge class="absolute -top-0.5 right-2.5" :text="String(props.requestCount)" variant="danger" size="small" />
526
+ <spr-badge
527
+ class="absolute -top-0.5 right-2.5"
528
+ :text="String(props.requestCount)"
529
+ variant="danger"
530
+ size="small"
531
+ />
527
532
  </div>
528
533
  </div>
529
534
 
@@ -625,7 +630,7 @@ import 'floating-vue/dist/style.css';
625
630
  import { sidenavPropTypes, sidenavEmitTypes } from './sidenav';
626
631
  import { useSidenav } from './use-sidenav';
627
632
 
628
- import badge from '../badge/badge.vue';
633
+ import SprBadge from '../badge/badge.vue';
629
634
 
630
635
  const props = defineProps(sidenavPropTypes);
631
636
  const emit = defineEmits(sidenavEmitTypes);
@@ -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
+ }
@@ -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
  };