tide-design-system 2.0.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/README.md +55 -0
- package/dist/IconAccountBalance-91cf067b.js +10 -0
- package/dist/IconAdd-95c51c0e.js +10 -0
- package/dist/IconAi-08172540.js +22 -0
- package/dist/IconAlignSpace-9ab2bdf2.js +10 -0
- package/dist/IconApplePay-1ee6317b.js +10 -0
- package/dist/IconArrowBack-a2226a94.js +10 -0
- package/dist/IconArrowForward-e1ca9957.js +10 -0
- package/dist/IconArrowRight-53382084.js +10 -0
- package/dist/IconAssignment-332c2b2b.js +10 -0
- package/dist/IconAwardStar-1ca35385.js +10 -0
- package/dist/IconBookmark-49b42628.js +10 -0
- package/dist/IconCalendarMonth-22c938d7.js +10 -0
- package/dist/IconCall-989a47fc.js +10 -0
- package/dist/IconCheck-a3467b47.js +10 -0
- package/dist/IconChevronLeft-c1d90bb7.js +10 -0
- package/dist/IconChevronRight-ad47891f.js +10 -0
- package/dist/IconClear-7c8fad4e.js +10 -0
- package/dist/IconClose-4b6c5aed.js +10 -0
- package/dist/IconCycle-99d40f2d.js +10 -0
- package/dist/IconDelete-446eff93.js +10 -0
- package/dist/IconDiamond-765a7d8d.js +10 -0
- package/dist/IconEdit-ce05f3b5.js +10 -0
- package/dist/IconError-7983707a.js +10 -0
- package/dist/IconExpandContent-8b6e2125.js +10 -0
- package/dist/IconExpandLess-9e23f1e9.js +10 -0
- package/dist/IconExpandMore-ded098a7.js +10 -0
- package/dist/IconFacebook-3cab65a8.js +10 -0
- package/dist/IconFavorite-5fe831f4.js +10 -0
- package/dist/IconFavoriteFilled-58fa0bf7.js +10 -0
- package/dist/IconFormatBold-889f6b8b.js +10 -0
- package/dist/IconFormatItalic-103eba00.js +10 -0
- package/dist/IconFormatListBulleted-4c824025.js +10 -0
- package/dist/IconForum-abc2fe82.js +10 -0
- package/dist/IconGoogle-281b6d80.js +27 -0
- package/dist/IconGooglePay-cc83c5c8.js +10 -0
- package/dist/IconGrid-ef763745.js +10 -0
- package/dist/IconHelp-2ad33f76.js +10 -0
- package/dist/IconInfo-5878df77.js +10 -0
- package/dist/IconInsertText-0c62badf.js +10 -0
- package/dist/IconInstagram-69e21cfb.js +10 -0
- package/dist/IconIosShare-be5f117c.js +10 -0
- package/dist/IconLayout-c1ffbcd3.js +10 -0
- package/dist/IconLinkedIn-807ef6f5.js +10 -0
- package/dist/IconLocalShipping-2c6d71e0.js +10 -0
- package/dist/IconLock-9178e816.js +10 -0
- package/dist/IconMail-0123a7c6.js +10 -0
- package/dist/IconMenu-33ed2d99.js +10 -0
- package/dist/IconMoreHoriz-c281099f.js +10 -0
- package/dist/IconNotifications-89f80e0f.js +10 -0
- package/dist/IconOpenInNew-87ad0454.js +10 -0
- package/dist/IconPalette-7ee5b40c.js +10 -0
- package/dist/IconPaypal-e311eadd.js +10 -0
- package/dist/IconPerson-932fbcbc.js +10 -0
- package/dist/IconPhotoCamera-fdbd5767.js +10 -0
- package/dist/IconPinterest-2d19c2eb.js +10 -0
- package/dist/IconPlayArrow-9837a5c0.js +10 -0
- package/dist/IconRemove-29ef8f82.js +10 -0
- package/dist/IconRoundedCorners-8ad194fc.js +10 -0
- package/dist/IconSearch-5ff23d26.js +10 -0
- package/dist/IconSell-0e0ecd20.js +10 -0
- package/dist/IconShare-47084765.js +10 -0
- package/dist/IconShoppingCart-9d6495b3.js +10 -0
- package/dist/IconSms-5ba18382.js +10 -0
- package/dist/IconStar-ef69284b.js +10 -0
- package/dist/IconSwapVert-05e14e3d.js +10 -0
- package/dist/IconThreeDRotation-2433b2e8.js +25 -0
- package/dist/IconTune-3c6452f0.js +10 -0
- package/dist/IconTwitter-a634cef4.js +10 -0
- package/dist/IconVideocam-5712435e.js +10 -0
- package/dist/IconViewInAr-d38a23d5.js +10 -0
- package/dist/IconVisibility-8cdf7151.js +10 -0
- package/dist/IconWarning-b9e61180.js +10 -0
- package/dist/IconYoutube-92447826.js +10 -0
- package/dist/css/animation.css +14 -0
- package/dist/css/dynamic-buttons.css +79 -0
- package/dist/css/dynamic-inputs.css +43 -0
- package/dist/css/dynamic-utilities.css +30 -0
- package/dist/css/main.css +10 -0
- package/dist/css/page-layout.css +57 -0
- package/dist/css/realm/aero.css +42 -0
- package/dist/css/realm/atv.css +43 -0
- package/dist/css/realm/boatmart.css +42 -0
- package/dist/css/realm/cycle.css +42 -0
- package/dist/css/realm/equip.css +42 -0
- package/dist/css/realm/pwc.css +42 -0
- package/dist/css/realm/rv.css +42 -0
- package/dist/css/realm/snow.css +42 -0
- package/dist/css/realm/truck.css +42 -0
- package/dist/css/reset.css +79 -0
- package/dist/css/storybook.css +4 -0
- package/dist/css/utilities.css +301 -0
- package/dist/css/variables.css +114 -0
- package/dist/index-c5bc4216.js +1910 -0
- package/dist/style.css +1 -0
- package/dist/tide2-design-system.js +36 -0
- package/dist/types/Alert.ts +8 -0
- package/dist/types/Badge.ts +21 -0
- package/dist/types/BreadCrumb.ts +5 -0
- package/dist/types/Detail.ts +4 -0
- package/dist/types/Element.ts +13 -0
- package/dist/types/Field.ts +55 -0
- package/dist/types/Form.ts +1 -0
- package/dist/types/Formatted.ts +22 -0
- package/dist/types/Icon.ts +77 -0
- package/dist/types/Link.ts +5 -0
- package/dist/types/ListingMedia.ts +43 -0
- package/dist/types/Priority.ts +11 -0
- package/dist/types/Raw.ts +5 -0
- package/dist/types/Realm.ts +12 -0
- package/dist/types/RealmConfig.ts +14 -0
- package/dist/types/Select.ts +4 -0
- package/dist/types/Size.ts +6 -0
- package/dist/types/Storybook.ts +14 -0
- package/dist/types/StorybookStyles.ts +209 -0
- package/dist/types/Styles.ts +479 -0
- package/dist/types/Tab.ts +5 -0
- package/dist/types/Target.ts +6 -0
- package/dist/types/TextInput.ts +16 -0
- package/dist/types/Validation.ts +18 -0
- package/dist/types/Vehicle.ts +139 -0
- package/dist/types/VehicleDetail.ts +44 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/utilities/format.ts +148 -0
- package/dist/utilities/forms.ts +47 -0
- package/dist/utilities/media.ts +77 -0
- package/dist/utilities/storybook.ts +237 -0
- package/dist/utilities/validation.ts +92 -0
- package/package.json +63 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { RealmConfig } from '@/types/RealmConfig';
|
|
2
|
+
|
|
3
|
+
const formatCamelCase = (input: string): string => {
|
|
4
|
+
return input && typeof input === 'string'
|
|
5
|
+
? input
|
|
6
|
+
.replace(/^[\s_-]/, '')
|
|
7
|
+
.replace(/([a-z]{1})([A-Z]{1})/g, '$1 $2')
|
|
8
|
+
.replace(/[\s_-]/g, ' ')
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.split(' ')
|
|
11
|
+
.map((word, index) => (index === 0 ? word : word.slice(0, 1).toUpperCase() + word.slice(1)))
|
|
12
|
+
.join('')
|
|
13
|
+
: input;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const formatKebabCase = (input: string): string => {
|
|
17
|
+
return input && typeof input === 'string'
|
|
18
|
+
? input
|
|
19
|
+
.replace(/([a-z]{1})([A-Z]{1})/g, '$1 $2')
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[\s_-]/g, '-')
|
|
22
|
+
: input;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const formatNumber = (input: number | string): string => {
|
|
26
|
+
const integer = typeof input === 'number' ? input : parseInt(input.match(/\d/g)?.join('') || '', 10);
|
|
27
|
+
const output = integer ? new Intl.NumberFormat().format(integer) : '';
|
|
28
|
+
|
|
29
|
+
return output;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const formatPascalCase = (input: string): string => {
|
|
33
|
+
return input && typeof input === 'string'
|
|
34
|
+
? input
|
|
35
|
+
.replace(/([a-z]{1})([A-Z]{1})/g, '$1 $2')
|
|
36
|
+
.replace(/[\s_-]/g, ' ')
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.split(' ')
|
|
39
|
+
.map((word) => word.slice(0, 1).toUpperCase() + word.slice(1))
|
|
40
|
+
.join('')
|
|
41
|
+
: input;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const formatPhone = (input: number | string): string => {
|
|
45
|
+
const integer = typeof input === 'number' ? input : parseInt(input.match(/\d/g)?.join('') || '', 10);
|
|
46
|
+
let output = integer ? String(integer) : '';
|
|
47
|
+
|
|
48
|
+
if (integer && typeof integer === 'number') {
|
|
49
|
+
const digits = String(integer);
|
|
50
|
+
|
|
51
|
+
output = digits;
|
|
52
|
+
|
|
53
|
+
if (digits) {
|
|
54
|
+
switch (digits.length) {
|
|
55
|
+
case 7:
|
|
56
|
+
output = `${digits.slice(0, 3)}-${digits.slice(3)}`;
|
|
57
|
+
break;
|
|
58
|
+
case 10:
|
|
59
|
+
output = `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`;
|
|
60
|
+
break;
|
|
61
|
+
case 11:
|
|
62
|
+
output = `${digits.slice(0, 1)}-${digits.slice(1, 4)}-${digits.slice(4, 7)}-${digits.slice(7)}`;
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return output;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const formatPrice = (input: number | string): string => {
|
|
74
|
+
const output = formatNumber(input);
|
|
75
|
+
|
|
76
|
+
return `$${output || '--'}`;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const formatSentenceCase = (input: string): string => {
|
|
80
|
+
let sentenceCase = input;
|
|
81
|
+
|
|
82
|
+
if (input && typeof input === 'string') {
|
|
83
|
+
const lowerCase = input
|
|
84
|
+
.replace(/([a-z]{1})([A-Z]{1})/g, '$1 $2')
|
|
85
|
+
.toLowerCase()
|
|
86
|
+
.replace(/[\s_-]/g, ' ');
|
|
87
|
+
|
|
88
|
+
sentenceCase = lowerCase.slice(0, 1).toUpperCase() + lowerCase.slice(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return sentenceCase;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const formatSnakeCase = (input: string): string => {
|
|
95
|
+
return input && typeof input === 'string'
|
|
96
|
+
? input
|
|
97
|
+
.replace(/([a-z]{1})([A-Z]{1})/g, '$1 $2')
|
|
98
|
+
.toLowerCase()
|
|
99
|
+
.replace(/[\s_-]/g, '_')
|
|
100
|
+
: input;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const formatTitleCase = (input: string): string => {
|
|
104
|
+
return input && typeof input === 'string'
|
|
105
|
+
? input
|
|
106
|
+
.replace(/([a-z]{1})([A-Z]{1})/g, '$1 $2')
|
|
107
|
+
.toLowerCase()
|
|
108
|
+
.replace(/[\s_-]/g, ' ')
|
|
109
|
+
.split(' ')
|
|
110
|
+
.map((word) => word.slice(0, 1).toUpperCase() + word.slice(1))
|
|
111
|
+
.join(' ')
|
|
112
|
+
: input;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const getArticle = (noun: string, isPlural = false, isDefinite = false) => {
|
|
116
|
+
const vowels = ['a', 'e', 'i', 'o', 'u'];
|
|
117
|
+
const isVowelLeading = vowels.includes(noun.charAt(0));
|
|
118
|
+
|
|
119
|
+
return isDefinite ? 'the' : isPlural ? 'some' : isVowelLeading ? 'an' : 'a';
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const getCdn = (realm: RealmConfig) => {
|
|
123
|
+
return `https://cdn.${realm.cdn.domain}.com`;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const getCdnMediaRoot = (realm: RealmConfig) => {
|
|
127
|
+
return `https://cdn.${realm.cdn.domain}.com/${realm.cdn.version}/media`;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const removeMarkup = (markup: string) => {
|
|
131
|
+
return markup.replace(/<[^>]*>/g, '');
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
formatCamelCase,
|
|
136
|
+
formatKebabCase,
|
|
137
|
+
formatNumber,
|
|
138
|
+
formatPascalCase,
|
|
139
|
+
formatPhone,
|
|
140
|
+
formatPrice,
|
|
141
|
+
formatSentenceCase,
|
|
142
|
+
formatSnakeCase,
|
|
143
|
+
formatTitleCase,
|
|
144
|
+
getArticle,
|
|
145
|
+
getCdn,
|
|
146
|
+
getCdnMediaRoot,
|
|
147
|
+
removeMarkup,
|
|
148
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
import type { SelectOption } from '@/types/Select';
|
|
3
|
+
import type { ValidationError, Validator } from '@/types/Validation';
|
|
4
|
+
|
|
5
|
+
import { errorMessageDefault, validateProperty } from '@/utilities/validation';
|
|
6
|
+
|
|
7
|
+
export const getFieldHasError = (errorFromProps: ValidationError, errorFromRef: ValidationError) =>
|
|
8
|
+
errorFromProps !== false || errorFromRef !== false;
|
|
9
|
+
|
|
10
|
+
export const getSelectOptionsFromStrings = (strings: string[]) =>
|
|
11
|
+
strings.map(
|
|
12
|
+
(option) =>
|
|
13
|
+
({
|
|
14
|
+
label: option,
|
|
15
|
+
value: option,
|
|
16
|
+
} as SelectOption)
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export const getSupportingText = (errorFromProps: ValidationError, errorFromRef: ValidationError) => {
|
|
20
|
+
// error in props takes precedence over validation error
|
|
21
|
+
if (typeof errorFromProps === 'string' && errorFromProps.length > 0) return errorFromProps;
|
|
22
|
+
|
|
23
|
+
return typeof errorFromRef === 'string' && errorFromRef.length > 0 ? errorFromRef : errorMessageDefault;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const handleFieldValidation = ({
|
|
27
|
+
error,
|
|
28
|
+
errorFromProps,
|
|
29
|
+
validators,
|
|
30
|
+
value,
|
|
31
|
+
}: {
|
|
32
|
+
error: Ref<ValidationError>;
|
|
33
|
+
errorFromProps: ValidationError;
|
|
34
|
+
validators?: Validator[];
|
|
35
|
+
value: Ref<string>;
|
|
36
|
+
}) => {
|
|
37
|
+
// error in props takes precedence over validation error
|
|
38
|
+
error.value = errorFromProps;
|
|
39
|
+
|
|
40
|
+
if (!errorFromProps && validators) {
|
|
41
|
+
const validation = validateProperty(value.value, validators);
|
|
42
|
+
|
|
43
|
+
if (!validation.valid) {
|
|
44
|
+
error.value = validation.message;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { MEDIA_SLIDE_TYPES, type ListingMedia } from '@/types/ListingMedia';
|
|
2
|
+
import type { VehicleDetail } from '@/types/VehicleDetail';
|
|
3
|
+
|
|
4
|
+
export function getIdFromYoutubeUrl(url: string): string {
|
|
5
|
+
const prefixes = [
|
|
6
|
+
'http://www.youtube.com/v/',
|
|
7
|
+
'https://www.youtube.com/v/',
|
|
8
|
+
'http://www.youtube.com/watch?v=',
|
|
9
|
+
'https://www.youtube.com/watch?v=',
|
|
10
|
+
'http://youtu.be/',
|
|
11
|
+
'https://youtu.be/',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
let id = url;
|
|
15
|
+
prefixes.forEach((separator) => {
|
|
16
|
+
if (url.includes(separator)) {
|
|
17
|
+
id = url.split(separator).join('');
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return id;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getListingMediaSlides(vehicle: VehicleDetail): ListingMedia[] {
|
|
25
|
+
const slides: ListingMedia[] = [];
|
|
26
|
+
|
|
27
|
+
// insert at a specific index but also handle cases where the index is out of bounds
|
|
28
|
+
const insertSlideAtIndex = (slide: ListingMedia, index: number) => {
|
|
29
|
+
slides.splice(Math.min(index, slides.length), 0, slide);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
vehicle.photos.forEach((photo) => {
|
|
33
|
+
slides.push({
|
|
34
|
+
imageUrl: String(photo.url),
|
|
35
|
+
type: MEDIA_SLIDE_TYPES.IMAGE.IMAGE,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (vehicle.floorPlanMediaId) {
|
|
40
|
+
insertSlideAtIndex(
|
|
41
|
+
{
|
|
42
|
+
imageUrl: String(vehicle.floorPlanMediaId),
|
|
43
|
+
type: MEDIA_SLIDE_TYPES.IMAGE.FLOORPLAN,
|
|
44
|
+
},
|
|
45
|
+
1
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (vehicle.video) {
|
|
50
|
+
insertSlideAtIndex(
|
|
51
|
+
{
|
|
52
|
+
imageUrl: String(vehicle.video.thumbnail),
|
|
53
|
+
type: MEDIA_SLIDE_TYPES.VIDEO,
|
|
54
|
+
videoUrl: String(vehicle.video.url),
|
|
55
|
+
},
|
|
56
|
+
2
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (vehicle.vrUrl) {
|
|
61
|
+
insertSlideAtIndex(
|
|
62
|
+
{
|
|
63
|
+
imageUrl: vehicle.photos?.[0]?.url && String(vehicle.photos[0].url),
|
|
64
|
+
type: MEDIA_SLIDE_TYPES.VR,
|
|
65
|
+
vrUrl: String(vehicle.vrUrl),
|
|
66
|
+
},
|
|
67
|
+
3
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return slides;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getSlideIndex(slideToFind: ListingMedia, allSlides: ListingMedia[]): number {
|
|
75
|
+
// TODO: find a better way of matching a slide. this is not good.
|
|
76
|
+
return allSlides.findIndex((slide) => slide === slideToFind);
|
|
77
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import type { ArgTypes } from '@storybook/vue3';
|
|
2
|
+
|
|
3
|
+
import { BOOLEAN_UNREQUIRED } from '@/types/Storybook';
|
|
4
|
+
import { ELEMENT, ELEMENT_TEXT_AS_ICON } from '@/types/Element';
|
|
5
|
+
|
|
6
|
+
// Extensible object of key/value pairs
|
|
7
|
+
type KeyValue = { [key: string]: any };
|
|
8
|
+
|
|
9
|
+
// Object with a retrievable key and an extensible object of key/value pairs as the value
|
|
10
|
+
type KeyValueNamed = {
|
|
11
|
+
[key: string]: KeyValue;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
import type { StoryContext } from '@storybook/vue3';
|
|
15
|
+
|
|
16
|
+
import { formatKebabCase } from '@/utilities/format';
|
|
17
|
+
import { NoneAsEmpty, NoneAsUndefined } from '@/types/Storybook';
|
|
18
|
+
|
|
19
|
+
export const lineBreak = '\r';
|
|
20
|
+
export const tab = ' ';
|
|
21
|
+
|
|
22
|
+
export const argTypeBooleanUnrequired = {
|
|
23
|
+
control: 'select',
|
|
24
|
+
description: 'True, False, or undefined<br />(for demonstration purposes)',
|
|
25
|
+
options: BOOLEAN_UNREQUIRED,
|
|
26
|
+
table: {
|
|
27
|
+
defaultValue: { summary: 'None' },
|
|
28
|
+
type: { summary: 'boolean' },
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const argTypeDimension = {
|
|
33
|
+
control: {
|
|
34
|
+
max: 500,
|
|
35
|
+
min: 100,
|
|
36
|
+
step: 100,
|
|
37
|
+
type: 'number',
|
|
38
|
+
},
|
|
39
|
+
table: {
|
|
40
|
+
defaultValue: { summary: 'None' },
|
|
41
|
+
type: { summary: 'number (px)' },
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const click = {
|
|
46
|
+
control: 'text',
|
|
47
|
+
description: 'JS function to execute on click',
|
|
48
|
+
table: {
|
|
49
|
+
defaultValue: { summary: 'None' },
|
|
50
|
+
type: { summary: 'function' },
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const dataTrack = {
|
|
55
|
+
control: 'text',
|
|
56
|
+
description: 'Data attribute for external tracking',
|
|
57
|
+
table: {
|
|
58
|
+
defaultValue: { summary: 'None' },
|
|
59
|
+
type: { summary: 'string' },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const doSomething = () => {
|
|
64
|
+
alert('Did something.');
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Accept a KeyValue as the value of an object with a retrievable key as a Storybook argType
|
|
68
|
+
export const formatArgType = (collection: KeyValueNamed) => {
|
|
69
|
+
const constant = getKey(collection);
|
|
70
|
+
const keyValues: KeyValue = collection[constant];
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
constant,
|
|
74
|
+
control: 'select',
|
|
75
|
+
options: {
|
|
76
|
+
...keyValues,
|
|
77
|
+
},
|
|
78
|
+
table: {
|
|
79
|
+
defaultValue: { summary: 'None' },
|
|
80
|
+
type: { summary: constant },
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const formatArgTypeCheck = (collection: KeyValueNamed) => {
|
|
86
|
+
const constant = getKey(collection);
|
|
87
|
+
const keyValues: KeyValue = collection[constant];
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
constant,
|
|
91
|
+
control: 'check',
|
|
92
|
+
options: {
|
|
93
|
+
...keyValues,
|
|
94
|
+
},
|
|
95
|
+
table: {
|
|
96
|
+
defaultValue: { summary: 'None' },
|
|
97
|
+
type: { summary: constant },
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const formatValueAsConstant = (keyValue: KeyValue, argTypes: ArgTypes) => {
|
|
103
|
+
const [key, value] = Object.entries(keyValue)[0];
|
|
104
|
+
let constant;
|
|
105
|
+
|
|
106
|
+
const arg: ArgTypes = argTypes[key];
|
|
107
|
+
|
|
108
|
+
Object.entries(arg.options).forEach(([optionKey, optionValue]) => {
|
|
109
|
+
if (value === optionValue) {
|
|
110
|
+
constant = `${argTypes[key].constant}.${optionKey}`;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return constant;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const formatSnippet = (code: string, context: StoryContext) => {
|
|
118
|
+
const tag = context.component?.__name;
|
|
119
|
+
const { args, argTypes } = context;
|
|
120
|
+
|
|
121
|
+
let classNames: string[] = [];
|
|
122
|
+
|
|
123
|
+
let attributes = Object.entries(args).map((arg: any) => {
|
|
124
|
+
const key = arg[0];
|
|
125
|
+
let value = arg[1];
|
|
126
|
+
const argType: ArgTypes = argTypes[key];
|
|
127
|
+
const conditionKey = argType.if?.arg;
|
|
128
|
+
const conditionValue = argType.if?.eq;
|
|
129
|
+
|
|
130
|
+
// TODO: TypeScript doesn't seem to believe the implict shapes of Storybook's native types?
|
|
131
|
+
const controlType = argType?.control?.type as any;
|
|
132
|
+
|
|
133
|
+
// If arg is conditional, hide when conditional is not met.
|
|
134
|
+
const isClick = key === 'click';
|
|
135
|
+
const isConditionMet = argType.if ? args[conditionKey] === conditionValue : true;
|
|
136
|
+
const isConstant = Object.keys(argTypes).includes(key) && !!argType.constant && controlType === 'select';
|
|
137
|
+
const isConstants = Object.keys(argTypes).includes(key) && !!argType.constant && controlType === 'check';
|
|
138
|
+
const isCustom = argType.isCustom;
|
|
139
|
+
const isDynamic = argType.isDynamic || isConstant || isConstants || typeof value === 'boolean';
|
|
140
|
+
const isEmpty = !isDynamic && value === '';
|
|
141
|
+
const isSlot = key === 'default';
|
|
142
|
+
const isExcluded = value === undefined || (Array.isArray(value) && !value.length);
|
|
143
|
+
|
|
144
|
+
if (argType.isCss) {
|
|
145
|
+
classNames.push(value);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (isConstant && value !== 'None') {
|
|
149
|
+
const arg: ArgTypes = argType;
|
|
150
|
+
|
|
151
|
+
Object.entries(arg.options).forEach(([optionKey, optionValue]) => {
|
|
152
|
+
if (value === optionValue) {
|
|
153
|
+
value = `${argType.constant}.${optionKey}`;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (isConstants && value.length) {
|
|
159
|
+
const constantSlots: string[] = [];
|
|
160
|
+
|
|
161
|
+
Object.entries(argType.options).forEach(([optionKey, optionValue]) => {
|
|
162
|
+
value.forEach((valueSlot: any) => {
|
|
163
|
+
if (valueSlot === optionValue) {
|
|
164
|
+
constantSlots.push(`${argTypes[key].constant}.${optionKey}`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
value = `[${constantSlots.join(', ')}]`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (isConditionMet && !isClick && !isCustom && !isEmpty && !isExcluded && !isSlot) {
|
|
173
|
+
return `${isDynamic ? ':' : ''}${formatKebabCase(key)}="${value}"`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (isCustom) {
|
|
177
|
+
return `:${formatKebabCase(key)}="${key}"`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (
|
|
181
|
+
isClick &&
|
|
182
|
+
value &&
|
|
183
|
+
(!args.element || args.element === ELEMENT.BUTTON || args.element === ELEMENT_TEXT_AS_ICON.BUTTON)
|
|
184
|
+
) {
|
|
185
|
+
return `@click="${value}"`;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
classNames = classNames.filter((className) => !!className);
|
|
190
|
+
|
|
191
|
+
if (classNames.length > 0) attributes.push(`class="${classNames.join(' ')}"`);
|
|
192
|
+
|
|
193
|
+
attributes = attributes.filter((attribute) => !!attribute).sort();
|
|
194
|
+
|
|
195
|
+
if (attributes) attributes.unshift('');
|
|
196
|
+
|
|
197
|
+
return args.default
|
|
198
|
+
? `<${tag}${attributes.join(' ')}>${lineBreak}${tab}${args.default}${lineBreak}</${tag}>`
|
|
199
|
+
: `<${tag}${attributes.join(' ')} />`;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export const formatSnippetMinimal = (code: string) => {
|
|
203
|
+
return code.replace(/<[/]*template>/g, '');
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export const getKey = (input: any) => Object.keys(input)[0];
|
|
207
|
+
|
|
208
|
+
// Invert key/value pairs bc Storybook control option format is unintuitive.
|
|
209
|
+
export const getLabelsFromOptions = (options: any) => {
|
|
210
|
+
const labels: { [key: string]: string } = {};
|
|
211
|
+
|
|
212
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
213
|
+
labels[`${value}`] = key;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return labels;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const parameters = {
|
|
220
|
+
docs: {
|
|
221
|
+
source: {
|
|
222
|
+
format: 'vue',
|
|
223
|
+
language: 'html',
|
|
224
|
+
transform: formatSnippet,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Prepend a key/value pair to a constant.
|
|
230
|
+
export const prependKeyValue = (collection: KeyValue, keyValue: KeyValue) => ({
|
|
231
|
+
...keyValue,
|
|
232
|
+
...collection,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
export const prependNoneAsUndefined = (collection: KeyValue) => prependKeyValue(collection, NoneAsUndefined);
|
|
236
|
+
|
|
237
|
+
export const prependNoneAsEmpty = (collection: KeyValue) => prependKeyValue(collection, NoneAsEmpty);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
import type { StringField } from '@/types/Field';
|
|
3
|
+
import type { ValidationResult } from '@/types/Validation';
|
|
4
|
+
|
|
5
|
+
export function checkFormat(format: RegExp) {
|
|
6
|
+
return (value: string): ValidationResult => {
|
|
7
|
+
let result = {
|
|
8
|
+
message: '',
|
|
9
|
+
valid: true,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
if (!value.trim().match(format)) {
|
|
13
|
+
result = {
|
|
14
|
+
message: errorMessageDefault,
|
|
15
|
+
valid: false,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function checkLength(minlength?: number, maxlength?: number) {
|
|
24
|
+
return (value: string): ValidationResult => {
|
|
25
|
+
const valid = getFieldLengthIsValid({
|
|
26
|
+
maxlength,
|
|
27
|
+
minlength,
|
|
28
|
+
value,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
let message = valid ? '' : errorMessageDefault;
|
|
32
|
+
|
|
33
|
+
if (maxlength && minlength) {
|
|
34
|
+
message = `Please enter a value between ${minlength} and ${maxlength} characters in length.`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
message,
|
|
39
|
+
valid,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const getFieldLengthIsValid = ({
|
|
45
|
+
maxlength,
|
|
46
|
+
minlength,
|
|
47
|
+
value,
|
|
48
|
+
}: {
|
|
49
|
+
maxlength?: number;
|
|
50
|
+
minlength?: number;
|
|
51
|
+
value: string;
|
|
52
|
+
}) => {
|
|
53
|
+
const tooShort = maxlength && value.length > maxlength;
|
|
54
|
+
const tooLong = minlength && value.length < minlength;
|
|
55
|
+
|
|
56
|
+
return !tooShort && !tooLong;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const errorMessageDefault = 'Please enter a valid value.';
|
|
60
|
+
|
|
61
|
+
export function validateFieldsFromRefs(fields: { [key: string]: Ref<StringField | null> }) {
|
|
62
|
+
let valid = true;
|
|
63
|
+
|
|
64
|
+
for (const key in fields) {
|
|
65
|
+
if (fields[key].value?.required) {
|
|
66
|
+
const value = fields[key].value?.value;
|
|
67
|
+
|
|
68
|
+
valid = valid && !!value && value.trim() !== '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const error = fields[key].value?.error;
|
|
72
|
+
|
|
73
|
+
valid = valid && !error;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return valid;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function validateProperty(value: string, validators: ((value: string) => ValidationResult)[]): ValidationResult {
|
|
80
|
+
for (const validator of validators) {
|
|
81
|
+
const validation = validator(value);
|
|
82
|
+
|
|
83
|
+
if (!validation.valid) {
|
|
84
|
+
return validation;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
message: '',
|
|
90
|
+
valid: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"author": "Trader Interactive",
|
|
3
|
+
"dependencies": {},
|
|
4
|
+
"description": "TIDE Design System",
|
|
5
|
+
"devDependencies": {
|
|
6
|
+
"@rushstack/eslint-patch": "^1.2.0",
|
|
7
|
+
"@storybook/addon-a11y": "^8.0.9",
|
|
8
|
+
"@storybook/addon-essentials": "^8.0.9",
|
|
9
|
+
"@storybook/blocks": "^8.0.9",
|
|
10
|
+
"@storybook/test": "^8.0.9",
|
|
11
|
+
"@storybook/vue3": "^8.0.9",
|
|
12
|
+
"@storybook/vue3-vite": "^8.0.9",
|
|
13
|
+
"@tsconfig/node18": "^2.0.1",
|
|
14
|
+
"@types/node": "^18.16.16",
|
|
15
|
+
"@vitejs/plugin-vue": "^4.0.0",
|
|
16
|
+
"@vitest/coverage-c8": "^0.32.0",
|
|
17
|
+
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
|
18
|
+
"@vue/eslint-config-prettier": "^7.0.0",
|
|
19
|
+
"@vue/eslint-config-typescript": "^11.0.2",
|
|
20
|
+
"@vue/test-utils": "^2.0.0-rc.18",
|
|
21
|
+
"@vue/tsconfig": "^0.4.0",
|
|
22
|
+
"eslint-plugin-storybook": "^0.8.0",
|
|
23
|
+
"eslint-plugin-vue": "^9.9.0",
|
|
24
|
+
"happy-dom": "^8.9.0",
|
|
25
|
+
"jsdom": "^21.1.0",
|
|
26
|
+
"npm-run-all": "^4.1.5",
|
|
27
|
+
"prettier": "^2.7.1",
|
|
28
|
+
"rollup-plugin-typescript2": "^0.36.0",
|
|
29
|
+
"storybook": "^8.0.9",
|
|
30
|
+
"typescript": "^5.0.0",
|
|
31
|
+
"vite": "^4.0.0",
|
|
32
|
+
"vitest": "^0.31.0",
|
|
33
|
+
"vue": "^3.2.45",
|
|
34
|
+
"vue-tsc": "^1.0.12"
|
|
35
|
+
},
|
|
36
|
+
"exports": {
|
|
37
|
+
".": "./dist/tide-design-system.js",
|
|
38
|
+
"./css": "./dist/style.css",
|
|
39
|
+
"./css/*": "./dist/css/*.css",
|
|
40
|
+
"./types/*": "./dist/types/*.ts"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"!dist/utilities/storybook.js"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"main": "./dist/tide-design-system.js",
|
|
48
|
+
"name": "tide-design-system",
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "run-p type-check build-only",
|
|
51
|
+
"build-only": "vite build && cp -r src/assets/css/ dist/css/ && cp -r src/types/ dist/types/ && cp -r src/utilities/ dist/utilities/",
|
|
52
|
+
"build-storybook": "storybook build",
|
|
53
|
+
"coverage": "vitest run --coverage",
|
|
54
|
+
"lint": "eslint . --ext .js,.ts,.vue --ignore-path .gitignore",
|
|
55
|
+
"lint:fix": "eslint . --ext .js,.ts,.vue --ignore-path .gitignore --fix",
|
|
56
|
+
"sb": "npm run storybook",
|
|
57
|
+
"storybook": "storybook dev -p 6006",
|
|
58
|
+
"test": "vitest run",
|
|
59
|
+
"type-check": "vue-tsc --noEmit"
|
|
60
|
+
},
|
|
61
|
+
"type": "module",
|
|
62
|
+
"version": "2.0.0"
|
|
63
|
+
}
|