+
+
+
diff --git a/src/components/bo_avatar/__stories__/bo_avatar.mdx b/src/components/bo_avatar/__stories__/bo_avatar.mdx
new file mode 100644
index 00000000..ad71d51b
--- /dev/null
+++ b/src/components/bo_avatar/__stories__/bo_avatar.mdx
@@ -0,0 +1,108 @@
+import { Canvas, Meta, Controls } from '@storybook/blocks'
+import * as BoAvatarStories from './bo_avatar.stories'
+
+
+
+# bo-avatar
+
+An avatar is a small image that represents a user or entity.
+
+## Basic usage
+
+The avatar component can be used to display an image or a text as an avatar.
+
+To use it with an image, you can use the `imageData` prop to set the image source.
+
+```html
+
+```
+
+To use it with a text, you can use the `initialsData` prop to set the text.
+
+```html
+
+```
+
+## Props
+
+- `imageData` - The image data to be displayed as an avatar (optional)
+ - default: `undefined`
+- `initialsData` - The initials data to be displayed as an avatar (optional)
+ - default: `undefined`
+- `type` - The type of the avatar (optional)
+ - default: `BoAvatarType.default`
+- `shape` - The shape of the avatar (optional)
+ - default: `BoAvatarShape.circle`
+- `size` - The size of the avatar (optional)
+ - default: `BoAvatarSize.default`
+- `indicator` - The type and position of the avatar indicator in a `BoAvatarNotificationProps` object (optional)
+ - default: `undefined`
+- `colorHex` - The color of the avatar bg in `hex` format. Applies only to avatar with text (optional)
+ - default: `undefined`
+- `fontColorHex` - The color of the avatar text in `hex` format. Applies only to avatar with text (optional)
+ - default: `undefined`
+- `clickable` - Display the avatar reactive to hover and click events (optional)
+ - default: `false`
+
+## Examples
+
+
+
+
+## Types
+
+The `type` prop can be used to change the type of the avatar. The `type` prop should be a member of the `BoAvatarType` enum.
+
+### Image
+
+In case of an image avatar, the `imageData` prop should be used to set the image source.
+This will display the image as an avatar.
+
+
+
+### Initials
+
+In case of an initials avatar, the `initialsData` prop should be used to set the initials.
+This will display the initials as an avatar.
+
+
+
+## Sizes
+
+The `size` prop can be used to change the size of the avatar. The `size` prop should be a member of the `BoAvatarSize` enum.
+
+- `extra_small`
+- `small`
+- `default`
+- `large`
+- `extra_large`
+
+
+
+## Shapes
+
+The `shape` prop can be used to change the shape of the avatar. The `shape` prop should be a member of the `BoAvatarShape` enum.
+
+- `circle`
+- `rounded`
+- `square`
+
+
+
+## Clickable
+
+The `clickable` prop can be used to make the avatar clickable. When the avatar is clickable, it will display a hover effect
+when the avatar is hovered over and the cursor is set to pointer.
+
+
+
+## With default image
+
+The `withDefaultImage` prop can be used to use the default image without any other props.
+
+
diff --git a/src/components/bo_avatar/__stories__/bo_avatar.stories.ts b/src/components/bo_avatar/__stories__/bo_avatar.stories.ts
new file mode 100644
index 00000000..dfd160f0
--- /dev/null
+++ b/src/components/bo_avatar/__stories__/bo_avatar.stories.ts
@@ -0,0 +1,250 @@
+import { BoAvatar, BoAvatarShape, BoAvatarType } from '@/components/bo_avatar'
+import { BoSize } from '@/data/bo_size.constant'
+import { StorybookUtils } from '@/utils'
+import type { Meta, StoryObj } from '@storybook/vue3'
+
+const meta = {
+ title: 'Components/bo-avatar',
+ component: BoAvatar,
+ argTypes: {
+ initialsData: {
+ description: 'The data for the avatar with a property for the initials',
+ control: { type: 'object' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ },
+ imageData: {
+ description: 'The data for the avatar with a property for the image',
+ control: { type: 'object' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ },
+ type: {
+ description: 'The type of the avatar (image, initials, unknown)',
+ control: { type: 'select' },
+ options: Object.values(BoAvatarType),
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ type: {
+ summary: 'BoAvatarType',
+ detail: StorybookUtils.stringEnumFormatter(
+ BoAvatarType,
+ 'BoAvatarType',
+ ),
+ },
+ },
+ defaultValue: BoAvatarType.unknown,
+ },
+ size: {
+ description: 'The size of the avatar',
+ control: { type: 'select' },
+ options: Object.values(BoSize),
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ type: {
+ summary: 'BoSize',
+ detail: StorybookUtils.stringEnumFormatter(BoSize, 'BoSize'),
+ },
+ },
+ defaultValue: BoSize.default,
+ },
+ shape: {
+ description: 'The shape of the avatar',
+ control: { type: 'select' },
+ options: Object.values(BoAvatarShape),
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ type: {
+ summary: 'BoAvatarShape',
+ detail: StorybookUtils.stringEnumFormatter(
+ BoAvatarShape,
+ 'BoAvatarShape',
+ ),
+ },
+ },
+ defaultValue: BoAvatarShape.rounded,
+ },
+ colorHex: {
+ description:
+ 'A hex color string used to set the background color of the avatar',
+ control: { type: 'color' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ },
+ fontColorHex: {
+ description:
+ 'The color of the font in hex format if it requires a customisation',
+ control: { type: 'color' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ },
+ clickable: {
+ description: 'Whether the avatar is clickable',
+ control: { type: 'boolean' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ defaultValue: false,
+ },
+ withDefaultImage: {
+ description: 'Whether to use the default image without any other props',
+ control: { type: 'boolean' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ defaultValue: false,
+ },
+ },
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Example: Story = {
+ args: {
+ type: BoAvatarType.initials,
+ initialsData: {
+ initials: 'BO',
+ },
+ },
+}
+
+export const Image: Story = {
+ render: (args) => ({
+ components: { BoAvatar },
+ setup() {
+ return { ...args }
+ },
+ template: `
+
_GDCi-y~^yFn~c_2S`3!SHEjbO*C?N;ops=^M(IxlFPDz8yThcGrRF+)ER>RAnFJIPA}$=V?554Zx8)jSTC4$lD(~rWW;TrMCddOJuNh($iJZ(67AU%h)a&q
z8HAqeP_xo*(UI_RS!$N;xX5i5Z@QVxrya26c>H>Y72^D6xsQTj*@H~ngyAZ&`G*hl
zj2k4QRL>K7ToXCkjm+=RzPWuUF&-8~kz}0CI)@NmcG2D29ab{)o&$EltB_OSVUoQ$
zdT+$y8iCF8ly$<(*)_i2vK|Q(wgX!xD<=>#`-Zk!vy~64X+4clBAqPAqz>yWM5}IB
z6#i;Ps1~_4YNXw)5RFpGtHeE?u6(w-Ss>8X;Mj+!9SKha3L!7Ge!XUHnNV{D{)uxL*!_I@CL&5VIF+XOX0302TjKh}
zB2Dlv4ZB`Ky8J$BY)jwWvHkZ71di}$*nj$N1L{yeRJFTOJTBKd;Z5=g$}jUax`Xla
z^14VDNRb-ai9XYXb%L=t6%bnMGaPI+u<(F@|+c=
z6REVkFW5R=Q={FlQl5N6zVgzBH+5|`-2+Q}bo`vCV&%FB+ug61`^{-C-?_M2UI|?I
zE)yE(+y4hy9SP&7l&!=YF6Uig|G1SEn4Ia*zwJ)-K10UUCd0zW0+-S^rDRS=u14sq
z7%b)u`Z?KlD_@?H0ZO)5X?xs%{LKlh`hQ%Y+Q|M)JD6#0Yjy2-G2l$@e}o$Y4O
zHD2KQgw4X`%8K0HYgyj$V|3*&nqbr2XRfF0!Y;BqqUZB}%x}FN^mCb(rz!KZsK+_3
zpypS~?Z?3pn@%A`fvPbv3gd4smQFWpQV(!k>rOt-OhPrP2vw#Pct)KQ_C&3E-E$=0
zEo8o8_^lWzIiJ2}=V6(Z=T0cJiEKLy{m?VmcZ(`k+S4g<4qW;814(MLH(Y4b!E2NoQmY*}p>50uefKBWhO`Sy=uG`mBa
zmWncE#y$zZ)(`c!wn#9Vdv9x}bsixaKN4hX7hpgx43}@h^B%LHZhroI*$
z0=!TIDgvnyxbW+xY7^7eO587LB=hKn*V7>I3myZ)ch$c0BA-OKIS0j1QZX9o9XDsg
z?~(opxIZ0$R6!W|LsGv!tWSf5X96i!rC@EQK@$H`K2!G<6K_9wz&XZ;ZFcx&d*##q
zOtB$p+QBEfql%eLKT>Se-*9b@W%z4KWg@O2I%ms+3%Qz!@A39VF25E_4*^{5&&O{d
zAN}q0(~0m^Q8(RFQw;BG;{g)}@To5%ZA*5HzmoNM3`!;Kr)E=6EWKjtRx!_iY!kjk
z+e)XczZPKDjC)31$(-^(7`9ldfC5DXRA^nFbU&pl&nJfC0z9xog}!PEdu9lto>x~}
zNx08w9C2YnE+E9=%bF!1dy$48hsNXSIEZbHMlbTyXK{QhzTwBuasBT@Vsaz6vnct9
zpb9O=$wimIx2m91w``*xZ89(3#q*xtLVR^x!<}d@{e*rtQ;On$=TR8$9$1dA0StX~qvyaH0@VHY0E5~!m>5NLL9uA{%X`Jw|740aBxXquxY`ggN
zBv){s9Ky7A%kjl1+DH_<_Qk_+1gAsH(k#*FhdoLTvAl|$7eZ@nUb#j4hT*+*1^f|T
zT0hwQj;DpTBu)E9mgF9y$rgJXf6{Br{UGO2wtfI_BXOu$+ow+zW)S9Ep?zg!9Ri+!
zMm<8WO~-RHtDnCZdzbOr>|+;rE?K=qzLlGgIuIDLzEnj88$1@j=sV?s?0R=YVB_*F
zHG#5byJYOgI!)N^M^uCq^Ap2d*3cp_N8b14;bLa)SlkQ_f02FQjmPen*+cy7rfSXb
zhXx0zWA||%zn6U|?C;4N=t&F@y&4BMSWX|jaVVXGc}aXTPY=|&!oz+#=@!^n?fP|T
z!Rwl5QBP02=~l4usK+ncLNe{W4x>AZ3G{|q*Vp_k?4Nq8!einBSL(L%
z(^$5&Dh_`V@eB&|%1?zgl5zjt=Z1GO1ZzaO3Z?;V+>k`}ST^4{OpP$WkkMWf9<17%
zTOeFc&%Hh#_AG~6hAnP~ux6zmC?pO$L^?_h5O=;Q^YG>9I{rTId$5T3#b+&YSw`0E
zQZhxa-cQA5SN&ChO4NI+!>>HTPK5=`Np(6$!dP>9%~Be?kN~rXp@})^?$2}lSd~O&
zDGlLm`qYEnyA`honqBBPO(Nw}OJKzzwQ!)H&c#ir5Fw<(Ur{X6{T?f0!wQ>{^Z_3v@{ZBNwQ{wOIW=a7R-ZLo$BEOxQ8t)z6(
z$fZ^2FmjAmN2V3W$VXPk7iav6{gd`hR-Ot(unwzk|B!ub^?A_xlfm`YrfV11Yzxpo
z40HhE*;O7S_OH-%Je@xcrAPesN#*yh+nvb7*1y!WFT2?Rn>_LCyR5c$T4np8mmg)G
zrt-h&f0ozRsR5k*qCLz#qbki`>2ba6_f?%lM$yd4Ip<#U{JMF)OW?g#Sb6I$l{MRJ
z`lkq${<9_HGlqE~bcfLLkfG0|XNc*a`r2FDnSFJkvj=8%I^I-_f`nxglbRkL6W+>T
z4h%hMoCt`$GodCd3-+8U8uZ(`Y33D@giaiElQe9-qhb{ndn(NO@6fwDH94g)Dd_r3
z#C^W)0(!@O%`NS9lB3t4MdzQaL_%Czp~hTG=3PD}C@{pf6#eOa1s)n{ADoaD%-`2O{l&4|MLSQvvYoY2
zi(62+8z?sTsYR_y!-o~Dn@3(AbC_vW3%F1m!#P-fq|jYtX!G#KgCTW~8;h9U_74Kf
zwhz!#p9EX~C}el{a!rftDaqD
z4y#)}YqwGDS9gHh3=n7UgqpCY-Q{e$HX#uY9XIk5#TzRJ%^q@kS|F83eD-
znEhT*xlq)eSB+z95&h_UBSNpQ2^
zaD6em+RaEBN+Z{=N~j+6pc~#Rvwy9>6SVm@3nj#U%@}pgD@jsWJ3aR3ML7IyYqSZgT2*3(8ujIfcvzaZ(Y#{GWbRSRaDca
zdn{?lj9lru$5S=v`3{YFW6Xu;(h-zOTv@mb%N4C~^2+#rn?85_Dgp=Gzx;k%-uBb+
zHUoOKNWIcBeke(guoEC%RkXZVu=FQGJ5uoB)o!ZxvH$dn%TbXx#{)W{h}^xEU!L?<
z9(nc!7MCLP)}ZH5&%tX`<@8pd3l~^QLW=zdn>yNa2LU2CK}zZF7aY0P7@5x%STNF#2TJ-1y9UYn*^w|8+h-h3&=-X+h82
zKis{V!r=?9mx<5MLX}3i2J0qb7I*PeW_GuYU~LQ+`d_~C6}PLCdbLhd;tANCa%
zS7yISZKwTX<$)JvotFnSg~55U$S-e#_An4MQ`#O*^r
zIvY8Ar|l3&*vSmlO9#izDzCX^guL53WrrA1zzpX1Tk-k}#@o<#5+6Sz1__dU$=*%-
ph)2LmwBKTX!#=pRcj)*5Ohfa}CZUUbz<2)x*5**N7L!{k{|Cj1gth {
+ suite('bo_avatar initials', () => {
+ let initialWrapper: ReturnType
+
+ beforeEach(() => {
+ initialWrapper = mount(BoAvatar, {
+ props: {
+ type: BoAvatarType.initials,
+ initialsData: {
+ initials: 'BO',
+ alt: 'avatar',
+ },
+ shape: BoAvatarShape.rounded,
+ size: BoSize.default,
+ colorHex: undefined,
+ fontColorHex: undefined,
+ clickable: false,
+ withDefaultImage: false,
+ },
+ })
+ })
+
+ test('the avatar container should have the default classes', () => {
+ expect(initialWrapper.find('.bo-avatar').classes()).contains('flex')
+ expect(initialWrapper.find('.bo-avatar').classes()).contains(
+ 'items-center',
+ )
+ expect(initialWrapper.find('.bo-avatar').classes()).contains(
+ 'justify-center',
+ )
+ expect(initialWrapper.find('.bo-avatar').classes()).contains(
+ 'overflow-hidden',
+ )
+ expect(initialWrapper.find('.bo-avatar').classes()).contains('shadow-sm')
+ expect(initialWrapper.find('.bo-avatar').classes()).contains('text-white')
+ })
+
+ test('the avatar should render the initials', () => {
+ expect(initialWrapper.find('span')).toBeTruthy()
+ expect(initialWrapper.find('span').text()).toBe('BO')
+ })
+
+ test('the avatar should have alt text added to the initials', () => {
+ expect(
+ initialWrapper
+ .find('.bo-avatar__initials')
+ .find('span')
+ .attributes('alt'),
+ ).toBe('avatar')
+ })
+
+ suite('avatar shapes', () => {
+ test('the avatar should have the default shape', () => {
+ expect(initialWrapper.find('.bo-avatar').classes()).toContain(
+ 'rounded-lg',
+ )
+ })
+
+ test('the avatar should be able to change the shape to circle', async () => {
+ await initialWrapper.setProps({ shape: BoAvatarShape.circle })
+ expect(initialWrapper.find('.bo-avatar').classes()).toContain(
+ 'rounded-full',
+ )
+ })
+
+ test('the avatar should be able to change the shape to square', async () => {
+ await initialWrapper.setProps({ shape: BoAvatarShape.square })
+ expect(initialWrapper.find('.bo-avatar').classes()).toContain(
+ 'rounded-none',
+ )
+ })
+
+ test('the avatar should be able to change the shape to rounded', async () => {
+ await initialWrapper.setProps({ shape: BoAvatarShape.rounded })
+ expect(initialWrapper.find('.bo-avatar').classes()).toContain(
+ 'rounded-lg',
+ )
+ })
+ })
+
+ suite('avatar sizes', () => {
+ test('the avatar should have the default size', () => {
+ const element = initialWrapper.find('.bo-avatar')
+
+ expect(element.classes()).toContain('h-[42px]')
+ expect(element.classes()).toContain('w-[42px]')
+ })
+
+ test('the avatar should be able to change the size to extra small', async () => {
+ await initialWrapper.setProps({ size: BoSize.extra_small })
+ const element = initialWrapper.find('.bo-avatar')
+
+ expect(element.classes()).toContain('h-[24px]')
+ expect(element.classes()).toContain('w-[24px]')
+ })
+
+ test('the avatar should be able to change the size to small', async () => {
+ await initialWrapper.setProps({ size: BoSize.small })
+ const element = initialWrapper.find('.bo-avatar')
+
+ expect(element.classes()).toContain('h-[32px]')
+ expect(element.classes()).toContain('w-[32px]')
+ })
+
+ test('the avatar should be able to change the size to default', async () => {
+ await initialWrapper.setProps({ size: BoSize.default })
+ const element = initialWrapper.find('.bo-avatar')
+
+ expect(element.classes()).toContain('h-[42px]')
+ expect(element.classes()).toContain('w-[42px]')
+ })
+
+ test('the avatar should be able to change the size to large', async () => {
+ await initialWrapper.setProps({ size: BoSize.large })
+ const element = initialWrapper.find('.bo-avatar')
+
+ expect(element.classes()).toContain('h-[50px]')
+ expect(element.classes()).toContain('w-[50px]')
+ })
+
+ test('the avatar should be able to change the size to extra large', async () => {
+ await initialWrapper.setProps({ size: BoSize.extra_large })
+ const element = initialWrapper.find('.bo-avatar')
+
+ expect(element.classes()).toContain('h-[60px]')
+ expect(element.classes()).toContain('w-[60px]')
+ })
+ })
+
+ suite('avatar colors', () => {
+ test('the avatar should have a random default background color if no cusotom one is provided', async () => {
+ await initialWrapper.setProps({ colorHex: undefined })
+ expect(initialWrapper.find('.bo-avatar').classes().join('')).includes(
+ 'bg-',
+ )
+ })
+
+ test('the avatar should be able to set a custom background color in hex format', async () => {
+ const colorHex = '#123456'
+ await initialWrapper.setProps({ colorHex })
+
+ const element = initialWrapper.find('.bo-avatar')
+
+ expect(element.attributes('style')).toBe(
+ 'background-color: rgb(18, 52, 86);',
+ )
+ })
+
+ test('the avatar should be able to change the font color in hex format', async () => {
+ const fontColorHex = '#123456'
+ await initialWrapper.setProps({ fontColorHex })
+
+ const element = initialWrapper.findComponent(BoText)
+
+ expect(element.attributes('style')).toBe('color: rgb(18, 52, 86);')
+ })
+ })
+
+ suite('avatar clickable', () => {
+ test('the avatar should be able to make the avatar clickable', async () => {
+ await initialWrapper.setProps({ clickable: true })
+ expect(initialWrapper.find('.bo-avatar').classes()).toContain(
+ 'cursor-pointer',
+ )
+ })
+
+ test('in case the avatar is not clickable, it should have the default cursor', async () => {
+ await initialWrapper.setProps({ clickable: false })
+ expect(initialWrapper.find('.bo-avatar').classes()).toContain(
+ 'cursor-default',
+ )
+ })
+ })
+ })
+
+ suite('bo_avatar image', () => {
+ let imageWrapper: ReturnType
+
+ beforeEach(() => {
+ imageWrapper = mount(BoAvatar, {
+ props: {
+ type: BoAvatarType.image,
+ imageData: {
+ src: 'https://example.com/image.jpg',
+ alt: 'avatar',
+ },
+ },
+ })
+ })
+
+ suite('avatar with default image', () => {
+ test('the avatar should render the default image', async () => {
+ await imageWrapper.setProps({ withDefaultImage: true })
+
+ const element = imageWrapper.find('.bo-avatar__default')
+ expect(element.exists()).toBe(true)
+ })
+
+ test('the avatar should not render this image if the prop is set to false', async () => {
+ await imageWrapper.setProps({ withDefaultImage: false })
+
+ const element = imageWrapper.find('.bo-avatar__default')
+ expect(element.exists()).toBe(false)
+ })
+ })
+
+ suite('avatar with image from src', () => {
+ test('the avatar should render the image', async () => {
+ await imageWrapper.setProps({
+ imageData: { src: 'https://example.com/image.jpg', alt: 'avatar' },
+ })
+ expect(imageWrapper.find('.bo-avatar__image')).toBeTruthy()
+ })
+
+ test('the avatar should have the alt text added to the image', async () => {
+ await imageWrapper.setProps({
+ imageData: { src: 'https://example.com/image.jpg', alt: 'avatar' },
+ })
+ expect(imageWrapper.find('.bo-avatar__image').attributes('alt')).toBe(
+ 'avatar',
+ )
+ })
+
+ test('the avatar should have the src of the image added to the image', async () => {
+ await imageWrapper.setProps({
+ imageData: { src: 'https://example.com/image.jpg', alt: 'avatar' },
+ type: BoAvatarType.image,
+ })
+
+ expect(imageWrapper.find('.bo-avatar__image').attributes('src')).toBe(
+ 'https://example.com/image.jpg',
+ )
+ })
+
+ suite('avatar image sizes', () => {
+ test('the avatar should have the default size', () => {
+ const element = imageWrapper.find('.bo-avatar')
+
+ expect(element.classes()).toContain('h-[42px]')
+ expect(element.classes()).toContain('w-[42px]')
+ })
+
+ test('the avatar should be able to change the size to extra small', async () => {
+ await imageWrapper.setProps({ size: BoSize.extra_small })
+ const element = imageWrapper.find('.bo-avatar')
+
+ expect(element.classes()).toContain('h-[24px]')
+ expect(element.classes()).toContain('w-[24px]')
+ })
+
+ test('the avatar should be able to change the size to small', async () => {
+ await imageWrapper.setProps({ size: BoSize.small })
+ const element = imageWrapper.find('.bo-avatar')
+
+ expect(element.classes()).toContain('h-[32px]')
+ expect(element.classes()).toContain('w-[32px]')
+ })
+
+ test('the avatar should be able to change the size to default', async () => {
+ await imageWrapper.setProps({ size: BoSize.default })
+ const element = imageWrapper.find('.bo-avatar')
+
+ expect(element.classes()).toContain('h-[42px]')
+ expect(element.classes()).toContain('w-[42px]')
+ })
+
+ test('the avatar should be able to change the size to large', async () => {
+ await imageWrapper.setProps({ size: BoSize.large })
+ const element = imageWrapper.find('.bo-avatar')
+
+ expect(element.classes()).toContain('h-[50px]')
+ expect(element.classes()).toContain('w-[50px]')
+ })
+
+ test('the avatar should be able to change the size to extra large', async () => {
+ await imageWrapper.setProps({ size: BoSize.extra_large })
+ const element = imageWrapper.find('.bo-avatar')
+
+ expect(element.classes()).toContain('h-[60px]')
+ expect(element.classes()).toContain('w-[60px]')
+ })
+ })
+
+ suite('avatar image shapes', () => {
+ test('the avatar should have the default shape', () => {
+ expect(imageWrapper.find('.bo-avatar').classes()).toContain(
+ 'rounded-lg',
+ )
+ })
+
+ test('the avatar should be able to change the shape to circle', async () => {
+ await imageWrapper.setProps({ shape: BoAvatarShape.circle })
+ expect(imageWrapper.find('.bo-avatar').classes()).toContain(
+ 'rounded-full',
+ )
+ })
+
+ test('the avatar should be able to change the shape to square', async () => {
+ await imageWrapper.setProps({ shape: BoAvatarShape.square })
+ expect(imageWrapper.find('.bo-avatar').classes()).toContain(
+ 'rounded-none',
+ )
+ })
+
+ test('the avatar should be able to change the shape to rounded', async () => {
+ await imageWrapper.setProps({ shape: BoAvatarShape.rounded })
+ expect(imageWrapper.find('.bo-avatar').classes()).toContain(
+ 'rounded-lg',
+ )
+ })
+ })
+ })
+ })
+})
diff --git a/src/components/bo_avatar/constants.ts b/src/components/bo_avatar/constants.ts
new file mode 100644
index 00000000..6ffa2402
--- /dev/null
+++ b/src/components/bo_avatar/constants.ts
@@ -0,0 +1,20 @@
+export enum BoAvatarShape {
+ circle = 'circle',
+ square = 'square',
+ rounded = 'rounded',
+}
+
+export enum BoAvatarVariant {
+ primary = 'primary',
+ secondary = 'secondary',
+ danger = 'danger',
+ warning = 'warning',
+ success = 'success',
+ dark = 'dark',
+}
+
+export enum BoAvatarType {
+ image = 'image',
+ initials = 'initials',
+ unknown = 'unknown',
+}
diff --git a/src/components/bo_avatar/index.ts b/src/components/bo_avatar/index.ts
new file mode 100644
index 00000000..5a422643
--- /dev/null
+++ b/src/components/bo_avatar/index.ts
@@ -0,0 +1,3 @@
+export { default as BoAvatar } from './BoAvatar.vue'
+export * from './constants'
+export * from './types'
diff --git a/src/components/bo_avatar/types.ts b/src/components/bo_avatar/types.ts
new file mode 100644
index 00000000..9fab17b5
--- /dev/null
+++ b/src/components/bo_avatar/types.ts
@@ -0,0 +1,24 @@
+import type { BoSize } from '@/data/bo_size.constant'
+import type { BoAvatarShape, BoAvatarType } from './constants'
+
+export type BoAvatarImageProps = {
+ src: string
+ alt?: string
+}
+
+export type BoAvatarInitialsProps = {
+ initials: string
+ alt?: string
+}
+
+export type BoAvatarProps = {
+ imageData?: BoAvatarImageProps
+ initialsData?: BoAvatarInitialsProps
+ type?: BoAvatarType
+ shape?: BoAvatarShape
+ size?: BoSize
+ colorHex?: string
+ fontColorHex?: string
+ clickable?: boolean
+ withDefaultImage?: boolean
+}
diff --git a/src/components/bo_badge/BoBadge.vue b/src/components/bo_badge/BoBadge.vue
index a24ea7fc..d185e181 100644
--- a/src/components/bo_badge/BoBadge.vue
+++ b/src/components/bo_badge/BoBadge.vue
@@ -3,138 +3,189 @@
class="bo-badge"
:class="classes"
>
-
+
-
+
diff --git a/src/components/bo_badge/__stories__/bo_badge.mdx b/src/components/bo_badge/__stories__/bo_badge.mdx
index 3bda8f0a..4cae086b 100644
--- a/src/components/bo_badge/__stories__/bo_badge.mdx
+++ b/src/components/bo_badge/__stories__/bo_badge.mdx
@@ -1,14 +1,14 @@
-import { Canvas, Meta, Controls } from '@storybook/blocks';
-import * as BoBadgeStories from './bo_badge.stories';
+import { Canvas, Meta, Controls } from '@storybook/blocks'
+import * as BoBadgeStories from './bo_badge.stories.ts'
# bo-badge
-A badge is a small label that is attached to an object or action.
+A badge is a small label that is attached to an object or action. The `bo-badge` component is a component that can be used to display a badge in various ways.
## Basic usage
@@ -20,31 +20,32 @@ A badge is a small label that is attached to an object or action.
- `label` - The label of the badge (optional)
- default: `''`
+ - either `label` or `prefixIcon` must be provided
- `type` - The type of the badge (optional)
- default: `BoBadgeType.default`
-- `size` - The size of the badge (optional)
+- `size` - The size of the badge member of the `BoSize` enum (optional)
- default: `BoSize.default`
-- `shape` - The shape of the badge (optional)
+- `shape` - The shape of the badge member of the `BoBadgeShape` enum (optional)
- default: `BoBadgeShape.default`
-- `variant` - The variant of the badge (optional)
+- `variant` - The variant of the badge member of the `BoBadgeVariant` enum (optional)
- default: `BoBadgeVariant.primary`
- `prefixIcon` - The icon to be displayed before the label (optional)
- default: `Icon.none`
+ - either `label` or `prefixIcon` must be provided
- `suffixIcon` - The icon to be displayed after the label (optional)
- default: `Icon.none`
+## Example
+
-## Sizes
-
-The `size` prop can be used to change the size of the badge. The `size` prop should be a member of the `BoSize` enum.
+## Icons
-- `small`
-- `default`
-- `large`
+The `prefixIcon` and `suffixIcon` props can be used to display an icon before and after the label of the badge.
+Both of these props should be a member of the `Icon` enum.
-
+
## Variants
@@ -56,11 +57,31 @@ To change the color of the component, you can use the predifened `BoBadgeVariant
- `warning`
- `success`
- `dark`
-- `purple`
-- `teal`
+## Icon only
+
+The `prefixIcon` prop can be used to display an icon only badge. In this case do not use the `label` prop
+
+
+
+## Sizes
+
+The `size` prop can be used to change the size of the badge. The `size` prop should be a member of the `BoSize` enum.
+
+- `extra-small`
+- `small`
+- `default`
+- `large`
+- `extra-large`
+
+
+
+### Sizes with icons
+
+
+
## Shapes
The `shape` prop can be used to change the shape of the badge. The `shape` prop should be a member of the `BoBadgeShape` enum.
@@ -68,6 +89,7 @@ The `shape` prop can be used to change the shape of the badge. The `shape` prop
- `default`
- `pill`
- `flat`
+- `circle`
@@ -79,17 +101,3 @@ The `type` prop can be used to change the type of the badge. The `type` prop sho
- `outline`
-
-## Icons
-
-The `prefixIcon` and `suffixIcon` props can be used to display an icon before and after the label of the badge.
-Both of these props should be a member of the `Icon` enum.
-
-
-
-## Icon only
-
-The `prefixIcon` or `suffixIcon` prop can be used to display an icon only badge. In this case do not use the `label` prop
-and set one of the before mentioned props to the desired icon.
-
-
diff --git a/src/components/bo_badge/__stories__/bo_badge.stories.ts b/src/components/bo_badge/__stories__/bo_badge.stories.ts
index 01bf91ef..71a9685e 100644
--- a/src/components/bo_badge/__stories__/bo_badge.stories.ts
+++ b/src/components/bo_badge/__stories__/bo_badge.stories.ts
@@ -1,9 +1,13 @@
-import { BoBadge, BoBadgeType, BoBadgeVariant } from '@/components/bo_badge';
-import { Icon } from '@/components/bo_icon';
-import { BoSize } from '@/global';
-import { StorybookUtils } from '@/utils';
-import type { Meta, StoryObj } from '@storybook/vue3';
-import { BoBadgeShape } from '../bo_badge';
+import {
+ BoBadge,
+ BoBadgeShape,
+ BoBadgeType,
+ BoBadgeVariant,
+} from '@/components/bo_badge'
+import { Icon } from '@/components/bo_icon'
+import { BoSize } from '@/data/bo_size.constant'
+import { StorybookUtils } from '@/utils'
+import type { Meta, StoryObj } from '@storybook/vue3'
const meta = {
title: 'Components/bo-badge',
@@ -46,7 +50,7 @@ const meta = {
detail: StorybookUtils.stringEnumFormatter(BoSize, 'BoSize'),
},
},
- defaultValue: BoBadgeVariant.primary,
+ defaultValue: BoSize.default,
},
shape: {
description: 'The shape of the badge',
@@ -111,108 +115,154 @@ const meta = {
options: Object.values(Icon),
},
},
-} satisfies Meta;
+} satisfies Meta
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
export const Example: Story = {
args: {
label: 'Badge',
},
-};
+}
export const Sizes: Story = {
- render: () => ({
+ render: (args) => ({
components: { BoBadge },
+ setup() {
+ const sizes = Object.values(BoSize)
+ return { sizes, ...args }
+ },
template: `
-
+ `,
+ }),
+ args: {
+ label: 'Label',
+ linkVariantWithShadow: true,
+ variant: BoButtonVariant.link,
+ type: HtmlButtonType.button,
},
-};
+}
diff --git a/src/components/bo_button/__test__/bo_button.test.ts b/src/components/bo_button/__test__/bo_button.test.ts
new file mode 100644
index 00000000..7535fcb6
--- /dev/null
+++ b/src/components/bo_button/__test__/bo_button.test.ts
@@ -0,0 +1,515 @@
+import {
+ BoButton,
+ BoButtonShape,
+ BoButtonVariant,
+} from '@/components/bo_button'
+import { Icon } from '@/components/bo_icon'
+import BoIcon from '@/components/bo_icon/BoIcon.vue'
+import BoLoadingSpinner from '@/components/bo_loading_spinner/BoLoadingSpinner.vue'
+import { BoText } from '@/components/bo_text'
+import { BoSize } from '@/data/bo_size.constant'
+import { BoLoaderVariant } from '@/data/loader.constant'
+import { HtmlButtonType } from '@/global'
+import { mount } from '@vue/test-utils'
+import { beforeEach, describe, expect, suite, test } from 'vitest'
+
+let globalWrapper: ReturnType
+
+beforeEach(() => {
+ globalWrapper = mount(BoButton, {
+ props: {
+ label: 'Label',
+ disabled: false,
+ size: BoSize.default,
+ type: HtmlButtonType.button,
+ variant: BoButtonVariant.primary,
+ },
+ })
+})
+
+describe('bo_button.vue', () => {
+ test('the button should render properly', () => {
+ expect(globalWrapper).toBeTruthy()
+ })
+
+ test('the container should have the correct default classes', () => {
+ expect(globalWrapper.classes()).toContain('bo-button')
+ expect(globalWrapper.classes()).toContain('inline-flex')
+ expect(globalWrapper.classes()).toContain('items-center')
+ expect(globalWrapper.classes()).toContain('justify-center')
+ expect(globalWrapper.classes()).toContain('cursor-pointer')
+ })
+
+ test('the button should render the label', () => {
+ expect(globalWrapper.findComponent(BoText).text()).toBe('Label')
+ })
+
+ suite('when the button is disabled', () => {
+ test('the button should have the disabled class', async () => {
+ await globalWrapper.setProps({ disabled: true })
+
+ expect(globalWrapper.classes()).toContain('disabled:opacity-50')
+ expect(globalWrapper.classes()).toContain('disabled:cursor-not-allowed')
+ })
+
+ test('the button should not be clickable', async () => {
+ await globalWrapper.setProps({ disabled: true })
+ expect(globalWrapper.classes()).toContain('disabled:pointer-events-none')
+ })
+ })
+
+ suite('loading state', () => {
+ test('the loading spinner should be rendered', async () => {
+ await globalWrapper.setProps({ isLoading: true })
+ expect(globalWrapper.findAllComponents(BoLoadingSpinner)).toHaveLength(1)
+ })
+
+ test('the spinner should be hidden if the button is not loading', async () => {
+ await globalWrapper.setProps({ isLoading: false })
+ expect(globalWrapper.find('bo-loading-spinner').exists()).toBe(false)
+ })
+ })
+
+ suite('icon button', () => {
+ let wrapper: ReturnType
+
+ beforeEach(() => {
+ wrapper = mount(BoButton, {
+ props: {
+ size: BoSize.default,
+ type: HtmlButtonType.button,
+ variant: BoButtonVariant.primary,
+ prefixIcon: Icon.activity,
+ },
+ })
+ })
+
+ test('the button should render the icon properly', () => {
+ expect(wrapper.findComponent(BoIcon)).toBeTruthy()
+ })
+
+ test('the button should have the correct icon size', () => {
+ expect(wrapper.findComponent(BoIcon).props('size')).toBe(BoSize.default)
+ })
+
+ test('the button should have the correct icon color', () => {
+ expect(wrapper.findComponent(BoIcon).props('color')).toBe('currentColor')
+ })
+
+ test('the label should be hidden if the button is an icon button', async () => {
+ await wrapper.setProps({ label: null })
+ expect(wrapper.find('.bo-button__label').exists()).toBe(false)
+ })
+ })
+
+ suite('button with prefix icon', () => {
+ test('the button should render the prefix icon properly', () => {
+ expect(globalWrapper.findComponent(BoIcon)).toBeTruthy()
+ })
+ })
+
+ suite('button with suffix icon', () => {
+ test('the button should render the suffix icon properly', () => {
+ expect(globalWrapper.findComponent(BoIcon)).toBeTruthy()
+ })
+ })
+
+ suite('button variants', () => {
+ let wrapper: ReturnType
+
+ beforeEach(() => {
+ wrapper = mount(BoButton, {
+ props: {
+ label: 'Label',
+ disabled: false,
+ size: BoSize.default,
+ type: HtmlButtonType.button,
+ variant: BoButtonVariant.primary,
+ },
+ })
+ })
+
+ test('primary button should have the correct color | background | focus classes', () => {
+ expect(wrapper.classes()).toContain('bg-blue-600')
+ expect(wrapper.classes()).toContain('hover:bg-blue-700')
+ expect(wrapper.classes()).toContain('focus:ring-blue-600')
+ expect(wrapper.classes()).toContain('text-white')
+ })
+
+ test('secondary button should have the correct color | background | focus classes', async () => {
+ await wrapper.setProps({ variant: BoButtonVariant.secondary })
+ expect(wrapper.classes()).toContain('bg-gray-400')
+ expect(wrapper.classes()).toContain('hover:bg-gray-700')
+ expect(wrapper.classes()).toContain('focus:ring-gray-400')
+ expect(wrapper.classes()).toContain('text-white')
+ })
+
+ test('danger button should have the correct color | background | focus classes', async () => {
+ await wrapper.setProps({ variant: BoButtonVariant.danger })
+ expect(wrapper.classes()).toContain('bg-red-600')
+ expect(wrapper.classes()).toContain('hover:bg-red-700')
+ expect(wrapper.classes()).toContain('focus:ring-red-600')
+ expect(wrapper.classes()).toContain('text-white')
+ })
+
+ test('warning button should have the correct color | background | focus classes', async () => {
+ await wrapper.setProps({ variant: BoButtonVariant.warning })
+ expect(wrapper.classes()).toContain('bg-yellow-500')
+ expect(wrapper.classes()).toContain('hover:bg-yellow-700')
+ expect(wrapper.classes()).toContain('focus:ring-yellow-500')
+ expect(wrapper.classes()).toContain('text-white')
+ })
+
+ test('success button should have the correct color | background | focus classes', async () => {
+ await wrapper.setProps({ variant: BoButtonVariant.success })
+ expect(wrapper.classes()).toContain('bg-green-600')
+ expect(wrapper.classes()).toContain('hover:bg-green-700')
+ expect(wrapper.classes()).toContain('focus:ring-green-600')
+ expect(wrapper.classes()).toContain('text-white')
+ })
+
+ test('dark button should have the correct color | background | focus classes', async () => {
+ await wrapper.setProps({ variant: BoButtonVariant.dark })
+ expect(wrapper.classes()).toContain('bg-black')
+ expect(wrapper.classes()).toContain('hover:bg-black/50')
+ expect(wrapper.classes()).toContain('focus:ring-black')
+ expect(wrapper.classes()).toContain('text-white')
+ })
+
+ test('link button should have the correct color | background | focus classes', async () => {
+ await wrapper.setProps({ variant: BoButtonVariant.link })
+ expect(wrapper.classes()).toContain('bg-transparent')
+ expect(wrapper.classes()).toContain('hover:bg-transparent')
+ expect(wrapper.classes()).toContain('focus:ring-transparent')
+ expect(wrapper.classes()).toContain('text-blue-600')
+ expect(wrapper.classes()).toContain('hover:text-blue-700')
+ expect(wrapper.classes()).toContain('focus:ring-transparent')
+ })
+
+ test('link-secondary button should have the correct color | background | focus classes', async () => {
+ await wrapper.setProps({ variant: BoButtonVariant.link_secondary })
+ expect(wrapper.classes()).toContain('bg-transparent')
+ expect(wrapper.classes()).toContain('hover:bg-transparent')
+ expect(wrapper.classes()).toContain('focus:ring-transparent')
+ expect(wrapper.classes()).toContain('text-gray-600')
+ expect(wrapper.classes()).toContain('hover:text-gray-700')
+ expect(wrapper.classes()).toContain('focus:ring-transparent')
+ })
+
+ test('link-danger button should have the correct color | background | focus classes', async () => {
+ await wrapper.setProps({ variant: BoButtonVariant.link_danger })
+ expect(wrapper.classes()).toContain('bg-transparent')
+ expect(wrapper.classes()).toContain('hover:bg-transparent')
+ expect(wrapper.classes()).toContain('focus:ring-transparent')
+ expect(wrapper.classes()).toContain('text-red-600')
+ expect(wrapper.classes()).toContain('hover:text-red-700')
+ expect(wrapper.classes()).toContain('focus:ring-transparent')
+ })
+
+ test('link-warning button should have the correct color | background | focus classes', async () => {
+ await wrapper.setProps({ variant: BoButtonVariant.link_warning })
+ expect(wrapper.classes()).toContain('bg-transparent')
+ expect(wrapper.classes()).toContain('hover:bg-transparent')
+ expect(wrapper.classes()).toContain('focus:ring-transparent')
+ expect(wrapper.classes()).toContain('text-yellow-500')
+ expect(wrapper.classes()).toContain('hover:text-yellow-700')
+ expect(wrapper.classes()).toContain('focus:ring-transparent')
+ })
+
+ test('link-success button should have the correct color | background | focus classes', async () => {
+ await wrapper.setProps({ variant: BoButtonVariant.link_success })
+ expect(wrapper.classes()).toContain('bg-transparent')
+ expect(wrapper.classes()).toContain('hover:bg-transparent')
+ expect(wrapper.classes()).toContain('focus:ring-transparent')
+ expect(wrapper.classes()).toContain('text-green-600')
+ expect(wrapper.classes()).toContain('hover:text-green-700')
+ expect(wrapper.classes()).toContain('focus:ring-transparent')
+ })
+
+ test('link-dark button should have the correct color | background | focus classes', async () => {
+ await wrapper.setProps({ variant: BoButtonVariant.link_dark })
+ expect(wrapper.classes()).toContain('bg-transparent')
+ expect(wrapper.classes()).toContain('hover:bg-transparent')
+ expect(wrapper.classes()).toContain('focus:ring-transparent')
+ expect(wrapper.classes()).toContain('text-black')
+ expect(wrapper.classes()).toContain('hover:text-black/50')
+ expect(wrapper.classes()).toContain('focus:ring-transparent')
+ })
+ })
+
+ suite('button sizes', () => {
+ let wrapper: ReturnType
+
+ beforeEach(() => {
+ wrapper = mount(BoButton, {
+ props: {
+ label: 'Label',
+ disabled: false,
+ size: BoSize.default,
+ type: HtmlButtonType.button,
+ variant: BoButtonVariant.primary,
+ },
+ })
+ })
+
+ test('the button should have the correct default size classes', () => {
+ expect(wrapper.classes()).toContain('h-[40px]')
+ expect(wrapper.classes()).toContain('px-[24px]')
+ })
+
+ test('the button should have the correct size classes if a the size is extra small', async () => {
+ await wrapper.setProps({ size: BoSize.extra_small })
+ expect(wrapper.classes()).toContain('h-[24px]')
+ expect(wrapper.classes()).toContain('px-[8px]')
+ })
+
+ test('the button should have the correct size classes if a the size is small', async () => {
+ await wrapper.setProps({ size: BoSize.small })
+ expect(wrapper.classes()).toContain('h-[32px]')
+ expect(wrapper.classes()).toContain('px-[16px]')
+ })
+
+ test('the button should have the correct size classes if a the size is default', async () => {
+ await wrapper.setProps({ size: BoSize.default })
+ expect(wrapper.classes()).toContain('h-[40px]')
+ expect(wrapper.classes()).toContain('px-[24px]')
+ })
+
+ test('the button should have the correct size classes if a the size is large', async () => {
+ await wrapper.setProps({ size: BoSize.large })
+ expect(wrapper.classes()).toContain('h-[48px]')
+ expect(wrapper.classes()).toContain('px-[32px]')
+ })
+
+ test('the button should have the correct size classes if a the size is extra large', async () => {
+ await wrapper.setProps({ size: BoSize.extra_large })
+ expect(wrapper.classes()).toContain('h-[56px]')
+ expect(wrapper.classes()).toContain('px-[40px]')
+ })
+ })
+
+ suite('button shapes', () => {
+ let wrapper: ReturnType
+
+ beforeEach(() => {
+ wrapper = mount(BoButton, {
+ props: {
+ label: 'Label',
+ disabled: false,
+ size: BoSize.default,
+ type: HtmlButtonType.button,
+ shape: BoButtonShape.default,
+ variant: BoButtonVariant.primary,
+ },
+ })
+ })
+
+ test('default button should have the correct border radius classes', () => {
+ expect(wrapper.classes()).toContain('rounded-lg')
+ })
+
+ test('pill button should have the correct border radius classes', async () => {
+ await wrapper.setProps({ shape: BoButtonShape.pill })
+ expect(wrapper.classes()).toContain('rounded-full')
+ })
+
+ test('outline button should have the correct border radius classes', async () => {
+ await wrapper.setProps({ shape: BoButtonShape.outline })
+ expect(wrapper.classes()).toContain('rounded-lg')
+ })
+ })
+
+ suite('button shadows', () => {
+ let wrapper: ReturnType
+
+ beforeEach(() => {
+ wrapper = mount(BoButton, {
+ props: {
+ label: 'Label',
+ disabled: false,
+ size: BoSize.default,
+ type: HtmlButtonType.button,
+ variant: BoButtonVariant.primary,
+ },
+ })
+ })
+
+ test('default button should have the correct shadow classes', () => {
+ expect(wrapper.classes()).toContain('shadow-md')
+ })
+
+ test('the button should have the correct shadow classes', () => {
+ expect(wrapper.classes()).toContain('shadow-md')
+ })
+
+ test('by default the link types buttton should not have a shadow', async () => {
+ await wrapper.setProps({ variant: BoButtonVariant.link })
+ expect(wrapper.classes()).toContain('shadow-none')
+ })
+
+ test('if the prop is set to true the link types buttton should have a shadow', async () => {
+ await wrapper.setProps({ linkVariantWithShadow: true })
+ expect(wrapper.classes()).toContain('shadow-md')
+ })
+ })
+
+ suite('button loader variants', () => {
+ suite('outline button', () => {
+ let wrapper: ReturnType
+
+ beforeEach(() => {
+ wrapper = mount(BoButton, {
+ props: {
+ label: 'Label',
+ disabled: false,
+ isLoading: true,
+ size: BoSize.default,
+ type: HtmlButtonType.button,
+ shape: BoButtonShape.outline,
+ variant: BoButtonVariant.primary,
+ },
+ })
+ })
+
+ test('the button should have the correct default loader if no other loader is provided', () => {
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('variant')).toBe(BoLoaderVariant.primary)
+ })
+
+ test('the button should have the correct loader if a different loader is provided', () => {
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('variant')).toBe(BoLoaderVariant.primary)
+ })
+
+ test('the button should be able to render a blue primary loader', async () => {
+ await wrapper.setProps({ variant: BoLoaderVariant.primary })
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('variant')).toBe(BoLoaderVariant.primary)
+ })
+
+ test('the button should be able to render a gray secondary loader', async () => {
+ await wrapper.setProps({ variant: BoLoaderVariant.secondary })
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('variant')).toBe(BoLoaderVariant.secondary)
+ })
+
+ test('the button should be able to render a red danger loader', async () => {
+ await wrapper.setProps({ variant: BoLoaderVariant.danger })
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('variant')).toBe(BoLoaderVariant.danger)
+ })
+
+ test('the button should be able to render a yellow warning loader', async () => {
+ await wrapper.setProps({ variant: BoLoaderVariant.warning })
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('variant')).toBe(BoLoaderVariant.warning)
+ })
+
+ test('the button should be able to render a green success loader', async () => {
+ await wrapper.setProps({ variant: BoLoaderVariant.success })
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('variant')).toBe(BoLoaderVariant.success)
+ })
+
+ test('the button should be able to render a dark dark loader', async () => {
+ await wrapper.setProps({ variant: BoLoaderVariant.dark })
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('variant')).toBe(BoLoaderVariant.dark)
+ })
+ })
+
+ suite('pill button', () => {
+ let wrapper: ReturnType
+
+ beforeEach(() => {
+ wrapper = mount(BoButton, {
+ props: {
+ label: 'Label',
+ disabled: false,
+ isLoading: true,
+ size: BoSize.default,
+ type: HtmlButtonType.button,
+ shape: BoButtonShape.pill,
+ variant: BoButtonVariant.primary,
+ },
+ })
+ })
+
+ test('the button should render a white loader with the default button variant', () => {
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('variant')).toBe(BoLoaderVariant.white)
+ })
+
+ test('the button should render a white loader with the primary button variant', async () => {
+ await wrapper.setProps({ variant: BoLoaderVariant.primary })
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('variant')).toBe(BoLoaderVariant.white)
+ })
+
+ test('the button should render a white loader with the secondary button variant', async () => {
+ await wrapper.setProps({ variant: BoLoaderVariant.secondary })
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('variant')).toBe(BoLoaderVariant.white)
+ })
+
+ test('the button should render a white loader with the danger button variant', async () => {
+ await wrapper.setProps({ variant: BoLoaderVariant.danger })
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('variant')).toBe(BoLoaderVariant.white)
+ })
+
+ test('the button should render a white loader with the warning button variant', async () => {
+ await wrapper.setProps({ variant: BoLoaderVariant.warning })
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('variant')).toBe(BoLoaderVariant.white)
+ })
+
+ test('the button should render a white loader with the success button variant', async () => {
+ await wrapper.setProps({ variant: BoLoaderVariant.success })
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('variant')).toBe(BoLoaderVariant.white)
+ })
+
+ test('the button should render a white loader with the dark button variant', async () => {
+ await wrapper.setProps({ variant: BoLoaderVariant.dark })
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('variant')).toBe(BoLoaderVariant.white)
+ })
+ })
+ })
+
+ suite('loader size', () => {
+ let wrapper: ReturnType
+
+ beforeEach(() => {
+ wrapper = mount(BoButton, {
+ props: {
+ label: 'Label',
+ disabled: false,
+ isLoading: true,
+ size: BoSize.default,
+ type: HtmlButtonType.button,
+ shape: BoButtonShape.outline,
+ variant: BoButtonVariant.primary,
+ },
+ })
+ })
+
+ test('the button should have the correct default loader size', () => {
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('size')).toBe(BoSize.default)
+ })
+
+ test('the button should have the correct loader size if a different size is provided', async () => {
+ await wrapper.setProps({ size: BoSize.small })
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('size')).toBe(BoSize.small)
+ })
+
+ test('the button should be able to render a large loader', async () => {
+ await wrapper.setProps({ size: BoSize.large })
+ const loader = wrapper.findComponent(BoLoadingSpinner)
+ expect(loader.props('size')).toBe(BoSize.default)
+ })
+ })
+})
diff --git a/src/components/bo_button/__tests__/__snapshots__/bo_button.test.ts.snap b/src/components/bo_button/__tests__/__snapshots__/bo_button.test.ts.snap
deleted file mode 100644
index 1d8c5057..00000000
--- a/src/components/bo_button/__tests__/__snapshots__/bo_button.test.ts.snap
+++ /dev/null
@@ -1,8 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`bo_button.vue > should match snapshot 1`] = `
-""
-`;
diff --git a/src/components/bo_button/__tests__/bo_button.test.ts b/src/components/bo_button/__tests__/bo_button.test.ts
deleted file mode 100644
index f993aca0..00000000
--- a/src/components/bo_button/__tests__/bo_button.test.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { Icon } from '@/components/bo_icon';
-import { BoSize } from '@/global';
-import { mount } from '@vue/test-utils';
-import { beforeEach, describe, expect, test } from 'vitest';
-import {
- BoButton,
- BoButtonBorderRadiusClasses,
- BoButtonFilledColorClasses,
- BoButtonType,
- BoButtonVariant,
-} from '..';
-
-let wrapper: ReturnType;
-
-beforeEach(() => {
- wrapper = mount(BoButton, {
- props: {
- label: 'Label',
- variant: BoButtonVariant.primary,
- type: BoButtonType.default,
- disabled: false,
- size: BoSize.default,
- },
- });
-});
-
-describe('bo_button.vue', () => {
- test('should match snapshot', () => {
- expect(wrapper.html()).toMatchSnapshot();
- });
-
- test('should render the button', () => {
- expect(wrapper.find('button')).toBeTruthy();
- });
-
- test('should have the default size', () => {
- expect(wrapper.find('w-6')).toBeTruthy();
- expect(wrapper.find('h-6')).toBeTruthy();
- });
-
- test('should have the default type', () => {
- expect(wrapper.find('bg-blue-500')).toBeTruthy();
- expect(wrapper.find('rounded-lg')).toBeTruthy();
- });
-
- test('should be able to change the size', async () => {
- await wrapper.setProps({ size: BoSize.large });
- expect(wrapper.find('w-8')).toBeTruthy();
- expect(wrapper.find('h-8')).toBeTruthy();
- });
-
- test('should be able to change the type', async () => {
- await wrapper.setProps({ type: BoSize.large });
- expect(wrapper.find('bg-blue-500')).toBeTruthy();
- expect(wrapper.find('rounded-lg')).toBeTruthy();
- });
-
- test('should be able to change the color', async () => {
- await wrapper.setProps({ color: '#000000' });
- expect(wrapper.find('bg-blue-500')).toBeTruthy();
- expect(wrapper.find('rounded-lg')).toBeTruthy();
- });
-
- test('should be able to change the variant', async () => {
- await wrapper.setProps({ variant: BoButtonVariant.secondary });
- expect(wrapper.find('bg-gray-600')).toBeTruthy();
- expect(wrapper.find('rounded-lg')).toBeTruthy();
- });
-
- test('should be able to change the type', async () => {
- await wrapper.setProps({ type: BoButtonType.pill });
- expect(wrapper.find('bg-blue-600')).toBeTruthy();
- expect(wrapper.find('rounded-full')).toBeTruthy();
- });
-
- test('should be able to change the disabled', async () => {
- await wrapper.setProps({ disabled: true });
- expect(wrapper.find('disabled')).toBeTruthy();
- });
-
- test('should be able to change the prefix icon', async () => {
- await wrapper.setProps({ prefixIcon: Icon.alert_circle });
- expect(wrapper.find('svg')).toBeTruthy();
- });
-
- test('should be able to change the suffix icon', async () => {
- await wrapper.setProps({ suffixIcon: Icon.alert_circle });
- expect(wrapper.find('svg')).toBeTruthy();
- });
-
- test('should be able to change the border radius', async () => {
- await wrapper.setProps({ borderRadius: BoButtonBorderRadiusClasses.pill });
- expect(wrapper.find('rounded-full')).toBeTruthy();
- });
-
- test('should be able to change the fill color', async () => {
- await wrapper.setProps({ fillColor: BoButtonFilledColorClasses.primary });
- expect(wrapper.classes().join(' ')).includes(
- 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-600 text-white',
- );
- });
-});
diff --git a/src/components/bo_button/bo_button.ts b/src/components/bo_button/bo_button.ts
deleted file mode 100644
index 9ae48e7d..00000000
--- a/src/components/bo_button/bo_button.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-export enum BoButtonType {
- default = 'default',
- pill = 'pill',
- outline = 'outline',
-}
-
-export enum BoButtonVariant {
- primary = 'primary',
- secondary = 'secondary',
- danger = 'danger',
- warning = 'warning',
- success = 'success',
- dark = 'dark',
- purple = 'purple',
- teal = 'teal',
- link = 'link',
-}
-
-export enum BoButtonBorderRadiusClasses {
- pill = /*tw*/ 'rounded-full',
- default = /*tw*/ 'rounded-lg',
- outline = /*tw*/ 'rounded-lg',
-}
-
-export enum BoButtonPaddingClasses {
- small = /*tw*/ 'px-2.5',
- default = /*tw*/ 'px-3',
- large = /*tw*/ 'px-3.5',
-}
-
-export enum BoButtonTextSizeClasses {
- small = /*tw*/ 'text-small leading-small',
- default = /*tw*/ 'text-default leading-default',
- large = /*tw*/ 'text-large leading-large',
-}
-
-export enum BoButtonHeightClasses {
- small = /*tw*/ 'h-6',
- default = /*tw*/ 'h-8',
- large = /*tw*/ 'h-10',
-}
-
-export enum BoButtonShadowClasses {
- primary = /*tw*/ 'shadow-sm shadow-blue-500/50 dark:shadow-sm dark:shadow-blue-800/80',
- secondary = /*tw*/ 'shadow-sm shadow-gray-500/50 dark:shadow-sm dark:shadow-gray-800/80',
- danger = /*tw*/ 'shadow-sm shadow-red-500/50 dark:shadow-sm dark:shadow-red-800/80',
- warning = /*tw*/ 'shadow-sm shadow-yellow-500/50 dark:shadow-sm dark:shadow-yellow-800/80',
- success = /*tw*/ 'shadow-sm shadow-green-500/50 dark:shadow-sm dark:shadow-green-800/80',
- dark = /*tw*/ 'shadow-sm shadow-black-500/50 dark:shadow-sm dark:shadow-black-800/80',
- purple = /*tw*/ 'shadow-sm shadow-purple-500/50 dark:shadow-sm dark:shadow-purple-800/80',
- teal = /*tw*/ 'shadow-sm shadow-teal-500/50 dark:shadow-sm dark:shadow-teal-800/80',
- link = /*tw*/ 'shadow-none',
-}
-
-export enum BoButtonFilledColorClasses {
- primary = /*tw*/ 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-600 text-white',
- secondary = /*tw*/ 'bg-gray-600 hover:bg-gray-700 focus:ring-gray-600 text-white',
- danger = /*tw*/ 'bg-red-600 hover:bg-red-700 focus:ring-red-600 text-white',
- warning = /*tw*/ 'bg-yellow-600 hover:bg-yellow-700 focus:ring-yellow-600 text-white',
- success = /*tw*/ 'bg-green-600 hover:bg-green-700 focus:ring-green-600 text-white',
- dark = /*tw*/ 'bg-black hover:bg-black/50 focus:ring-black text-white',
- purple = /*tw*/ 'bg-purple-600 hover:bg-purple-700 focus:ring-purple-600 text-white',
- teal = /*tw*/ 'bg-teal-600 hover:bg-teal-700 focus:ring-teal-600 text-white',
- link = /*tw*/ 'bg-transparent hover:bg-transparent focus:ring-transparent text-blue-600 hover:text-blue-700 focus:ring-blue-600',
-}
-
-export enum BoButtonFilledDarkColorClasses {
- primary = /*tw*/ 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-600 text-white',
- secondary = /*tw*/ 'bg-gray-600 hover:bg-gray-700 focus:ring-gray-600 text-white',
- danger = /*tw*/ 'bg-red-600 hover:bg-red-700 focus:ring-red-600 text-white',
- warning = /*tw*/ 'bg-yellow-600 hover:bg-yellow-700 focus:ring-yellow-600 text-white',
- success = /*tw*/ 'bg-green-600 hover:bg-green-700 focus:ring-green-600 text-white',
- dark = /*tw*/ 'bg-black hover:bg-black/50 focus:ring-black text-white',
- purple = /*tw*/ 'bg-purple-600 hover:bg-purple-700 focus:ring-purple-600 text-white',
- teal = /*tw*/ 'bg-teal-600 hover:bg-teal-700 focus:ring-teal-600 text-white',
- link = /*tw*/ 'bg-transparent hover:bg-transparent focus:ring-transparent text-blue-600 hover:text-blue-700 focus:ring-blue-600',
-}
-
-export enum BoButtonOutlineColorClasses {
- primary = /*tw*/ 'border border-blue-600 hover:bg-blue-600 focus:ring-blue-600 text-blue-600 hover:text-white',
- secondary = /*tw*/ 'border border-gray-600 hover:bg-gray-600 focus:ring-gray-600 text-gray-600 hover:text-white',
- danger = /*tw*/ 'border border-red-600 hover:bg-red-600 focus:ring-red-600 text-red-600 hover:text-white',
- warning = /*tw*/ 'border border-yellow-600 hover:bg-yellow-600 focus:ring-yellow-600 text-yellow-600 hover:text-white',
- success = /*tw*/ 'border border-green-600 hover:bg-green-600 focus:ring-green-600 text-green-600 hover:text-white',
- dark = /*tw*/ 'border border-black hover:bg-black focus:ring-black text-black hover:text-white',
- purple = /*tw*/ 'border border-purple-600 hover:bg-purple-600 focus:ring-purple-600 text-purple-600 hover:text-white',
- teal = /*tw*/ 'border border-teal-600 hover:bg-teal-600 focus:ring-teal-600 text-teal-600 hover:text-white',
- link = /*tw*/ 'bg-transparent hover:bg-transparent focus:ring-transparent text-blue-600 hover:text-blue-700 focus:ring-blue-600',
-}
-
-export enum BoButtonOutlineDarkColorClasses {
- primary = /*tw*/ 'border border-blue-600 hover:bg-blue-600 focus:ring-blue-600 text-blue-600 hover:text-white',
- secondary = /*tw*/ 'border border-gray-600 hover:bg-gray-600 focus:ring-gray-600 text-gray-600 hover:text-white',
- danger = /*tw*/ 'border border-red-600 hover:bg-red-600 focus:ring-red-600 text-red-600 hover:text-white',
- warning = /*tw*/ 'border border-yellow-600 hover:bg-yellow-600 focus:ring-yellow-600 text-yellow-600 hover:text-white',
- success = /*tw*/ 'border border-green-600 hover:bg-green-600 focus:ring-green-600 text-green-600 hover:text-white',
- dark = /*tw*/ 'border border-black hover:bg-black focus:ring-black text-black hover:text-white',
- purple = /*tw*/ 'border border-purple-600 hover:bg-purple-600 focus:ring-purple-600 text-purple-600 hover:text-white',
- teal = /*tw*/ 'border border-teal-600 hover:bg-teal-600 focus:ring-teal-600 text-teal-600 hover:text-white',
- link = /*tw*/ 'bg-transparent hover:bg-transparent focus:ring-transparent text-blue-600 hover:text-blue-700 focus:ring-blue-600',
-}
diff --git a/src/components/bo_button/constants.ts b/src/components/bo_button/constants.ts
new file mode 100644
index 00000000..dad7c15d
--- /dev/null
+++ b/src/components/bo_button/constants.ts
@@ -0,0 +1,20 @@
+export enum BoButtonVariant {
+ primary = 'primary',
+ secondary = 'secondary',
+ danger = 'danger',
+ warning = 'warning',
+ success = 'success',
+ dark = 'dark',
+ link = 'link',
+ link_secondary = 'link_secondary',
+ link_danger = 'link_danger',
+ link_warning = 'link_warning',
+ link_success = 'link_success',
+ link_dark = 'link_dark',
+}
+
+export enum BoButtonShape {
+ default = 'default',
+ pill = 'pill',
+ outline = 'outline',
+}
diff --git a/src/components/bo_button/index.ts b/src/components/bo_button/index.ts
index 3c363f63..ee10bc50 100644
--- a/src/components/bo_button/index.ts
+++ b/src/components/bo_button/index.ts
@@ -1,14 +1,3 @@
-export { default as BoButton } from './BoButton.vue';
-export {
- BoButtonBorderRadiusClasses,
- BoButtonFilledColorClasses,
- BoButtonFilledDarkColorClasses,
- BoButtonHeightClasses,
- BoButtonOutlineColorClasses,
- BoButtonOutlineDarkColorClasses,
- BoButtonPaddingClasses,
- BoButtonShadowClasses,
- BoButtonTextSizeClasses,
- BoButtonType,
- BoButtonVariant,
-} from './bo_button';
+export { default as BoButton } from './BoButton.vue'
+export * from './constants'
+export * from './types'
diff --git a/src/components/bo_button/types.ts b/src/components/bo_button/types.ts
new file mode 100644
index 00000000..04f46f83
--- /dev/null
+++ b/src/components/bo_button/types.ts
@@ -0,0 +1,17 @@
+import type { Icon } from '@/components/bo_icon'
+import type { BoSize } from '@/data/bo_size.constant'
+import type { HtmlButtonType } from '@/global/html_button'
+import type { BoButtonShape, BoButtonVariant } from './constants'
+
+export type BoButtonProps = {
+ label?: string
+ variant?: BoButtonVariant
+ shape?: BoButtonShape
+ type?: HtmlButtonType
+ disabled?: boolean
+ isLoading?: boolean
+ size?: BoSize
+ prefixIcon?: Icon
+ suffixIcon?: Icon
+ linkVariantWithShadow?: boolean
+}
diff --git a/src/components/bo_card/BoCard.vue b/src/components/bo_card/BoCard.vue
new file mode 100644
index 00000000..8528f8e0
--- /dev/null
+++ b/src/components/bo_card/BoCard.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/bo_card/__stories__/bo_card.mdx b/src/components/bo_card/__stories__/bo_card.mdx
new file mode 100644
index 00000000..72801592
--- /dev/null
+++ b/src/components/bo_card/__stories__/bo_card.mdx
@@ -0,0 +1,90 @@
+import { Canvas, Meta, Controls } from '@storybook/blocks'
+import * as BoCardStories from './bo_card.stories'
+
+
+
+# bo-card
+
+A card is a container that holds related content and actions. The `bo-card` component is a component that can be used to display a card in various ways.
+
+## Basic usage
+
+The component to use is called `bo-card`.
+
+```html
+
+
+
// some content
+
+
+```
+
+## Props
+
+- `title` - The title of the card (optional)
+ - default: `undefined`
+- `description` - The description of the card (optional)
+ - default: `undefined`
+- `clickable` - Whether the card is clickable (optional)
+ - default: `false`
+- `widthInPx` - The width of the card in pixels (optional)
+ - default: `undefined`
+- `widthInPercent` - The width of the card in percent (optional)
+ - default: `undefined`
+- `widthAsTailwindClass` - The width of the card as a tailwind class (optional)
+ - default: `undefined`
+
+## Examples
+
+
+
+
+## Title and description
+
+In case you don't want to use the `content` slot, you can use the `title` and `description` props to display a title and description.
+
+
+
+## Clickable
+
+To make the card clickable, you can use the `clickable` prop.
+This will add a cursor pointer and a hover effect to the card.
+
+
+
+## No padding
+
+To remove the padding from the card, you can use the `padding` prop.
+The `padding` prop should be an object with the following properties:
+
+- `top` - Whether to add padding to the top (optional)
+ - default: `true`
+- `right` - Whether to add padding to the right (optional)
+ - default: `true`
+- `bottom` - Whether to add padding to the bottom (optional)
+ - default: `true`
+- `left` - Whether to add padding to the left (optional)
+ - default: `true`
+
+
+
+## Width in px
+
+To set the width of the card in pixels, you can use the `widthInPx` prop.
+
+
+
+## Width in percent
+
+To set the width of the card in percent, you can use the `widthInPercent` prop.
+
+
+
+## Width as tailwind class
+
+To set the width of the card as a tailwind class, you can use the `widthAsTailwindClass` prop.
+
+
diff --git a/src/components/bo_card/__stories__/bo_card.stories.ts b/src/components/bo_card/__stories__/bo_card.stories.ts
new file mode 100644
index 00000000..188b3b62
--- /dev/null
+++ b/src/components/bo_card/__stories__/bo_card.stories.ts
@@ -0,0 +1,224 @@
+import { BoBadge, BoBadgeVariant } from '@/components/bo_badge'
+import { BoCard } from '@/components/bo_card'
+import {
+ BoFontFamily,
+ BoFontSize,
+ BoText,
+ BoTextColor,
+} from '@/components/bo_text'
+import type { Meta, StoryObj } from '@storybook/vue3'
+
+const meta = {
+ title: 'Components/bo-card',
+ component: BoCard,
+ argTypes: {
+ content: {
+ description: 'The main slot of the card',
+ table: {
+ category: 'slots',
+ subcategory: 'optional',
+ },
+ },
+ title: {
+ description: 'The title of the card',
+ control: { type: 'text' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ },
+ description: {
+ description: 'The description of the card',
+ control: { type: 'text' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ },
+ clickable: {
+ description: 'Whether the card is clickable',
+ control: { type: 'boolean' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ defaultValue: false,
+ },
+ widthInPx: {
+ description: 'The width of the card in pixels',
+ control: { type: 'number' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ },
+ widthInPercent: {
+ description: 'The width of the card in percent',
+ control: { type: 'number' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ },
+ widthAsTailwindClass: {
+ description: 'The width of the card as a tailwind class',
+ control: { type: 'text' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ },
+ padding: {
+ description: 'The padding of the card',
+ control: { type: 'object' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ defaultValue: {
+ top: true,
+ right: true,
+ bottom: true,
+ left: true,
+ },
+ },
+ },
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Example: Story = {
+ render: () => ({
+ components: { BoCard, BoBadge },
+ template: `
+
+
+
+
+
+
diff --git a/src/components/bo_input/__stories__/bo_input.mdx b/src/components/bo_input/__stories__/bo_input.mdx
new file mode 100644
index 00000000..6958ef86
--- /dev/null
+++ b/src/components/bo_input/__stories__/bo_input.mdx
@@ -0,0 +1,126 @@
+import { Canvas, Meta, Controls } from '@storybook/blocks'
+import * as BoInputStories from './bo_input.stories'
+
+
+
+# bo-input
+
+A input is a control that allows the user to enter and edit text in a single line.
+
+## Basic usage
+
+The component to use is called `bo-input`.
+
+```html
+
+```
+
+## Props
+
+- `modelValue` - The value of the input (required)
+ - default: `''`
+- `id` - The id of the input (optional)
+ - default: `IdentityUtils.generateRandomIdWithPrefix('bo-input')`
+- `label` - The label of the input (optional)
+ - default: `''`
+- `description` - The description of the input (optional)
+ - default: `''`
+- `type` - The type of the input (optional)
+ - default: `'text'`
+- `size` - The size of the input (optional)
+ - default: `BoInputSize.default`
+- `state` - The state of the input (optional)
+ - default: `BoInputState.none`
+- `disabled` - Whether the input is disabled (optional)
+ - default: `false`
+- `isLoading` - Whether the input is loading (optional)
+ - default: `false`
+- `placeholder` - The placeholder of the input (optional)
+ - default: `''`
+- `readonly` - Whether the input is readonly (optional)
+ - default: `false`
+- `prefixIcon` - The icon to be displayed before the input (optional)
+ - default: `null`
+- `suffixIcon` - The icon to be displayed after the input (optional)
+ - default: `null`
+- `errorMessage` - The error message to be displayed (optional)
+ - default: `null`
+
+## Examples
+
+
+
+
+## Disabled
+
+The `disabled` prop can be used to disable the input. When the input is disabled, it will not respond to user interactions and will appear in a disabled state.
+
+
+
+## Readonly
+
+The `readonly` prop can be used to make the input readonly. When the input is readonly, the user will not be able to modify the value of the input.
+
+
+
+## Sizes
+
+The `size` prop can be used to change the size of the input. The `size` prop should be a member of the `BoInputSize` enum.
+
+- `small`
+- `default`
+- `large`
+
+
+
+## States
+
+The `state` prop can be used to change the state of the input. The `state` prop should be a member of the `BoInputState` enum.
+
+- `none`
+- `valid`
+- `invalid`
+- `required`
+
+
+
+## Loading
+
+The `isLoading` prop can be used to show a loading spinner. When the spinner is shown, the input will be disabled and will not respond to user interactions.
+
+
+
+## Loader variant
+
+The `loaderVariant` prop can be used to change the loader variant. The `loaderVariant` prop should be a string that represents the variant of the loader. The available values for the `loaderVariant` prop are:
+
+- `spinner`
+- `pulse`
+
+
+
+## Required
+
+The `state` prop can be used to make the input required. When the input is required, the `errorMessage` prop should be provided.
+
+
+
+## Error
+
+The `state` prop can be used to show an error message. When the input is in an error state, the `errorMessage` prop should be provided.
+
+
+
+## Icons
+
+The `prefixIcon` and `suffixIcon` props can be used to display an icon before and after the input.
+Both of these props should be a member of the `Icon` enum.
+
+
diff --git a/src/components/bo_input/__stories__/bo_input.stories.ts b/src/components/bo_input/__stories__/bo_input.stories.ts
new file mode 100644
index 00000000..c4fd4622
--- /dev/null
+++ b/src/components/bo_input/__stories__/bo_input.stories.ts
@@ -0,0 +1,466 @@
+import { Icon } from '@/components/bo_icon'
+import { BoInput, BoInputSize, BoInputState } from '@/components/bo_input'
+import { HtmlInputType } from '@/global'
+import { StorybookUtils } from '@/utils'
+import type { Meta, StoryObj } from '@storybook/vue3'
+
+const meta = {
+ title: 'Components/bo-input',
+ component: BoInput,
+ argTypes: {
+ id: {
+ description: 'The id of the input',
+ control: { type: 'text' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ },
+ required: {
+ description: 'Whether the input is required',
+ control: { type: 'boolean' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ defaultValue: false,
+ },
+ modelValue: {
+ description: 'The value of the input',
+ control: { type: 'text' },
+ table: {
+ category: 'props',
+ subcategory: 'required',
+ },
+ },
+ label: {
+ description: 'The label of the input',
+ control: { type: 'text' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ },
+ type: {
+ description: 'The type of the input',
+ control: { type: 'select' },
+ options: [
+ HtmlInputType.tel,
+ HtmlInputType.url,
+ HtmlInputType.date,
+ HtmlInputType.time,
+ HtmlInputType.week,
+ HtmlInputType.text,
+ HtmlInputType.month,
+ HtmlInputType.email,
+ HtmlInputType.number,
+ HtmlInputType.password,
+ HtmlInputType.datetime_local,
+ ],
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ type: {
+ summary: 'InputTypeHTMLAttribute',
+ },
+ },
+ defaultValue: 'text',
+ },
+ description: {
+ description: 'The description of the input',
+ control: { type: 'text' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ },
+ size: {
+ description: 'The size of the input',
+ control: { type: 'select' },
+ options: Object.values(BoInputSize),
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ type: {
+ summary: 'BoInputSize',
+ detail: StorybookUtils.stringEnumFormatter(
+ BoInputSize,
+ 'BoInputSize',
+ ),
+ },
+ },
+ defaultValue: BoInputSize.default,
+ },
+ state: {
+ description: 'The state of the input',
+ control: { type: 'select' },
+ options: Object.values(BoInputState),
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ type: {
+ summary: 'BoInputState',
+ detail: StorybookUtils.stringEnumFormatter(
+ BoInputState,
+ 'BoInputState',
+ ),
+ },
+ },
+ defaultValue: BoInputState.none,
+ },
+ disabled: {
+ description: 'Whether the input is disabled',
+ control: { type: 'boolean' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ defaultValue: false,
+ },
+ isLoading: {
+ description: 'Whether the input is loading',
+ control: { type: 'boolean' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ defaultValue: false,
+ },
+ placeholder: {
+ description: 'The placeholder of the input',
+ control: { type: 'text' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ defaultValue: '',
+ },
+ readonly: {
+ description: 'Whether the input is readonly',
+ control: { type: 'boolean' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ defaultValue: false,
+ },
+ errorMessage: {
+ description: 'The error message to be displayed',
+ control: { type: 'text' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ defaultValue: null,
+ },
+ prefixIcon: {
+ description: 'The icon to be displayed before the input',
+ control: { type: 'text' },
+ defaultValue: null,
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ type: {
+ summary: 'Icon',
+ detail: StorybookUtils.stringEnumFormatter(Icon, 'Icon'),
+ },
+ },
+ options: Object.values(Icon),
+ },
+ suffixIcon: {
+ description: 'The icon to be displayed after the input',
+ control: { type: 'text' },
+ defaultValue: null,
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ type: {
+ summary: 'Icon',
+ detail: StorybookUtils.stringEnumFormatter(Icon, 'Icon'),
+ },
+ },
+ options: Object.values(Icon),
+ },
+ loaderVariant: {
+ description: 'The variant of the loader',
+ control: { type: 'select' },
+ options: ['spinner', 'pulse'],
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ type: {
+ summary: 'string',
+ },
+ },
+ defaultValue: 'pulse',
+ },
+ },
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+function generateRandomId(): string {
+ return Symbol(
+ Math.random().toString(36).substring(2, 15) + Date.now(),
+ ).toString()
+}
+
+export const Example: Story = {
+ args: {
+ modelValue: 'Hello world!',
+ label: 'Name',
+ description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
+ size: BoInputSize.default,
+ state: BoInputState.none,
+ disabled: false,
+ isLoading: false,
+ readonly: false,
+ placeholder: 'Enter your name',
+ type: HtmlInputType.text,
+ },
+}
+
+export const Sizes: Story = {
+ render: (args) => ({
+ components: { BoInput },
+ setup() {
+ const sizes = Object.values(BoInputSize)
+ return { sizes, ...args, generateRandomId }
+ },
+ template: `
+
+
+
+
diff --git a/src/components/bo_loading_pulse/__stories__/bo_loading_pulse.mdx b/src/components/bo_loading_pulse/__stories__/bo_loading_pulse.mdx
new file mode 100644
index 00000000..abde58e8
--- /dev/null
+++ b/src/components/bo_loading_pulse/__stories__/bo_loading_pulse.mdx
@@ -0,0 +1,84 @@
+import { Canvas, Meta, Controls } from '@storybook/blocks'
+import * as BoLoadingPulseStories from './bo_loading_pulse.stories'
+
+
+
+# bo-loading-pulse
+
+A loading pulse is a graphical representation of an object or action that is being processed.
+
+## Basic usage
+
+```html
+
+```
+
+## Props
+
+- `size` - The size of the spinner (optional)
+ - default: `BoSize.default`
+- `variant` - The variant of the spinner (optional)
+ - default: `BoLoaderVariant.primary`
+- `loaderText` - The text to be displayed in the loader (optional)
+ - default: `''`
+- `customColor` - A hex value to set as the color of the spinner (optional)
+ - default: `undefined`
+
+## Examples
+
+
+
+
+## Variants
+
+To change the color of the component, you can use the predifened `BoLoaderVariant` enum. The `BoLoaderVariant` enum includes the following variants:
+
+- `primary`
+- `secondary`
+- `danger`
+- `warning`
+- `success`
+- `dark`
+- `white`
+
+
+
+## Sizes
+
+The `size` prop can be used to change the size of the spinner. The `size` prop should be a member of the `BoSize` enum.
+
+- `extra-small`
+- `small`
+- `default`
+- `large`
+- `extra-large`
+
+
+
+## Custom color
+
+The `customColor` prop can be used to change the custom color of the spinner.
+The `customColor` prop should be a string that represents the color in `hex` format.
+
+
+
+## With loader text
+
+The `loaderText` prop can be used to display a text in the spinner.
+
+
+
+## Text position
+
+The `textPosition` prop can be used to change the position of the text in the spinner.
+The `textPosition` prop should be a string that represents the position of the text in the spinner
+
+The available values for the `textPosition` prop are:
+
+- `side`
+- `bottom`
+
+
diff --git a/src/components/bo_loading_pulse/__stories__/bo_loading_pulse.stories.ts b/src/components/bo_loading_pulse/__stories__/bo_loading_pulse.stories.ts
new file mode 100644
index 00000000..51a2342d
--- /dev/null
+++ b/src/components/bo_loading_pulse/__stories__/bo_loading_pulse.stories.ts
@@ -0,0 +1,189 @@
+import { BoLoadingPulse } from '@/components/bo_loading_pulse'
+import { BoColor } from '@/data'
+import { BoSize } from '@/data/bo_size.constant'
+import { BoLoaderTextPosition, BoLoaderVariant } from '@/data/loader.constant'
+import { StorybookUtils } from '@/utils'
+import type { Meta, StoryObj } from '@storybook/vue3'
+
+const meta = {
+ title: 'Components/bo-loading-pulse',
+ component: BoLoadingPulse,
+ argTypes: {
+ size: {
+ description: 'The size of the spinner',
+ control: { type: 'select' },
+ options: Object.values(BoSize),
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ type: {
+ summary: 'BoSize',
+ detail: StorybookUtils.stringEnumFormatter(BoSize, 'BoSize'),
+ },
+ },
+ defaultValue: BoSize.default,
+ },
+ variant: {
+ description: 'The variant of the spinner',
+ control: { type: 'select' },
+ options: Object.values(BoLoaderVariant),
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ type: {
+ summary: 'BoLoaderVariant',
+ detail: StorybookUtils.stringEnumFormatter(
+ BoLoaderVariant,
+ 'BoLoaderVariant',
+ ),
+ },
+ },
+ },
+ loaderText: {
+ description: 'The text to be displayed in the loader',
+ control: { type: 'text' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ defaultValue: '',
+ },
+ customColor: {
+ description: 'The custom color of the text',
+ control: { type: 'color' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ },
+ textPosition: {
+ description: 'The position of the text',
+ control: { type: 'select' },
+ options: Object.values(BoLoaderTextPosition),
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ type: {
+ summary: 'BoLoaderTextPosition',
+ detail: StorybookUtils.stringEnumFormatter(
+ BoLoaderTextPosition,
+ 'BoLoaderTextPosition',
+ ),
+ },
+ },
+ defaultValue: BoLoaderTextPosition.bottom,
+ },
+ },
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Example: Story = {
+ args: {
+ size: BoSize.default,
+ variant: BoLoaderVariant.primary,
+ },
+}
+
+export const Variants: Story = {
+ render: (args) => ({
+ components: { BoLoadingPulse },
+ setup() {
+ const variants = Object.values(BoLoaderVariant)
+ return { variants, ...args }
+ },
+ template: `
+
+ `,
+ }),
+ args: {
+ size: BoSize.default,
+ variant: BoLoaderVariant.primary,
+ loaderText: 'Loading...',
+ textPosition: BoLoaderTextPosition.side,
+ },
+}
diff --git a/src/components/bo_loading_pulse/__test__/bo_loading_pulse.test.ts b/src/components/bo_loading_pulse/__test__/bo_loading_pulse.test.ts
new file mode 100644
index 00000000..777de152
--- /dev/null
+++ b/src/components/bo_loading_pulse/__test__/bo_loading_pulse.test.ts
@@ -0,0 +1,156 @@
+import { BoLoadingPulse } from '@/components/bo_loading_pulse'
+import { BoText } from '@/components/bo_text'
+import { BoSize } from '@/data/bo_size.constant'
+import { BoLoaderTextPosition, BoLoaderVariant } from '@/data/loader.constant'
+import { mount } from '@vue/test-utils'
+import { beforeEach, describe, expect, suite, test } from 'vitest'
+
+let globalWrapper: ReturnType
+
+beforeEach(() => {
+ globalWrapper = mount(BoLoadingPulse, {
+ props: {
+ size: BoSize.default,
+ variant: BoLoaderVariant.primary,
+ },
+ })
+})
+
+describe('bo_loading_pulse.vue', () => {
+ test('loading pulse should render properly', () => {
+ expect(globalWrapper).toBeDefined()
+ })
+
+ test('loading pulse should have the default classes', () => {
+ expect(globalWrapper.classes()).toContain('bo-loading-pulse__container')
+ expect(globalWrapper.classes()).toContain('flex')
+ expect(globalWrapper.classes()).toContain('h-full')
+ expect(globalWrapper.classes()).toContain('w-full')
+ expect(globalWrapper.classes()).toContain('content-center')
+ expect(globalWrapper.classes()).toContain('items-center')
+ expect(globalWrapper.classes()).toContain('justify-center')
+ expect(globalWrapper.classes()).toContain('gap-2')
+ })
+
+ test('absolute outer pulse should have the default classes', () => {
+ expect(
+ globalWrapper.find('.bo-loading-pulse__outer-pulse-absolute').classes(),
+ ).toContain('absolute')
+ expect(
+ globalWrapper.find('.bo-loading-pulse__outer-pulse-absolute').classes(),
+ ).toContain('inline-flex')
+ expect(
+ globalWrapper.find('.bo-loading-pulse__outer-pulse-absolute').classes(),
+ ).toContain('h-full')
+ expect(
+ globalWrapper.find('.bo-loading-pulse__outer-pulse-absolute').classes(),
+ ).toContain('w-full')
+ expect(
+ globalWrapper.find('.bo-loading-pulse__outer-pulse-absolute').classes(),
+ ).toContain('animate-ping')
+ expect(
+ globalWrapper.find('.bo-loading-pulse__outer-pulse-absolute').classes(),
+ ).toContain('rounded-full')
+ expect(
+ globalWrapper.find('.bo-loading-pulse__outer-pulse-absolute').classes(),
+ ).toContain('opacity-75')
+ })
+
+ test('relative inner pulse should have the default classes', () => {
+ expect(
+ globalWrapper.find('.bo-loading-pulse__inner-pulse-relative').classes(),
+ ).toContain('relative')
+ expect(
+ globalWrapper.find('.bo-loading-pulse__inner-pulse-relative').classes(),
+ ).toContain('flex')
+ })
+
+ suite('loader variant', () => {
+ test('should be ablte to render a default variant', () => {
+ expect(
+ globalWrapper.find('.bo-loading-pulse__outer-pulse-absolute').classes(),
+ ).toContain('bg-blue-600')
+ })
+
+ test('should be ablte to render a primary variant', async () => {
+ await globalWrapper.setProps({ variant: BoLoaderVariant.primary })
+ expect(
+ globalWrapper.find('.bo-loading-pulse__outer-pulse-absolute').classes(),
+ ).toContain('bg-blue-600')
+ })
+
+ test('should be ablte to render a secondary variant', async () => {
+ await globalWrapper.setProps({ variant: BoLoaderVariant.secondary })
+ expect(
+ globalWrapper.find('.bo-loading-pulse__outer-pulse-absolute').classes(),
+ ).toContain('bg-gray-600')
+ })
+
+ test('should be ablte to render a danger variant', async () => {
+ await globalWrapper.setProps({ variant: BoLoaderVariant.danger })
+ expect(
+ globalWrapper.find('.bo-loading-pulse__outer-pulse-absolute').classes(),
+ ).toContain('bg-red-600')
+ })
+
+ test('should be ablte to render a warning variant', async () => {
+ await globalWrapper.setProps({ variant: BoLoaderVariant.warning })
+ expect(
+ globalWrapper.find('.bo-loading-pulse__outer-pulse-absolute').classes(),
+ ).toContain('bg-yellow-600')
+ })
+
+ test('should be ablte to render a success variant', async () => {
+ await globalWrapper.setProps({ variant: BoLoaderVariant.success })
+ expect(
+ globalWrapper.find('.bo-loading-pulse__outer-pulse-absolute').classes(),
+ ).toContain('bg-green-600')
+ })
+
+ test('should be ablte to render a dark variant', async () => {
+ await globalWrapper.setProps({ variant: BoLoaderVariant.dark })
+ expect(
+ globalWrapper.find('.bo-loading-pulse__outer-pulse-absolute').classes(),
+ ).toContain('bg-black')
+ })
+
+ test('should be ablte to render a white variant', async () => {
+ await globalWrapper.setProps({ variant: BoLoaderVariant.white })
+ expect(
+ globalWrapper.find('.bo-loading-pulse__outer-pulse-absolute').classes(),
+ ).toContain('bg-white')
+ })
+ })
+
+ suite('loader text', () => {
+ test('should be ablte to render text', async () => {
+ await globalWrapper.setProps({ loaderText: 'Loading...' })
+ expect(globalWrapper.findComponent(BoText).props('text')).toBe(
+ 'Loading...',
+ )
+ })
+
+ test('by default the text should positioned under the text', async () => {
+ await globalWrapper.setProps({ loaderText: 'Loading...' })
+ expect(
+ globalWrapper.find('.bo-loading-pulse__container').classes(),
+ ).toContain('flex-col')
+ })
+
+ test('the loader text should be positioned on the side', async () => {
+ await globalWrapper.setProps({ textPosition: BoLoaderTextPosition.side })
+ expect(
+ globalWrapper.find('.bo-loading-pulse__container').classes(),
+ ).toContain('flex-row')
+ })
+
+ test('the loader text should be positioned on the bottom', async () => {
+ await globalWrapper.setProps({
+ textPosition: BoLoaderTextPosition.bottom,
+ })
+ expect(
+ globalWrapper.find('.bo-loading-pulse__container').classes(),
+ ).toContain('flex-col')
+ })
+ })
+})
diff --git a/src/components/bo_loading_pulse/index.ts b/src/components/bo_loading_pulse/index.ts
new file mode 100644
index 00000000..827ae4ed
--- /dev/null
+++ b/src/components/bo_loading_pulse/index.ts
@@ -0,0 +1,2 @@
+export { default as BoLoadingPulse } from './BoLoadingPulse.vue'
+export * from './types'
diff --git a/src/components/bo_loading_pulse/types.ts b/src/components/bo_loading_pulse/types.ts
new file mode 100644
index 00000000..9b3a8839
--- /dev/null
+++ b/src/components/bo_loading_pulse/types.ts
@@ -0,0 +1,13 @@
+import type { BoSize } from '@/data/bo_size.constant'
+import type {
+ BoLoaderTextPosition,
+ BoLoaderVariant,
+} from '@/data/loader.constant'
+
+export type BoLoadingPulseProps = {
+ size?: BoSize
+ variant?: BoLoaderVariant
+ customColor?: string
+ loaderText?: string
+ textPosition?: BoLoaderTextPosition
+}
diff --git a/src/components/bo_loading_spinner/BoLoadingSpinner.vue b/src/components/bo_loading_spinner/BoLoadingSpinner.vue
new file mode 100644
index 00000000..61a67f6e
--- /dev/null
+++ b/src/components/bo_loading_spinner/BoLoadingSpinner.vue
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/bo_loading_spinner/__stories__/bo_loading_spinner.mdx b/src/components/bo_loading_spinner/__stories__/bo_loading_spinner.mdx
new file mode 100644
index 00000000..1d2ea9c9
--- /dev/null
+++ b/src/components/bo_loading_spinner/__stories__/bo_loading_spinner.mdx
@@ -0,0 +1,84 @@
+import { Canvas, Meta, Controls } from '@storybook/blocks'
+import * as BoLoadingSpinnerStories from './bo_loading_spinner.stories'
+
+
+
+# bo-loading-spinner
+
+A loading spinner is a graphical representation of an object or action that is being processed.
+
+## Basic usage
+
+```html
+
+```
+
+## Props
+
+- `size` - The size of the spinner (optional)
+ - default: `BoSize.default`
+- `variant` - The variant of the spinner (optional)
+ - default: `BoLoadingSpinnerVariant.primary`
+- `loaderText` - The text to be displayed in the loader (optional)
+ - default: `''`
+- `customColor` - A hex value to set as the color of the spinner (optional)
+ - default: `undefined`
+
+## Examples
+
+
+
+
+## Variants
+
+To change the color of the component, you can use the predifened `BoLoadingSpinnerVariant` enum. The `BoLoadingSpinnerVariant` enum includes the following variants:
+
+- `primary`
+- `secondary`
+- `danger`
+- `warning`
+- `success`
+- `dark`
+- `white`
+
+
+
+## Sizes
+
+The `size` prop can be used to change the size of the spinner. The `size` prop should be a member of the `BoSize` enum.
+
+- `extra-small`
+- `small`
+- `default`
+- `large`
+- `extra-large`
+
+
+
+## Custom color
+
+The `customColor` prop can be used to change the custom color of the spinner.
+The `customColor` prop should be a string that represents the color in `hex` format.
+
+
+
+## With loader text
+
+The `loaderText` prop can be used to display a text in the spinner.
+
+
+
+## Text position
+
+The `textPosition` prop can be used to change the position of the text in the spinner.
+The `textPosition` prop should be a string that represents the position of the text in the spinner
+
+The available values for the `textPosition` prop are:
+
+- `side`
+- `bottom`
+
+
diff --git a/src/components/bo_loading_spinner/__stories__/bo_loading_spinner.stories.ts b/src/components/bo_loading_spinner/__stories__/bo_loading_spinner.stories.ts
new file mode 100644
index 00000000..aa76537f
--- /dev/null
+++ b/src/components/bo_loading_spinner/__stories__/bo_loading_spinner.stories.ts
@@ -0,0 +1,189 @@
+import { BoLoadingSpinner } from '@/components/bo_loading_spinner'
+import { BoColor } from '@/data'
+import { BoSize } from '@/data/bo_size.constant'
+import { BoLoaderTextPosition, BoLoaderVariant } from '@/data/loader.constant'
+import { StorybookUtils } from '@/utils'
+import type { Meta, StoryObj } from '@storybook/vue3'
+
+const meta = {
+ title: 'Components/bo-loading-spinner',
+ component: BoLoadingSpinner,
+ argTypes: {
+ size: {
+ description: 'The size of the spinner',
+ control: { type: 'select' },
+ options: Object.values(BoSize),
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ type: {
+ summary: 'BoSize',
+ detail: StorybookUtils.stringEnumFormatter(BoSize, 'BoSize'),
+ },
+ },
+ defaultValue: BoSize.default,
+ },
+ variant: {
+ description: 'The variant of the spinner',
+ control: { type: 'select' },
+ options: Object.values(BoLoaderVariant),
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ type: {
+ summary: 'BoLoaderVariant',
+ detail: StorybookUtils.stringEnumFormatter(
+ BoLoaderVariant,
+ 'BoLoaderVariant',
+ ),
+ },
+ },
+ },
+ loaderText: {
+ description: 'The text to be displayed in the loader',
+ control: { type: 'text' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ defaultValue: '',
+ },
+ customColor: {
+ description: 'The custom color of the text',
+ control: { type: 'color' },
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ },
+ },
+ textPosition: {
+ description: 'The position of the text',
+ control: { type: 'select' },
+ options: Object.values(BoLoaderTextPosition),
+ table: {
+ category: 'props',
+ subcategory: 'optional',
+ type: {
+ summary: 'BoLoaderTextPosition',
+ detail: StorybookUtils.stringEnumFormatter(
+ BoLoaderTextPosition,
+ 'BoLoaderTextPosition',
+ ),
+ },
+ },
+ defaultValue: BoLoaderTextPosition.bottom,
+ },
+ },
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Example: Story = {
+ args: {
+ size: BoSize.default,
+ variant: BoLoaderVariant.primary,
+ },
+}
+
+export const Variants: Story = {
+ render: (args) => ({
+ components: { BoLoadingSpinner },
+ setup() {
+ const variants = Object.values(BoLoaderVariant)
+ return { variants, ...args }
+ },
+ template: `
+
+ `,
+ }),
+ args: {
+ text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
+ clickable: true,
},
-};
+}
diff --git a/src/components/bo_text/__tests__/bo_text.test.ts b/src/components/bo_text/__tests__/bo_text.test.ts
new file mode 100644
index 00000000..7e997e09
--- /dev/null
+++ b/src/components/bo_text/__tests__/bo_text.test.ts
@@ -0,0 +1,303 @@
+import {
+ BoFontFamily,
+ BoFontSize,
+ BoFontWeight,
+ BoText,
+ BoTextAlign,
+ BoTextColor,
+ BoTextWhiteSpace,
+} from '@/components/bo_text'
+import { mount } from '@vue/test-utils'
+import { beforeEach, describe, expect, suite, test } from 'vitest'
+
+let wrapper: ReturnType
+
+beforeEach(() => {
+ wrapper = mount(BoText, {
+ props: {
+ id: 'test-id',
+ text: 'Hello World!',
+ size: BoFontSize.base,
+ weight: BoFontWeight.regular,
+ fontFamily: BoFontFamily.sans,
+ whiteSpace: BoTextWhiteSpace.normal,
+ },
+ })
+})
+
+describe('bo_text.vue', () => {
+ test('the text shoudl render properly', () => {
+ expect(wrapper.find('#test-id')).toBeTruthy()
+ })
+
+ test('the text container should have the correct default classes', () => {
+ expect(wrapper.find('#test-id').classes()).toContain('bo-text')
+ expect(wrapper.find('#test-id').classes()).toContain('inline-flex')
+ expect(wrapper.find('#test-id').classes()).toContain('items-center')
+ expect(wrapper.find('#test-id').classes()).toContain('shrink')
+ })
+
+ suite('text sizes', () => {
+ test('the text should have the correct extra small size classes', async () => {
+ await wrapper.setProps({ size: BoFontSize.xs })
+ expect(wrapper.find('#test-id').classes()).toContain('text-xs')
+ })
+
+ test('the text should have the correct small size classes', async () => {
+ await wrapper.setProps({ size: BoFontSize.sm })
+ expect(wrapper.find('#test-id').classes()).toContain('text-sm')
+ })
+
+ test('the text should have the correct base size classes', async () => {
+ await wrapper.setProps({ size: BoFontSize.base })
+ expect(wrapper.find('#test-id').classes()).toContain('text-base')
+ })
+
+ test('the text should have the correct large size classes', async () => {
+ await wrapper.setProps({ size: BoFontSize.lg })
+ expect(wrapper.find('#test-id').classes()).toContain('text-lg')
+ })
+
+ test('the text should have the correct extra large size classes', async () => {
+ await wrapper.setProps({ size: BoFontSize.xl })
+ expect(wrapper.find('#test-id').classes()).toContain('text-xl')
+ })
+
+ test('the text should have the correct 2xl size classes', async () => {
+ await wrapper.setProps({ size: BoFontSize['2xl'] })
+ expect(wrapper.find('#test-id').classes()).toContain('text-2xl')
+ })
+
+ test('the text should have the correct 3xl size classes', async () => {
+ await wrapper.setProps({ size: BoFontSize['3xl'] })
+ expect(wrapper.find('#test-id').classes()).toContain('text-3xl')
+ })
+
+ test('the text should have the correct 4xl size classes', async () => {
+ await wrapper.setProps({ size: BoFontSize['4xl'] })
+ expect(wrapper.find('#test-id').classes()).toContain('text-4xl')
+ })
+
+ test('the text should have the correct 5xl size classes', async () => {
+ await wrapper.setProps({ size: BoFontSize['5xl'] })
+ expect(wrapper.find('#test-id').classes()).toContain('text-5xl')
+ })
+
+ test('the text should have the correct 6xl size classes', async () => {
+ await wrapper.setProps({ size: BoFontSize['6xl'] })
+ expect(wrapper.find('#test-id').classes()).toContain('text-6xl')
+ })
+
+ test('the text should have the correct 7xl size classes', async () => {
+ await wrapper.setProps({ size: BoFontSize['7xl'] })
+ expect(wrapper.find('#test-id').classes()).toContain('text-7xl')
+ })
+
+ test('the text should have the correct 8xl size classes', async () => {
+ await wrapper.setProps({ size: BoFontSize['8xl'] })
+ expect(wrapper.find('#test-id').classes()).toContain('text-8xl')
+ })
+
+ test('the text should have the correct 9xl size classes', async () => {
+ await wrapper.setProps({ size: BoFontSize['9xl'] })
+ expect(wrapper.find('#test-id').classes()).toContain('text-9xl')
+ })
+ })
+
+ suite('text-cursor', () => {
+ test('the text should have the correct default cursor classes', () => {
+ expect(wrapper.find('#test-id').classes()).toContain('cursor-default')
+ })
+
+ test('the text should have the correct pointer cursor classes', async () => {
+ await wrapper.setProps({ clickable: true })
+ expect(wrapper.find('#test-id').classes()).toContain('cursor-pointer')
+ })
+ })
+
+ suite('font-weight', () => {
+ test('the text should have the correct default font weight classes', () => {
+ expect(wrapper.find('#test-id').classes()).toContain('font-normal')
+ })
+
+ test('the text should have the correct light font weight classes', async () => {
+ await wrapper.setProps({ weight: BoFontWeight.light })
+ expect(wrapper.find('#test-id').classes()).toContain('font-light')
+ })
+
+ test('the text should have the correct regular font weight classes', async () => {
+ await wrapper.setProps({ weight: BoFontWeight.regular })
+ expect(wrapper.find('#test-id').classes()).toContain('font-normal')
+ })
+
+ test('the text should have the correct medium font weight classes', async () => {
+ await wrapper.setProps({ weight: BoFontWeight.medium })
+ expect(wrapper.find('#test-id').classes()).toContain('font-medium')
+ })
+
+ test('the text should have the correct semibold font weight classes', async () => {
+ await wrapper.setProps({ weight: BoFontWeight.semibold })
+ expect(wrapper.find('#test-id').classes()).toContain('font-semibold')
+ })
+
+ test('the text should have the correct bold font weight classes', async () => {
+ await wrapper.setProps({ weight: BoFontWeight.bold })
+ expect(wrapper.find('#test-id').classes()).toContain('font-bold')
+ })
+ })
+
+ suite('font-family', () => {
+ test('the text should have the correct default font family classes', () => {
+ expect(wrapper.find('#test-id').classes()).toContain('font-sans')
+ })
+
+ test('the text should have the correct sans font family classes', async () => {
+ await wrapper.setProps({ fontFamily: BoFontFamily.sans })
+ expect(wrapper.find('#test-id').classes()).toContain('font-sans')
+ })
+
+ test('the text should have the correct mono font family classes', async () => {
+ await wrapper.setProps({ fontFamily: BoFontFamily.mono })
+ expect(wrapper.find('#test-id').classes()).toContain('font-mono')
+ })
+
+ test('the text should have the correct serif font family classes', async () => {
+ await wrapper.setProps({ fontFamily: BoFontFamily.serif })
+ expect(wrapper.find('#test-id').classes()).toContain('font-serif')
+ })
+ })
+
+ suite('white-space', () => {
+ test('the text should have the correct default white space classes', () => {
+ expect(wrapper.find('#test-id').classes()).toContain('whitespace-normal')
+ expect(wrapper.find('#test-id').classes()).toContain('text-clip')
+ })
+
+ test('the text should have the correct nowrap white space classes', async () => {
+ await wrapper.setProps({ whiteSpace: BoTextWhiteSpace.nowrap })
+ expect(wrapper.find('#test-id').classes()).toContain('truncate')
+ })
+
+ test('the text should have the correct normal white space classes', async () => {
+ await wrapper.setProps({ whiteSpace: BoTextWhiteSpace.normal })
+ expect(wrapper.find('#test-id').classes()).toContain('whitespace-normal')
+ expect(wrapper.find('#test-id').classes()).toContain('text-clip')
+ })
+
+ test('the text should have the correct pre white space classes', async () => {
+ await wrapper.setProps({ whiteSpace: BoTextWhiteSpace.pre })
+ expect(wrapper.find('#test-id').classes()).toContain('whitespace-pre')
+ expect(wrapper.find('#test-id').classes()).toContain('text-clip')
+ })
+
+ test('the text should have the correct pre-line white space classes', async () => {
+ await wrapper.setProps({ whiteSpace: BoTextWhiteSpace.pre_line })
+ expect(wrapper.find('#test-id').classes()).toContain(
+ 'whitespace-pre-line',
+ )
+ expect(wrapper.find('#test-id').classes()).toContain('text-clip')
+ })
+
+ test('the text should have the correct pre-wrap white space classes', async () => {
+ await wrapper.setProps({ whiteSpace: BoTextWhiteSpace.pre_wrap })
+ expect(wrapper.find('#test-id').classes()).toContain(
+ 'whitespace-pre-wrap',
+ )
+ expect(wrapper.find('#test-id').classes()).toContain('text-clip')
+ })
+
+ test('the text should have the correct break-spaces white space classes', async () => {
+ await wrapper.setProps({ whiteSpace: BoTextWhiteSpace.break_spaces })
+ expect(wrapper.find('#test-id').classes()).toContain('break-spaces')
+ expect(wrapper.find('#test-id').classes()).toContain('text-clip')
+ })
+ })
+
+ suite('text-color', () => {
+ test('the text should have the correct default color classes without text color', () => {
+ expect(wrapper.find('#test-id').classes()).toContain('text-current')
+ })
+
+ test('the text should have the correct default color classes with text color', async () => {
+ await wrapper.setProps({ color: BoTextColor.default })
+ expect(wrapper.find('#test-id').classes()).toContain('text-gray-900')
+ })
+
+ test('the text should have the correct current color classes', async () => {
+ await wrapper.setProps({ color: BoTextColor.current })
+ expect(wrapper.find('#test-id').classes()).toContain('text-current')
+ })
+
+ test('the text should have the correct inherit color classes', async () => {
+ await wrapper.setProps({ color: BoTextColor.inherit })
+ expect(wrapper.find('#test-id').classes()).toContain('text-inherit')
+ })
+
+ test('the text should have the correct success color classes', async () => {
+ await wrapper.setProps({ color: BoTextColor.success })
+ expect(wrapper.find('#test-id').classes()).toContain('text-green-600')
+ })
+
+ test('the text should have the correct warning color classes', async () => {
+ await wrapper.setProps({ color: BoTextColor.warning })
+ expect(wrapper.find('#test-id').classes()).toContain('text-yellow-600')
+ })
+
+ test('the text should have the correct danger color classes', async () => {
+ await wrapper.setProps({ color: BoTextColor.danger })
+ expect(wrapper.find('#test-id').classes()).toContain('text-red-600')
+ })
+
+ test('the text should have the correct secondary color classes', async () => {
+ await wrapper.setProps({ color: BoTextColor.secondary })
+ expect(wrapper.find('#test-id').classes()).toContain('text-gray-500')
+ })
+
+ test('the text should have the correct disabled color classes', async () => {
+ await wrapper.setProps({ color: BoTextColor.disabled })
+ expect(wrapper.find('#test-id').classes()).toContain('text-gray-400')
+ })
+
+ test('the text should have the correct info color classes', async () => {
+ await wrapper.setProps({ color: BoTextColor.info })
+ expect(wrapper.find('#test-id').classes()).toContain('text-blue-600')
+ })
+ })
+
+ suite('text-align', () => {
+ test('the text should have the correct default text align classes', () => {
+ expect(wrapper.find('#test-id').classes()).toContain('text-justify')
+ })
+
+ test('the text should have the correct left text align classes', async () => {
+ await wrapper.setProps({ textAlign: BoTextAlign.left })
+ expect(wrapper.find('#test-id').classes()).toContain('text-left')
+ })
+
+ test('the text should have the correct center text align classes', async () => {
+ await wrapper.setProps({ textAlign: BoTextAlign.center })
+ expect(wrapper.find('#test-id').classes()).toContain('text-center')
+ })
+
+ test('the text should have the correct right text align classes', async () => {
+ await wrapper.setProps({ textAlign: BoTextAlign.right })
+ expect(wrapper.find('#test-id').classes()).toContain('text-right')
+ })
+
+ test('the text should have the correct justify text align classes', async () => {
+ await wrapper.setProps({ textAlign: BoTextAlign.justify })
+ expect(wrapper.find('#test-id').classes()).toContain('text-justify')
+ })
+ })
+
+ suite('custom color style', () => {
+ test('the container should have the correct custom color style', async () => {
+ await wrapper.setProps({ customColor: 'red' })
+
+ const element = wrapper.find('#test-id')
+
+ const style = element.attributes('style')
+ expect(style).toBe('color: red;')
+ })
+ })
+})
diff --git a/src/components/bo_text/bo_text.ts b/src/components/bo_text/bo_text.ts
deleted file mode 100644
index 7c361539..00000000
--- a/src/components/bo_text/bo_text.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-export enum BoFontWeight {
- light = 'light',
- regular = 'regular',
- medium = 'medium',
- bold = 'bold',
-}
-
-export enum BoFontSize {
- overline = 'overline',
- caption = 'caption',
- default = 'default',
- body = 'body',
- h6 = 'h6',
- h5 = 'h5',
- h4 = 'h4',
- h3 = 'h3',
- h2 = 'h2',
- h1 = 'h1',
-}
-
-export enum BoFontSizeClasses {
- overline = /*tw*/ 'text-overline leading-overline',
- caption = /*tw*/ 'text-caption leading-caption',
- default = /*tw*/ 'text-default leading-default',
- body = /*tw*/ 'text-body leading-body',
- h1 = /*tw*/ 'text-h1 leading-h1',
- h2 = /*tw*/ 'text-h2 leading-h2',
- h3 = /*tw*/ 'text-h3 leading-h3',
- h4 = /*tw*/ 'text-h4 leading-h4',
- h5 = /*tw*/ 'text-h5 leading-h5',
- h6 = /*tw*/ 'text-h6 leading-h6',
-}
-
-export enum BoTextFontWeightClasses {
- light = /*tw*/ 'font-light',
- regular = /*tw*/ 'font-normal',
- medium = /*tw*/ 'font-medium',
- bold = /*tw*/ 'font-bold',
-}
diff --git a/src/components/bo_text/constants.ts b/src/components/bo_text/constants.ts
new file mode 100644
index 00000000..7cdd6093
--- /dev/null
+++ b/src/components/bo_text/constants.ts
@@ -0,0 +1,57 @@
+export enum BoFontWeight {
+ light = 'light',
+ regular = 'regular',
+ medium = 'medium',
+ semibold = 'semibold',
+ bold = 'bold',
+}
+
+export enum BoFontSize {
+ xs = 'xs',
+ sm = 'sm',
+ base = 'base',
+ lg = 'lg',
+ xl = 'xl',
+ '2xl' = '2xl',
+ '3xl' = '3xl',
+ '4xl' = '4xl',
+ '5xl' = '5xl',
+ '6xl' = '6xl',
+ '7xl' = '7xl',
+ '8xl' = '8xl',
+ '9xl' = '9xl',
+}
+
+export enum BoFontFamily {
+ sans = 'sans',
+ mono = 'mono',
+ serif = 'serif',
+}
+
+export enum BoTextWhiteSpace {
+ normal = 'normal',
+ nowrap = 'nowrap',
+ pre = 'pre',
+ pre_line = 'pre-line',
+ pre_wrap = 'pre-wrap',
+ break_spaces = 'break-spaces',
+}
+
+export enum BoTextColor {
+ default = 'default',
+ secondary = 'secondary',
+ inherit = 'inherit',
+ current = 'currentColor',
+ success = 'success',
+ warning = 'warning',
+ danger = 'danger',
+ disabled = 'disabled',
+ info = 'info',
+}
+
+export enum BoTextAlign {
+ left = 'left',
+ center = 'center',
+ right = 'right',
+ justify = 'justify',
+}
diff --git a/src/components/bo_text/index.ts b/src/components/bo_text/index.ts
index 1658ffbb..56989dd8 100644
--- a/src/components/bo_text/index.ts
+++ b/src/components/bo_text/index.ts
@@ -1,7 +1,3 @@
-export { default as BoText } from './BoText.vue';
-export {
- BoFontSize,
- BoFontSizeClasses,
- BoFontWeight,
- BoTextFontWeightClasses,
-} from './bo_text';
+export { default as BoText } from './BoText.vue'
+export * from './constants'
+export * from './types'
diff --git a/src/components/bo_text/types.ts b/src/components/bo_text/types.ts
new file mode 100644
index 00000000..52742147
--- /dev/null
+++ b/src/components/bo_text/types.ts
@@ -0,0 +1,22 @@
+import type {
+ BoFontFamily,
+ BoFontSize,
+ BoFontWeight,
+ BoTextAlign,
+ BoTextColor,
+ BoTextWhiteSpace,
+} from './constants'
+
+export type BoTextProps = {
+ id?: string
+ text: string
+ size?: BoFontSize
+ weight?: BoFontWeight
+ fontFamily?: BoFontFamily
+ whiteSpace?: BoTextWhiteSpace
+ color?: BoTextColor
+ customColor?: string
+ cssClass?: string
+ clickable?: boolean
+ textAlign?: BoTextAlign
+}
diff --git a/src/components/index.ts b/src/components/index.ts
index 4aec7fa8..b8925e28 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -1,6 +1,10 @@
-export * from '@/components/bo_badge';
-export * from '@/components/bo_button';
-export * from '@/components/bo_icon';
-export * from '@/components/bo_icon_button';
-export * from '@/components/bo_spinner';
-export * from '@/components/bo_text';
+export * from '@/components/bo_avatar'
+export * from '@/components/bo_badge'
+export * from '@/components/bo_button'
+export * from '@/components/bo_divider'
+export * from '@/components/bo_icon'
+export * from '@/components/bo_input'
+export * from '@/components/bo_loading_pulse'
+export * from '@/components/bo_loading_spinner'
+export * from '@/components/bo_modal'
+export * from '@/components/bo_text'
diff --git a/src/global/bo_color.ts b/src/data/bo_color.constant.ts
similarity index 100%
rename from src/global/bo_color.ts
rename to src/data/bo_color.constant.ts
diff --git a/src/global/bo_size.ts b/src/data/bo_size.constant.ts
similarity index 57%
rename from src/global/bo_size.ts
rename to src/data/bo_size.constant.ts
index 5eb9a3d3..a2711037 100644
--- a/src/global/bo_size.ts
+++ b/src/data/bo_size.constant.ts
@@ -1,5 +1,7 @@
export enum BoSize {
+ extra_small = 'extra-small',
small = 'small',
default = 'default',
large = 'large',
+ extra_large = 'extra-large',
}
diff --git a/src/data/index.ts b/src/data/index.ts
new file mode 100644
index 00000000..ed83804b
--- /dev/null
+++ b/src/data/index.ts
@@ -0,0 +1 @@
+export * from './bo_color.constant'
diff --git a/src/data/loader.constant.ts b/src/data/loader.constant.ts
new file mode 100644
index 00000000..d4133d01
--- /dev/null
+++ b/src/data/loader.constant.ts
@@ -0,0 +1,14 @@
+export enum BoLoaderVariant {
+ primary = 'primary',
+ secondary = 'secondary',
+ danger = 'danger',
+ warning = 'warning',
+ success = 'success',
+ dark = 'dark',
+ white = 'white',
+}
+
+export enum BoLoaderTextPosition {
+ side = 'side',
+ bottom = 'bottom',
+}
diff --git a/src/docs/bamboo.mdx b/src/docs/bamboo.mdx
index 7bb3af5b..7330e1ed 100644
--- a/src/docs/bamboo.mdx
+++ b/src/docs/bamboo.mdx
@@ -1,70 +1,6 @@
-import Readme from '../../README.md?raw';
-import { Meta, Markdown } from '@storybook/blocks';
+import Readme from '../../README.md?raw'
+import { Meta, Markdown } from '@storybook/blocks'
-Lightweight and flexible (hence the name) UI Library built with [Vite](https://vitejs.dev/) and [Vue 3](https://vuejs.org/). The documentation is created with [Storybook](https://storybook.js.org/).
-
-### Project setup
-
-You need to have node and npm installed on your machine as a prerequisite. You can download and install them from [here](https://nodejs.org/en/).
-
-### Install dependencies
-
-To install the dependencies, run the following command:
-
-```bash
-npm install
-```
-
-### Run documentation
-
-To start the development server, run the following command:
-
-```bash
-npm run storybook
-```
-
-This will start the Storybook server and open the browser at [http://localhost:6006](http://localhost:6006).
-Here you can see the documentation for the components and play around with them.
-
-### Build for production
-
-To build the components for production, run the following command:
-
-```bash
-npm run build
-```
-
-This will build the components and generate the static files in the `dist` folder.
-
-### Build storybook for production
-
-To build the Storybook documentation for production, run the following command:
-
-```bash
-npm run build-storybook
-```
-
-This will build the Storybook documentation and generate the static files in the `static` folder.
-GitHub Pages uses the directory to host the documentation.
-
-### Run your unit tests
-
-The project uses [Vitest](https://vitest.dev) for unit and snapshot testing. To run the tests, run the following command:
-
-```bash
-npm run test
-```
-
-### Lints and fixes files
-
-The project uses [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/) for linting and formatting. To run the linter, run the following command:
-
-```bash
-npm run lint
-```
-
-### License
-
-[MIT](../../LICENSE)
+{Readme}
diff --git a/src/docs/colors.mdx b/src/docs/colors.mdx
index 5cdfb15d..0acbf52e 100644
--- a/src/docs/colors.mdx
+++ b/src/docs/colors.mdx
@@ -1,523 +1,523 @@
-import { Meta, ColorPalette, ColorItem } from '@storybook/blocks';
+import { Meta, ColorPalette, ColorItem } from '@storybook/blocks'
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/docs/license/bamboo_license.mdx b/src/docs/license/bamboo_license.mdx
index 0e0d841d..f97f031a 100644
--- a/src/docs/license/bamboo_license.mdx
+++ b/src/docs/license/bamboo_license.mdx
@@ -1,5 +1,5 @@
-import License from '../../../LICENSE?raw';
-import { Meta, Markdown } from '@storybook/blocks';
+import License from '../../../LICENSE?raw'
+import { Meta, Markdown } from '@storybook/blocks'
diff --git a/src/docs/license/feather_license.mdx b/src/docs/license/feather_license.mdx
deleted file mode 100644
index ddf17b03..00000000
--- a/src/docs/license/feather_license.mdx
+++ /dev/null
@@ -1,6 +0,0 @@
-import License from '../../../vendor/feather_license.md?raw';
-import { Meta, Markdown } from '@storybook/blocks';
-
-
-
-{License}
diff --git a/src/global/html_button.ts b/src/global/html_button.ts
new file mode 100644
index 00000000..8d98aa63
--- /dev/null
+++ b/src/global/html_button.ts
@@ -0,0 +1,5 @@
+export enum HtmlButtonType {
+ button = 'button',
+ submit = 'submit',
+ reset = 'reset',
+}
diff --git a/src/global/html_input.ts b/src/global/html_input.ts
new file mode 100644
index 00000000..814fed23
--- /dev/null
+++ b/src/global/html_input.ts
@@ -0,0 +1,24 @@
+export enum HtmlInputType {
+ button = 'button',
+ checkbox = 'checkbox',
+ color = 'color',
+ date = 'date',
+ datetime_local = 'datetime-local',
+ email = 'email',
+ file = 'file',
+ hidden = 'hidden',
+ image = 'image',
+ month = 'month',
+ number = 'number',
+ password = 'password',
+ radio = 'radio',
+ range = 'range',
+ reset = 'reset',
+ search = 'search',
+ submit = 'submit',
+ tel = 'tel',
+ text = 'text',
+ time = 'time',
+ url = 'url',
+ week = 'week',
+}
diff --git a/src/global/index.ts b/src/global/index.ts
index 8e9d494b..289229ec 100644
--- a/src/global/index.ts
+++ b/src/global/index.ts
@@ -1,2 +1,2 @@
-export { BoColor } from './bo_color';
-export { BoSize } from './bo_size';
+export * from './html_button'
+export * from './html_input'
diff --git a/src/index.ts b/src/index.ts
index e56abfc9..71215f08 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,4 @@
-export * from './components';
-export * from './global';
-export * from './utils';
+export * from './components'
+export * from './data'
+export * from './global'
+export * from './utils'
diff --git a/src/index.css b/src/lib.css
similarity index 100%
rename from src/index.css
rename to src/lib.css
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 00000000..57fe34e6
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1 @@
+export * from './style'
diff --git a/src/types/style.ts b/src/types/style.ts
new file mode 100644
index 00000000..3a504f04
--- /dev/null
+++ b/src/types/style.ts
@@ -0,0 +1,6 @@
+import type { StyleValue } from 'vue'
+
+export type StyleConstruct = {
+ style: StyleValue
+ class: string
+}
diff --git a/src/utils/__stories__/identity_utils.mdx b/src/utils/__stories__/identity_utils.mdx
new file mode 100644
index 00000000..8d022765
--- /dev/null
+++ b/src/utils/__stories__/identity_utils.mdx
@@ -0,0 +1,43 @@
+import { Meta, Story } from '@storybook/blocks'
+import { IdentityUtils } from '@/utils'
+
+
+
+# IdentityUtils
+
+The `IdentityUtils` class is a utility class that provides a collection of
+`static` methods to make identity management easier.
+
+## Methods
+
+## generateRandomId
+
+```ts
+generateRandomId(): string
+```
+
+The `generateRandomId` method is used to generate a new random identity.
+
+- Returns: A random id as a `string`.
+
+```ts
+IdentityUtils.instance.generateRandomId() // "lqjxigk"
+```
+
+## generateRandomIdWithPrefix
+
+```ts
+generateRandomIdWithPrefix(prefix: string): string
+```
+
+The `generateRandomIdWithPrefix` method is used to generate a new random identity with the specified prefix.
+
+- Parameters:
+
+ - `prefix`: The prefix to be added to the random id.
+
+- Returns: A random id with the specified prefix as a `string`.
+
+```ts
+IdentityUtils.instance.generateRandomIdWithPrefix('my-prefix') // "my-prefix-lqjxigk"
+```
diff --git a/src/utils/__stories__/keyboard_utils.mdx b/src/utils/__stories__/keyboard_utils.mdx
new file mode 100644
index 00000000..69035c44
--- /dev/null
+++ b/src/utils/__stories__/keyboard_utils.mdx
@@ -0,0 +1,60 @@
+import { Meta, Story } from '@storybook/blocks'
+import { KeyboardUtils } from '@/utils'
+
+
+
+# KeyboardUtils
+
+The `KeyboardUtils` class is a utility class that provides a collection of
+`static` methods to support keyboard related interactions.
+
+## Methods
+
+## trapTabKey
+
+```ts
+trapTabKey(e: KeyboardEvent, id: string): void
+```
+
+The `trapTabKey` method is used to trap the tab key within an element with the specified id.
+
+- Parameters:
+
+ - `e`: The `KeyboardEvent` object.
+ - `id`: The id of the element where the tab key should be trapped.
+
+- Returns: `void`
+
+```html
+
+
+
+```
+
+## registerEnterKeyHandler
+
+```ts
+registerEnterKeyHandler(e: KeyboardEvent, handler: () => void): void
+```
+
+The `registerEnterKeyHandler` method is used to trigger a handler when the enter key is pressed
+without the need to implement the required checks manually.
+
+- Parameters:
+
+ - `e`: The `KeyboardEvent` object.
+ - `handler`: The handler function that will be called when the enter key is pressed.
+
+- Returns: `void`
+
+```html
+
+
+
+```
diff --git a/src/utils/__stories__/storybook_utils.mdx b/src/utils/__stories__/storybook_utils.mdx
new file mode 100644
index 00000000..64e9249f
--- /dev/null
+++ b/src/utils/__stories__/storybook_utils.mdx
@@ -0,0 +1,65 @@
+import { Meta, Story } from '@storybook/blocks'
+import { StorybookUtils } from '@/utils'
+
+
+
+# StorybookUtils
+
+The `StorybookUtils` class is a utility class that provides a collection of
+`static` methods to make Storybook related tasks easier.
+
+## Methods
+
+## stringEnumFormatter
+
+```ts
+stringEnumFormatter(stringEnum: { [key: string]: string }, name: string): string
+```
+
+The `stringEnumFormatter` method is used to generate a string representation of an enum in
+a readable format.
+
+- Parameters:
+
+ - `stringEnum`: The enum to be formatted.
+ - `name`: The name of the enum.
+
+- Returns: The formatted `string`.
+
+```ts
+StorybookUtils.stringEnumFormatter(BoColor, 'BoColor')
+/*
+ * enum BoColor {
+ * inherit = 'inherit',
+ * current = 'currentColor',
+ * transparent = 'transparent',
+ * black = '#000',
+ * ...
+ * }
+ */
+```
+
+## arrayFormatter
+
+```ts
+arrayFormatter(array: string[], name: string): string
+```
+
+The `arrayFormatter` method is used to generate a string representation of an array in
+a readable format.
+
+- Parameters:
+
+ - `array`: The array to be formatted.
+ - `name`: The name of the array.
+
+- Returns: The formatted `string`.
+
+```ts
+StorybookUtils.arrayFormatter(['hello', 'world'], 'array');
+/*
+ * array [
+ * 'hello',
+ * 'world',
+ * ]
+```
diff --git a/src/utils/__stories__/string_utils.mdx b/src/utils/__stories__/string_utils.mdx
new file mode 100644
index 00000000..cff21ec3
--- /dev/null
+++ b/src/utils/__stories__/string_utils.mdx
@@ -0,0 +1,67 @@
+import { Meta, Story } from '@storybook/blocks'
+import { StringUtils } from '@/utils'
+
+
+
+# StringUtils
+
+The `StringUtils` class is a utility class that provides a collection of
+`static` methods to make `string` checking and manipulation easier.
+
+## Methods
+
+## capitalize
+
+```ts
+capitalize(str: string): string
+```
+
+The `capitalize` method is used to capitalize the first letter of a string.
+
+- Parameters:
+
+ - `str`: The string to be capitalized.
+
+- Returns: The capitalized `string`.
+
+```ts
+StringUtils.capitalize('hello world') // Hello world
+```
+
+## camelCaseToTitleCase
+
+```ts
+camelCaseToTitleCase(str: string): string
+```
+
+The `camelCaseToTitleCase` method is used to convert a string from camel case to title case.
+
+- Parameters:
+
+ - `str`: The string to be converted.
+
+- Returns: The converted `string`.
+
+```ts
+StringUtils.camelCaseToTitleCase('helloWorld') // Hello World
+```
+
+## isEmptyStr
+
+```ts
+isEmptyStr(str: unknown): boolean
+```
+
+The `isEmptyStr` method is used to check if a `string` is `empty`, `null`, or `undefined`.
+
+- Parameters:
+
+ - `str`: The string to be checked.
+
+ - Returns: A `boolean` value indicating the result of the check.
+
+```ts
+StringUtils.isEmptyStr('') // true
+StringUtils.isEmptyStr('hello world') // false
+StringUtils.isEmptyStr(null) // true
+```
diff --git a/src/utils/__stories__/tailwind_utils.mdx b/src/utils/__stories__/tailwind_utils.mdx
new file mode 100644
index 00000000..ce8d9140
--- /dev/null
+++ b/src/utils/__stories__/tailwind_utils.mdx
@@ -0,0 +1,37 @@
+import { Meta, Story } from '@storybook/blocks'
+import { TailwindUtils } from '@/utils'
+
+
+
+# TailwindUtils
+
+The `TailwindUtils` class is a utility class that provides a collection of
+`static` methods to make working with Tailwind CSS easier.
+
+## Methods
+
+## merge
+
+```ts
+merge(...classes: (string | null | undefined)[]): string
+```
+
+The `merge` method is used to merge multiple classes into a single `string` that can later
+be applied to an element.
+If any of the argument values equals to `null` or `undefined` the corresponding argument will be ignored.
+
+- Example:
+
+```ts
+TailwindUtils.merge('bg-blue-500', 'text-white', 'rounded-lg') // bg-blue-500 text-white rounded-lg
+```
+
+- Parameters:
+
+ - `classes`: An array of classes to be merged.
+
+- Returns: The merged classes as a `string`.
+
+```ts
+TailwindUtils.merge('bg-blue-500', null, 'text-white', 'rounded-lg') // bg-blue-500 text-white rounded-lg
+```
diff --git a/src/utils/__test__/identity_utils.test.ts b/src/utils/__test__/identity_utils.test.ts
new file mode 100644
index 00000000..a1c6c2bd
--- /dev/null
+++ b/src/utils/__test__/identity_utils.test.ts
@@ -0,0 +1,31 @@
+import { describe, expect, suite, test } from 'vitest'
+import { IdentityUtils } from '../identity_utils'
+
+describe('IdentityUtils', () => {
+ suite('generateRandomId', () => {
+ test('should generate a random id', () => {
+ const id = IdentityUtils.generateRandomId()
+ expect(id).toBeTruthy()
+ expect(id.length).toBeGreaterThan(0)
+ })
+
+ test('the returned id should be a string', () => {
+ const id = IdentityUtils.generateRandomId()
+ expect(typeof id).toBe('string')
+ })
+ })
+
+ suite('generateRandomIdWithPrefix', () => {
+ test('should generate a random id with a prefix', () => {
+ const id = IdentityUtils.generateRandomIdWithPrefix('test')
+ expect(id).toBeTruthy()
+ expect(id.length).toBeGreaterThan(0)
+ expect(id).toContain('test')
+ })
+
+ test('the returned id should be a string', () => {
+ const id = IdentityUtils.generateRandomIdWithPrefix('test')
+ expect(typeof id).toBe('string')
+ })
+ })
+})
diff --git a/src/utils/__test__/keyboard_utils.test.ts b/src/utils/__test__/keyboard_utils.test.ts
new file mode 100644
index 00000000..4ea67fc1
--- /dev/null
+++ b/src/utils/__test__/keyboard_utils.test.ts
@@ -0,0 +1,24 @@
+import { KeyboardUtils } from '@/utils'
+import { describe, expect, suite, test, vi } from 'vitest'
+
+describe('KeyboardUtils', () => {
+ suite('registerEnterKeyHandler', () => {
+ test('should register a handler for the enter key', () => {
+ const handler = vi.fn()
+ KeyboardUtils.registerEnterKeyHandler(
+ new KeyboardEvent('keydown', { key: 'Enter' }),
+ handler,
+ )
+ expect(handler).toHaveBeenCalled()
+ })
+
+ test('should not register a handler for a non-enter key', () => {
+ const handler = vi.fn()
+ KeyboardUtils.registerEnterKeyHandler(
+ new KeyboardEvent('keydown', { key: 'Tab' }),
+ handler,
+ )
+ expect(handler).not.toHaveBeenCalled()
+ })
+ })
+})
diff --git a/src/utils/__test__/storybook_utils.test.ts b/src/utils/__test__/storybook_utils.test.ts
new file mode 100644
index 00000000..2cf8460a
--- /dev/null
+++ b/src/utils/__test__/storybook_utils.test.ts
@@ -0,0 +1,51 @@
+import { describe, expect, suite, test } from 'vitest'
+import { StorybookUtils } from '../storybook_utils'
+
+const control = (stringEnum: { [key: string]: string }, name: string) => {
+ return `enum ${name} {\n${
+ Object.keys(stringEnum)
+ .map(
+ (m: string) =>
+ ` ${m} = "${stringEnum[m as keyof typeof stringEnum]}",`,
+ )
+ .join('\r\n') + '\r\n}'
+ }`
+}
+
+const controlArray = (array: string[], name: string) => {
+ return `array ${name} {\n${array.map((m: string) => ` ${m},`).join('\r\n')}\r\n}`
+}
+
+describe('StorybookUtils', () => {
+ suite('stringEnumFormatter', () => {
+ test('should format a string enum', () => {
+ const enumObj = {
+ hello: 'Hello',
+ world: 'World',
+ }
+ const formattedEnum = StorybookUtils.stringEnumFormatter(
+ enumObj,
+ 'string',
+ )
+
+ const compareValue = control(enumObj, 'string')
+
+ expect(formattedEnum).toBeTruthy()
+ expect(typeof formattedEnum).toBe('string')
+ expect(formattedEnum).toBe(compareValue)
+ })
+ })
+
+ suite('arrayFormatter', () => {
+ test('should format an array', () => {
+ const array = ['hello', 'world']
+ const formattedArray = StorybookUtils.arrayFormatter(array, 'array')
+
+ const compareValue = controlArray(array, 'array')
+
+ expect(formattedArray).toBeTruthy()
+ expect(typeof formattedArray).toBe('string')
+ expect(formattedArray).toBe(compareValue)
+ })
+ })
+})
diff --git a/src/utils/__test__/string_utils.test.ts b/src/utils/__test__/string_utils.test.ts
new file mode 100644
index 00000000..70d03bcc
--- /dev/null
+++ b/src/utils/__test__/string_utils.test.ts
@@ -0,0 +1,74 @@
+import { describe, expect, suite, test } from 'vitest'
+import { StringUtils } from '../string_utils'
+
+describe('StringUtils', () => {
+ suite('capitalize', () => {
+ test('should capitalize a string', () => {
+ const str = 'hello world'
+ const capitalizedStr = StringUtils.capitalize(str)
+ expect(capitalizedStr).toBe('Hello world')
+ })
+
+ test('should return the original string if it is already capitalized', () => {
+ const str = 'Hello world'
+ const capitalizedStr = StringUtils.capitalize(str)
+ expect(capitalizedStr).toBe('Hello world')
+ })
+
+ test('the returned value should be a string', () => {
+ const str = 'hello world'
+ const capitalizedStr = StringUtils.capitalize(str)
+ expect(typeof capitalizedStr).toBe('string')
+ })
+ })
+
+ suite('camelCaseToTitleCase', () => {
+ test('should convert a camel case string to title case', () => {
+ const str = 'helloWorld'
+ const titleCaseStr = StringUtils.camelCaseToTitleCase(str)
+ expect(titleCaseStr).toBe('Hello World')
+ })
+
+ test('should return the original string if it is already in title case', () => {
+ const str = 'Hello World'
+ const titleCaseStr = StringUtils.camelCaseToTitleCase(str)
+ expect(titleCaseStr).toBe('Hello World')
+ })
+
+ test('the returned value should be a string', () => {
+ const str = 'helloWorld'
+ const titleCaseStr = StringUtils.camelCaseToTitleCase(str)
+ expect(typeof titleCaseStr).toBe('string')
+ })
+ })
+
+ suite('isEmptyStr', () => {
+ test('should return true for an empty string', () => {
+ const str = ''
+ const isEmpty = StringUtils.isEmptyStr(str)
+ expect(isEmpty).toBeTruthy()
+ })
+
+ test('should return true for a string with only whitespace', () => {
+ const str = ' '
+ const isEmpty = StringUtils.isEmptyStr(str)
+ expect(isEmpty).toBeTruthy()
+ })
+
+ test('should return false for a non-empty string', () => {
+ const str = 'hello world'
+ const isEmpty = StringUtils.isEmptyStr(str)
+ expect(isEmpty).toBeFalsy()
+ })
+
+ test('should return true for a null or undefined value', () => {
+ const str = null
+ const isEmpty = StringUtils.isEmptyStr(str)
+ expect(isEmpty).toBeTruthy()
+
+ const str2 = undefined
+ const isEmpty2 = StringUtils.isEmptyStr(str2)
+ expect(isEmpty2).toBeTruthy()
+ })
+ })
+})
diff --git a/src/utils/__test__/tailwind_utils.test.ts b/src/utils/__test__/tailwind_utils.test.ts
new file mode 100644
index 00000000..c0ae8618
--- /dev/null
+++ b/src/utils/__test__/tailwind_utils.test.ts
@@ -0,0 +1,39 @@
+import { describe, expect, suite, test } from 'vitest'
+import { TailwindUtils } from '../tailwind_utils'
+
+describe('TailwindUtils', () => {
+ suite('merge', () => {
+ test('should merge two classes', () => {
+ const class1 = 'bg-blue-500'
+ const class2 = 'text-white'
+ const mergedClass = TailwindUtils.merge(class1, class2)
+ expect(mergedClass).toBe('bg-blue-500 text-white')
+ })
+
+ test('should merge multiple classes', () => {
+ const class1 = 'bg-blue-500'
+ const class2 = 'text-white'
+ const class3 = 'rounded-lg'
+ const mergedClass = TailwindUtils.merge(class1, class2, class3)
+ expect(mergedClass).toBe('bg-blue-500 text-white rounded-lg')
+ })
+
+ test('should merge classes with null or undefined values', () => {
+ const class1 = 'bg-blue-500'
+ const class2 = 'text-white'
+ const class3 = null
+ const class4 = undefined
+ const mergedClass = TailwindUtils.merge(class1, class2, class3, class4)
+ expect(mergedClass).toBe('bg-blue-500 text-white')
+ })
+
+ test('the returned value should only contain unique classes', () => {
+ const class1 = 'bg-blue-500'
+ const class2 = 'text-white'
+ const class3 = 'rounded-lg'
+ const class4 = 'bg-blue-500'
+ const mergedClass = TailwindUtils.merge(class1, class2, class3, class4)
+ expect(mergedClass).toBe('bg-blue-500 text-white rounded-lg')
+ })
+ })
+})
diff --git a/src/utils/identity_utils.ts b/src/utils/identity_utils.ts
new file mode 100644
index 00000000..be4ede12
--- /dev/null
+++ b/src/utils/identity_utils.ts
@@ -0,0 +1,22 @@
+export class IdentityUtils {
+ /**
+ * @description Generated a new random id.
+ *
+ * @returns A random id.
+ */
+ static generateRandomId(): string {
+ return Math.random()
+ .toString(36)
+ .replace(/[^a-z]+/g, '')
+ .slice(0, 10)
+ }
+ /**
+ * @description Generated a new random id with the specified prefix.
+ *
+ * @param prefix - The prefix to be added to the random id.
+ * @returns A random id with the specified prefix.
+ */
+ static generateRandomIdWithPrefix(prefix: string): string {
+ return [prefix, IdentityUtils.generateRandomId()].join('-')
+ }
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 87aa0fc4..14e47f4a 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,3 +1,5 @@
-export { StorybookUtils } from './storybook_utils';
-export { StringUtils } from './string_utils';
-export { TailwindUtils } from './tailwind_utils';
+export { IdentityUtils } from './identity_utils'
+export { KeyboardUtils } from './keyboard_utils'
+export { StorybookUtils } from './storybook_utils'
+export { StringUtils } from './string_utils'
+export { TailwindUtils } from './tailwind_utils'
diff --git a/src/utils/keyboard_utils.ts b/src/utils/keyboard_utils.ts
new file mode 100644
index 00000000..1d7a716d
--- /dev/null
+++ b/src/utils/keyboard_utils.ts
@@ -0,0 +1,53 @@
+export class KeyboardUtils {
+ static trapTabKey(e: KeyboardEvent, id: string): void {
+ const isTab = e.key === 'Tab' || e.keyCode === 9
+
+ if (isTab) {
+ const focusable: NodeListOf | undefined = document
+ ?.querySelector(`#${id}`)
+ ?.querySelectorAll('[tabindex]')
+
+ if (!focusable?.length) {
+ return
+ }
+
+ const sorted = [...focusable].sort((a, b) => {
+ return (
+ parseInt(a.getAttribute('tabindex') ?? '0') -
+ parseInt(b.getAttribute('tabindex') ?? '0')
+ )
+ })
+
+ const shift = e.shiftKey
+ const first = sorted[0]
+ const last = sorted[sorted.length - 1]
+
+ const firstHtmlElement = first as HTMLElement
+ const lastHtmlElement = last as HTMLElement
+
+ if (shift) {
+ if (e.target === first) {
+ lastHtmlElement.focus()
+ e.preventDefault()
+ }
+ } else {
+ if (e.target === last) {
+ firstHtmlElement.focus()
+ e.preventDefault()
+ }
+ }
+ }
+ }
+
+ static registerEnterKeyHandler(e: KeyboardEvent, handler: () => void): void {
+ e.preventDefault()
+
+ const isEnter = e.key === 'Enter' || e.keyCode === 13
+
+ if (!isEnter) {
+ return
+ }
+
+ handler()
+ }
+}
diff --git a/src/utils/storybook_utils.ts b/src/utils/storybook_utils.ts
index d3490eaa..24937ff6 100644
--- a/src/utils/storybook_utils.ts
+++ b/src/utils/storybook_utils.ts
@@ -1,4 +1,9 @@
export class StorybookUtils {
+ /**
+ * @description
+ * This method generates a string representation of an enum in
+ * a readable format.
+ */
static stringEnumFormatter(
stringEnum: { [key: string]: string },
name: string,
@@ -10,10 +15,14 @@ export class StorybookUtils {
` ${m} = "${stringEnum[m as keyof typeof stringEnum]}",`,
)
.join('\r\n') + '\r\n}'
- }`;
+ }`
}
-
+ /**
+ * @description
+ * This method generates a string representation of an array in
+ * a readable format.
+ */
static arrayFormatter(array: string[], name: string): string {
- return `array ${name} {\n${array.map((m: string) => ` ${m},`).join('\r\n')}\r\n}`;
+ return `array ${name} {\n${array.map((m: string) => ` ${m},`).join('\r\n')}\r\n}`
}
}
diff --git a/src/utils/string_utils.ts b/src/utils/string_utils.ts
index c548bc4d..93503af5 100644
--- a/src/utils/string_utils.ts
+++ b/src/utils/string_utils.ts
@@ -1,15 +1,53 @@
export class StringUtils {
+ /**
+ * @description
+ * This method capitalizes the first letter of a string.
+ *
+ * @example
+ * capitalize('hello world'); // Hello world
+ */
static capitalize(str: string): string {
- return str.charAt(0).toUpperCase() + str.slice(1);
+ return str.charAt(0).toUpperCase() + str.slice(1)
}
-
+ /**
+ * @description
+ * This method converts a string to title case.
+ *
+ * @example
+ * camelCaseToTitleCase('helloWorld'); // Hello World
+ */
static camelCaseToTitleCase(str: string): string {
- return str.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase());
+ return str
+ .replace(/([A-Z])/g, ' $1')
+ .replace(/^./, (s) => s.toUpperCase())
+ .trim()
}
-
- static isEmpty(str: string): boolean {
+ /**
+ * @description
+ * This method checks if a string is empty, null, or undefined.
+ *
+ * @example
+ * isEmptyStr(''); // true
+ * isEmptyStr('hello world'); // false
+ * isEmptyStr(null); // true
+ */
+ static isEmptyStr(str: unknown): boolean {
return (
- typeof str === 'string' && str.length === 0 && str.trim().length === 0
- );
+ (typeof str === 'string' && str.trim().length === 0) ||
+ str === null ||
+ str === undefined
+ )
+ }
+ /**
+ * @description
+ * This method returns an empty string if the input is empty or null, otherwise it returns the input as a string.
+ *
+ * @example
+ * safeString(''); // ''
+ * safeString('hello world'); // 'hello world'
+ * safeString(null); // ''
+ */
+ static safeString(str: unknown): string {
+ return StringUtils.isEmptyStr(str) ? '' : String(str)
}
}
diff --git a/src/utils/tailwind_utils.ts b/src/utils/tailwind_utils.ts
index f908c245..db8a16db 100644
--- a/src/utils/tailwind_utils.ts
+++ b/src/utils/tailwind_utils.ts
@@ -1,14 +1,26 @@
export class TailwindUtils {
+ /**
+ * @description
+ * This method merges multiple classes into a single class that can later
+ * directly be applied to an element.
+ *
+ * @example
+ * merge('bg-blue-500', 'text-white', 'rounded-lg'); // bg-blue-500 text-white rounded-lg
+ *
+ * In case a null or undefined value is passed, it will be ignored.
+ *
+ * merge('bg-blue-500', null, 'text-white', 'rounded-lg'); // bg-blue-500 text-white rounded-lg
+ */
static merge(...classes: (string | null | undefined)[]): string {
return Array.from(
- new Set([
- ...classes
- .filter(Boolean)
+ new Set(
+ classes
+ .filter((f) => f !== null && f !== undefined)
.map((c) => {
- return c?.split(' ');
+ return c.split(' ')
})
.flat(),
- ]),
- ).join(' ');
+ ),
+ ).join(' ')
}
}
diff --git a/tailwind.config.js b/tailwind.config.js
deleted file mode 100644
index c715d8d9..00000000
--- a/tailwind.config.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- darkMode: 'class',
- content: ['./src/**/*.{vue,ts}'],
- theme: {
- extend: {
- width: {
- small: '12px',
- default: '14px',
- large: '18px',
- },
- height: {
- small: '12px',
- default: '14px',
- large: '18px',
- },
- fontSize: {
- overline: '10px',
- caption: '12px',
- default: '14px',
- body: '16px',
- h6: '20px',
- h5: '24px',
- h4: '34px',
- h3: '48px',
- h2: '60px',
- h1: '96px',
- },
- lineHeight: {
- overline: '12px',
- caption: '16px',
- default: '20px',
- body: '24px',
- h6: '32px',
- h5: '40px',
- h4: '48px',
- h3: '56px',
- h2: '64px',
- h1: '96px',
- },
- },
- },
- plugins: [['postcss-import', {}], 'tailwindcss', ['postcss-preset-env', {}]],
-};
diff --git a/tailwind.config.mjs b/tailwind.config.mjs
new file mode 100644
index 00000000..8e97f927
--- /dev/null
+++ b/tailwind.config.mjs
@@ -0,0 +1,8 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,ts,vue}'],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
diff --git a/tsconfig.app.json b/tsconfig.app.json
new file mode 100644
index 00000000..913b8f27
--- /dev/null
+++ b/tsconfig.app.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
+ "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
+ "exclude": ["src/**/__tests__/*"],
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
index f2a1c535..100cf6a8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,18 +1,14 @@
{
- "extends": "@vue/tsconfig/tsconfig.dom.json",
- "compilerOptions": {
- "allowJs": true,
- "target": "ESNext",
- "module": "ESNext",
- "alwaysStrict": true,
- "lib": ["ESNext", "DOM"],
- "types": ["node", "jsdom"],
- "moduleResolution": "Bundler",
- "baseUrl": ".",
- "paths": {
- "@/*": ["./src/*"]
- }
- },
- "include": ["env.d.ts", "src/**/*.ts", "src/**/*.vue", "*.config.*"],
- "exclude": ["node_modules", "dist", "docs"]
+ "files": [],
+ "references": [
+ {
+ "path": "./tsconfig.node.json"
+ },
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.vitest.json"
+ }
+ ]
}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 00000000..4c399c2c
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,18 @@
+{
+ "extends": "@tsconfig/node22/tsconfig.json",
+ "include": [
+ "vite.config.*",
+ "vitest.config.*",
+ "cypress.config.*",
+ "nightwatch.conf.*",
+ "playwright.config.*"
+ ],
+ "compilerOptions": {
+ "noEmit": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "types": ["node"]
+ }
+}
diff --git a/tsconfig.vitest.json b/tsconfig.vitest.json
new file mode 100644
index 00000000..7d1d8cef
--- /dev/null
+++ b/tsconfig.vitest.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.app.json",
+ "include": ["src/**/__tests__/*", "env.d.ts"],
+ "exclude": [],
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
+
+ "lib": [],
+ "types": ["node", "jsdom"]
+ }
+}
diff --git a/vendor/feather_license.md b/vendor/feather_license.md
deleted file mode 100644
index 1f4f4336..00000000
--- a/vendor/feather_license.md
+++ /dev/null
@@ -1,21 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2013-2023 Cole Bemis
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/vite.config.ts b/vite.config.ts
index d39c43d9..9e38d63b 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,45 +1,26 @@
-import vue from '@vitejs/plugin-vue';
-import { fileURLToPath, URL } from 'node:url';
-import viteCompression from 'vite-plugin-compression';
-import dts from 'vite-plugin-dts';
-import svgLoader from 'vite-svg-loader';
-import { defineConfig, type UserConfig } from 'vitest/config';
+import vue from '@vitejs/plugin-vue'
+import { fileURLToPath, URL } from 'node:url'
+import { defineConfig } from 'vite'
+import dts from 'vite-plugin-dts'
+import vueDevTools from 'vite-plugin-vue-devtools'
+import svgLoader from 'vite-svg-loader'
-/**
- * @see https://vitejs.dev/config/
- */
-const bambooLibConfig: UserConfig = defineConfig({
+// https://vite.dev/config/
+export default defineConfig({
plugins: [
vue(),
- dts(),
+ dts({
+ rollupTypes: true,
+ tsconfigPath: './tsconfig.app.json',
+ }),
svgLoader(),
- viteCompression({ algorithm: 'gzip', ext: '.gz' }),
- viteCompression({ algorithm: 'brotliCompress', ext: '.br' }),
+ vueDevTools(),
],
build: {
- minify: true,
- emptyOutDir: true,
- cssMinify: true,
- cssCodeSplit: true,
lib: {
+ entry: fileURLToPath(new URL('./src/index.ts', import.meta.url)),
name: 'bamboo',
- formats: ['es', 'cjs'],
- entry: ['src/index.ts'],
- fileName: (format, entry) => {
- if (entry === 'main') return `index.${format}.js`;
- return `${entry}.${format}.js`;
- },
- },
- rollupOptions: {
- treeshake: true,
- external: ['vue'],
- output: {
- exports: 'named',
- globals: { vue: 'Vue' },
- },
- input: {
- main: fileURLToPath(new URL('./src/index.ts', import.meta.url)),
- },
+ fileName: 'bamboo',
},
},
resolve: {
@@ -47,6 +28,4 @@ const bambooLibConfig: UserConfig = defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
-});
-
-export default bambooLibConfig;
+})
diff --git a/vitest.config.ts b/vitest.config.ts
index fccdd304..2b69a8c5 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,21 +1,14 @@
-import { configDefaults, defineConfig, mergeConfig } from 'vitest/config';
-import viteConfig from './vite.config';
+import { fileURLToPath } from 'node:url'
+import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
+import viteConfig from './vite.config'
-const bambooTestConfig = mergeConfig(
+export default mergeConfig(
viteConfig,
defineConfig({
test: {
- alias: {
- '@/': new URL('./src/', import.meta.url).pathname,
- },
- coverage: {
- provider: 'v8',
- reporter: ['text', 'json-summary', 'json'],
- },
environment: 'jsdom',
- exclude: [...configDefaults.exclude],
+ exclude: [...configDefaults.exclude, 'e2e/**'],
+ root: fileURLToPath(new URL('./', import.meta.url)),
},
}),
-);
-
-export default bambooTestConfig;
+)