88 |
89 | `,
90 | }),
91 | }
92 |
--------------------------------------------------------------------------------
/.storybook/withSource.js:
--------------------------------------------------------------------------------
1 | import kebabCase from 'lodash.kebabcase';
2 | import { addons, makeDecorator } from '@storybook/addons';
3 | import * as prettier from 'prettier/standalone';
4 | import * as prettierHtml from 'prettier/parser-html';
5 |
6 | import { h, onMounted } from 'vue';
7 | import dedent from 'ts-dedent';
8 |
9 | export function templateSourceCode(
10 | templateSource,
11 | args,
12 | argTypes,
13 | replacing = 'v-bind="args"'
14 | ) {
15 | const componentArgs = {};
16 | for (const [k, t] of Object.entries(argTypes)) {
17 | const val = args[k];
18 | if (typeof val !== 'undefined' && t.table && t.table.category === 'props') {
19 | componentArgs[k] = val;
20 | }
21 | }
22 |
23 | const propToSource = (key, val) => {
24 | const type = typeof val;
25 | switch (type) {
26 | case 'boolean':
27 | return val ? key : '';
28 | case 'string':
29 | if (val === '') return '';
30 | return `${key}="${val}"`;
31 | default:
32 | return `:${key}='${JSON.stringify(val, null, 2)}'`;
33 | }
34 | };
35 |
36 | return templateSource.replace(
37 | replacing,
38 | Object.keys(componentArgs)
39 | .map((key) => ' ' + propToSource(kebabCase(key), args[key]))
40 | .join('')
41 | );
42 | }
43 |
44 | export const transformSource = (src, ctx, prettify = true) => {
45 | const args = {
46 | ...ctx.initialArgs,
47 | ...ctx.args,
48 | };
49 | const match = /\b("')?template\1:\s*`([^`]+)`/.exec(src);
50 | if (match) {
51 | const code = templateSourceCode(match[2], args, ctx.argTypes);
52 | if (!prettify) {
53 | return code;
54 | }
55 | return dedent(
56 | prettier.format(code, {
57 | parser: 'vue',
58 | plugins: [prettierHtml],
59 | htmlWhitespaceSensitivity: 'ignore',
60 | })
61 | );
62 | }
63 | return src;
64 | };
65 |
66 | export const withSource = makeDecorator({
67 | name: 'withSource',
68 | wrapper: (storyFn, ctx) => {
69 | const story = storyFn(ctx);
70 | return {
71 | setup() {
72 | onMounted(() => {
73 | try {
74 | const src = ctx.originalStoryFn.parameters.storySource.source;
75 | const transformedSource = transformSource(src, ctx);
76 | const channel = addons.getChannel();
77 | const storyId = ctx.id;
78 | channel.emit(
79 | 'storybook/docs/snippet-rendered',
80 | storyId,
81 | transformedSource
82 | );
83 | } catch (e) {
84 | console.warn('Failed to render code', e);
85 | }
86 | });
87 | return () => h(story);
88 | },
89 | };
90 | },
91 | });
92 |
--------------------------------------------------------------------------------
/src/components/Modal/Modal.mdx:
--------------------------------------------------------------------------------
1 | import { Canvas, Meta, Story, Controls } from '@storybook/blocks';
2 |
3 | import * as ModalStories from './Modal.stories';
4 |
5 |
6 |
7 | # Modal
8 |
9 | Modal is a component that renders a modal dialog. It is used to display content in a layer above the app.
10 |
11 | - [Overview](#overview)
12 |
13 | - [Playground](#playground)
14 |
15 | - [Usage with props](#usage)
16 |
17 | - [Slots](#slots)
18 |
19 | - [Stories](#stories)
20 |
21 | - [Tips](#tips)
22 |
23 | ## Overview
24 |
25 |
28 |
29 | ## Playground
30 |
31 | > Changes you make in the controls will be reflected in the example above.
32 |
33 |
34 |
35 | ## Usage with props
36 |
37 | ### modelValue (required)
38 |
39 | The `modelValue` prop is used to control the visibility of the modal. It is a boolean value that defaults to `false`.
40 |
41 | ### block (optional)
42 |
43 | The `block` prop is used to control the block behavior of the modal. It is a boolean value that defaults to `true`.
44 |
45 | ### title (optional)
46 |
47 | The `title` prop is used to set the title of the modal. It is a string value that defaults to `null`.
48 |
49 | ### description (optional)
50 |
51 | The `description` prop is used to set the description of the modal. It is a string value that defaults to `null`.
52 |
53 | ### icon (optional)
54 |
55 | The `icon` prop is used to set the icon of the modal. It is a string value that defaults to `null`.
56 |
57 | ## Slots
58 |
59 | ### header (optional)
60 |
61 | The `header` slot is used to render the header of the modal.
62 |
63 | ### default (optional)
64 |
65 | The `default` slot is used to render the body of the modal.
66 |
67 | ### actions (optional)
68 |
69 | The `actions` slot is used to render the actions of the modal.
70 |
71 | ## Stories
72 |
73 | ### Fully Slotable
74 |
75 |
78 |
79 | ### A Form Dialog
80 |
81 |
84 |
85 | ## Tips
86 |
87 | - Use the `modelValue` prop to control the visibility of the modal.
88 |
89 | - Use the `block` prop to control the block behavior of the modal.
90 |
91 | - Use the `title` prop to set the title of the modal.
92 |
93 | - Use the `description` prop to set the description of the modal.
94 |
95 | - Use the `icon` prop to set the icon of the modal.
96 |
--------------------------------------------------------------------------------
/src/components/Flex/Flex.stories.js:
--------------------------------------------------------------------------------
1 | import Box from '../Box/RBox.vue'
2 | import Flex from './RFlex.vue'
3 |
4 | function Default(args) {
5 | return {
6 | components: { Flex, Box },
7 | setup() {
8 | return { args }
9 | },
10 | template: `
11 |
12 | Box 1
13 | Box 2
14 | `,
15 | }
16 | }
17 |
18 | const defaultArgs = {
19 | align: {
20 | control: {
21 | type: 'select',
22 | options: [
23 | 'normal',
24 | 'stretch',
25 | 'center',
26 | 'start',
27 | 'end',
28 | 'flex-start',
29 | 'flex-end',
30 | 'baseline',
31 | 'first baseline',
32 | 'last baseline',
33 | 'safe center',
34 | 'unsafe center',
35 | ],
36 | },
37 | defaultValue: 'start',
38 | },
39 | justify: {
40 | control: {
41 | type: 'select',
42 | options: [
43 | 'start',
44 | 'center',
45 | 'end',
46 | 'flex-start',
47 | 'flex-end',
48 | 'left',
49 | 'right',
50 | 'normal',
51 | 'space-between',
52 | 'space-around',
53 | 'space-evenly',
54 | 'stretch',
55 | 'safe center',
56 | 'unsafe center',
57 | ],
58 | },
59 | defaultValue: 'start',
60 | },
61 | wrap: {
62 | control: {
63 | type: 'select',
64 | options: ['nowrap', 'wrap', 'wrap-reverse'],
65 | },
66 | defaultValue: 'nowrap',
67 | },
68 | direction: {
69 | control: {
70 | type: 'select',
71 | options: ['row', 'row-reverse', 'column', 'column-reverse'],
72 | },
73 | defaultValue: 'row',
74 | },
75 | basis: {
76 | control: {
77 | type: 'select',
78 | options: [
79 | 'auto',
80 | 'max-content',
81 | 'min-content',
82 | 'fit-content',
83 | 'content',
84 | 'inherit',
85 | 'initial',
86 | 'unset',
87 | ],
88 | },
89 | defaultValue: 'auto',
90 | },
91 | grow: {
92 | control: {
93 | type: 'select',
94 | options: ['0', '1', '2', '3', '4', '5'],
95 | },
96 | defaultValue: '0',
97 | },
98 | shrink: {
99 | control: {
100 | type: 'select',
101 | options: ['0', '1', '2', '3', '4', '5'],
102 | },
103 | defaultValue: '1',
104 | },
105 | }
106 |
107 | export default {
108 | title: 'Layout/Flex',
109 | component: Flex,
110 |
111 | argTypes: {
112 | ...defaultArgs,
113 | },
114 |
115 | parameters: {
116 | viewMode: 'docs',
117 | },
118 | }
119 |
120 | export const Overview = {
121 | render: Default.bind({}),
122 | name: 'Overview',
123 |
124 | argTypes: {
125 | ...Default.argTypes,
126 | },
127 |
128 | args: {},
129 | }
130 |
--------------------------------------------------------------------------------
/src/components/Label/Label.mdx:
--------------------------------------------------------------------------------
1 | import { Canvas, Meta, Story, Controls } from '@storybook/blocks';
2 | import * as LabelStories from './Label.stories';
3 |
4 |
5 |
6 | # Label
7 |
8 | - [Overview](#overview)
9 |
10 | - [Playground](#playground)
11 |
12 | - [Usage with props](#usage)
13 |
14 | - [Tips](#tips)
15 |
16 | ### Overview
17 |
18 | The Label component is a UI element that is used to display a label for a form element or other component. It is typically used in conjunction with form elements such as input fields, checkboxes, and radio buttons, as well as other components like sliders and switches. The Label component can be used to provide a clear and concise description of the associated form element or component, helping users understand its purpose and how to use it.
19 |
20 |
23 |
24 | ### Playground
25 |
26 |
27 |
28 | ## Usage with props
29 |
30 | ### id (required)
31 |
32 | The id prop is a unique identifier for the label element. It should be set to the same value as the id prop of the form element or component that the label is associated with.
33 |
34 | ### for (required)
35 |
36 | The for prop is used to associate the label with a form element or component. It should be set to the same value as the id prop of the form element or component that the label is associated with.
37 |
38 | ### text (required)
39 |
40 | The text prop is the text that will be displayed within the label element. It can be a string or a number.
41 |
42 | ### Tips
43 |
44 | 💡 Use clear and concise text for the text prop: Make sure that the text within the label accurately and clearly describes the associated form element or component. Avoid using jargon or technical terms that may be unfamiliar to some users.
45 |
46 | 💡 Use the for and id props correctly: The for prop should be set to the id of the form element or component that the label is associated with, and the id prop should be a unique identifier for the label element. This ensures that the label is properly associated with the correct form element or component, and helps screen readers and other assistive technologies correctly interpret the label.
47 |
48 | 💡 Consider using aria-label or aria-labelledby: If the label text is not visually displayed (e.g. because it is hidden using CSS), you may need to use the aria-label or aria-labelledby attributes to provide an accessible label for screen readers and other assistive technologies.
49 |
50 | 💡 Use label elements for all form elements: It is important to use label elements for all form elements, even if the form element is already described by surrounding text. This ensures that the form element is properly labeled and easier to use for all users, including those using assistive technologies.
51 |
--------------------------------------------------------------------------------
/src/components/Flex/RFlex.vue:
--------------------------------------------------------------------------------
1 |
90 |
91 |
92 |
134 |
135 |
--------------------------------------------------------------------------------
/src/components/Avatar/Avatar.mdx:
--------------------------------------------------------------------------------
1 | import { Canvas, Meta, Story, Controls } from '@storybook/blocks';
2 | import * as AvatarStories from './Avatar.stories';
3 |
4 |
5 |
6 | # Avatar
7 |
8 | Avatar component displays a visual representation of a user or an entity.
9 |
10 | - [Overview](#overview)
11 |
12 | - [Playground](#playground)
13 |
14 | - [Usage with props](#usage)
15 |
16 | - [Tips](#tips)
17 |
18 | ### Overview
19 |
20 | Avatar is typically used to identify a user or entity within a user interface, and can be displayed in various forms such as an image, text, or initials.
21 |
22 | To use an Avatar component, you can pass the necessary props to configure it to display the avatar in the desired way. For example, you can specify the type of avatar to display (e.g. image or text), the size of the avatar, and any additional props such as the source of the image or the text to display.
23 |
24 |
27 |
28 | ### Playground
29 |
30 | > Changes you make in the controls will be reflected in the example above.
31 |
32 |
33 |
34 | ## Stories
35 |
36 | ### Image
37 |
38 |
41 |
42 | ### Text
43 |
44 |
47 |
48 | ### Anonymous
49 |
50 |
53 |
54 | ### Online
55 |
56 |
59 |
60 | ## Usage with props
61 |
62 | ### type (optional)
63 |
64 | The **type** props is for the type of avatar to display. Can be either "image" or "text".
65 |
66 | ### src (optional)
67 |
68 | The **src** props is for the source of the image to display. This prop is only used when type is set to "image".
69 |
70 | ### alt (optional)
71 |
72 | The **alt** props is for the alt text for the image. This prop is only used when type is set to "image".
73 |
74 | ### size (optional)
75 |
76 | The **size** props is for the size of the avatar. Can be one of "xs", "sm", "md", "lg", "xl", or "2xl".
77 |
78 | ### online (optional)
79 |
80 | The **online** props is for whether to show a green circle indicator to signify that the user is online.
81 |
82 | ### text (optional)
83 |
84 | The **text** props is for the text to display when type is set to "text".
85 |
86 | ### Tips
87 |
88 | 💡 Use appropriate sizes for different contexts: The size of the avatar should be appropriate for the context in which it is being used. For example, a small avatar might be suitable for a list of users, while a larger avatar might be more suitable for a profile page.
89 |
90 | 💡 Use initials for text avatars: If you are using a text avatar, it is a good idea to use the user's initials rather than their full name. This can help to keep the avatar compact and easy to read.
91 |
92 | 💡 Use images sparingly: If you are using an image avatar, be mindful of the number of images you are loading on the page. If you are displaying a large number of avatars, it may be more efficient to use text avatars instead.
93 |
94 | 💡 Use alternative text for accessibility: When using an image avatar, make sure to include alt text to provide a textual representation of the image for screen readers and other assistive technologies.
95 |
96 | 💡 Consider the context of use: Think about the context in which the avatar will be used and design it accordingly. For example, if the avatar is being used in a chat application, you might want to include an online status indicator.
97 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
143 |
144 |
--------------------------------------------------------------------------------
/src/components/Accordion/Accordion.mdx:
--------------------------------------------------------------------------------
1 | import { Canvas, Meta, Story, Controls } from '@storybook/blocks';
2 | import * as AccordionStories from './Accordion.stories';
3 |
4 |
5 |
6 | # Accordion
7 |
8 | The Accordion component is a UI element that allows the user to show and hide content within an expandable and collapsible section. It consists of a container that holds multiple panels, where each panel has a header and a body. When a panel's header is clicked, the corresponding body is shown or hidden.
9 |
10 | - [Overview](#overview)
11 |
12 | - [Playground](#playground)
13 |
14 | - [Usage with props](#usage)
15 |
16 | - [Stories](#stories)
17 |
18 | - [Customizable Slots](#customizable-slots)
19 |
20 | - [Tips](#tips)
21 |
22 | ## Overview
23 |
24 | The Accordion component provides a compact way of displaying large amounts of information in a small space. It is particularly useful for displaying hierarchical data, where each panel represents a different level of information. The Accordion component is also a good way to present frequently asked questions (FAQs) as it makes it easy for users to find the information they are looking for.
25 |
26 |
29 |
30 | ## Playground
31 |
32 | > Changes you make in the controls will be reflected in the example above. Try it yourself!
33 |
34 |
35 |
36 | ## Usage with props
37 |
38 | ### accordions (required)
39 |
40 | The **accordions** prop is an array that holds the data for each panel in the accordion component. Each panel is an object with properties such as header and body.
41 |
42 | ### multiple (optional)
43 |
44 | The **multiple** prop is a boolean that determines whether multiple panels can be expanded at the same time. A value of true indicates that multiple panels can be expanded, and a value of false indicates that only one panel can be expanded at a time.
45 |
46 | ### accordions: title (required)
47 |
48 | The **title** prop is a string that is used as the header for each panel in the accordion component.
49 |
50 | ### accordions: content (required)
51 |
52 | The **content** prop is a string that is used as the body for each panel in the accordion component.
53 |
54 | ### accordions: open (optional)
55 |
56 | The **open** prop is a boolean that determines whether the panel is in an expanded or collapsed state. A value of true indicates that the panel is expanded, and a value of false indicates that it is collapsed.
57 |
58 | ### accordions: disabled (optional)
59 |
60 | The **disabled** prop is a boolean that determines whether the panel is disabled. A value of true indicates that the panel is disabled, and a value of false indicates that it is enabled.
61 |
62 | ## Stories
63 |
64 | ### Single Collapse
65 |
66 |
69 |
70 | ### Accordion
71 |
72 |
75 |
76 |
79 |
80 | ## Customizable Slots
81 |
82 | ### Tips
83 |
84 | 💡 Use clear and concise headings for each panel's header to help users quickly understand the content within each panel.
85 |
86 | 💡 Consider using icons or graphics within the header to make the accordion more visually appealing.
87 |
88 | 💡 Make sure that the accordion is accessible to users who may be using assistive technologies, such as screen readers.
89 |
90 | 💡 Use the modelValue prop wisely to control the default state of the accordion. For example, you might want to set the modelValue prop to true for the panel that contains the most important information.
91 |
92 | 💡 Try to limit the amount of content within each panel to ensure that the accordion remains compact and easy to use.
93 |
--------------------------------------------------------------------------------
/src/components/ItemGroup/RItemGroup.vue:
--------------------------------------------------------------------------------
1 |
138 |
139 |
140 |
141 |
147 |
148 |
149 |
--------------------------------------------------------------------------------
/src/components/TextArea/TextArea.mdx:
--------------------------------------------------------------------------------
1 | import { Canvas, Meta, Story, Controls } from '@storybook/blocks';
2 | import * as TextAreaStories from './TextArea.stories';
3 |
4 |
5 |
6 | # TextArea
7 |
8 | The TextArea component is a UI element that allows users to input and edit multiple lines of text.
9 |
10 | - [Overview](#overview)
11 |
12 | - [Playground](#playground)
13 |
14 | - [Usage with props](#usage)
15 |
16 | - [Stories](#stories)
17 |
18 | - [Tips](#tips)
19 |
20 | ## Overview
21 |
22 | This component provides a text area for user input and includes several optional props for customization such as error message, disabled state, placeholder text, default value, label, and hint.
23 |
24 |
27 |
28 | ### Playground
29 |
30 | > Changes you make in the controls will be reflected in the example above.
31 |
32 |
33 |
34 | ## Usage
35 |
36 | ### id (required)
37 |
38 | The **id** prop is a unique identifier for the TextArea component.
39 |
40 | ### errorMsg (optional)
41 |
42 | The **errorMsg** prop is the error message to display when input is invalid.
43 |
44 | ### disabled (optional)
45 |
46 | The **disabled** props is a boolean value that sets the component to a disabled state.
47 |
48 | ### placeholder (optional)
49 |
50 | The **placeholder** prop is the placeholder text to display when the text area is empty.
51 |
52 | ### modelValue (optional)
53 |
54 | The **modelValue** props is the default value for the text area.
55 |
56 | ### label (optional)
57 |
58 | The **label** prop is the text to display as a label for the text area.
59 |
60 | ### hint (optional)
61 |
62 | The **hint** prop is for additional information or context for the text area.
63 |
64 | ## Stories
65 |
66 | ### Default
67 |
68 |
71 |
72 | ### Disabled
73 |
74 |
77 |
78 | ### Hint
79 |
80 |
83 |
84 | ### Error
85 |
86 |
89 |
90 | ### Tips
91 |
92 | 💡 Always include the 'id' prop: The 'id' prop is required for the TextArea component to function properly. Make sure to always include it and ensure that it is unique for each instance of the component.
93 |
94 | 💡 The 'label' prop is used to provide a text description of the TextArea component for accessibility purposes. This is especially important for users who rely on screen readers to navigate your application.
95 |
96 | 💡 The 'placeholder' prop allows you to provide users with an example or a hint of the kind of input you expect. This can be especially helpful for users who may not be sure what to type in the TextArea.
97 |
98 | 💡 The 'modelValue' prop allows you to set a default value for the TextArea component. This can be useful if you want to pre-populate the TextArea with a value or if you want to show the last input a user made.
99 |
100 | 💡 The 'errorMsg' prop allows you to display an error message when the input is invalid. This can be useful for providing feedback to the user when they make a mistake.
101 |
102 | 💡 The 'disabled' prop allows you to disable the textarea. This can be useful when you want to prevent users from making changes or inputting text.
103 |
104 | 💡 The 'hint' prop allows you to provide additional context or information about the TextArea component. This can be useful for providing more detailed instructions or information to the user.
105 |
106 | 💡 Limit the number of characters that can be entered, to avoid overflowing the layout or taking too much space in the database.
107 |
108 | 💡 Make sure the text area is adaptable to different screen sizes, to improve the user experience on different devices
109 |
--------------------------------------------------------------------------------
/src/components/Accordion/accordion.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 | import { mount } from '@vue/test-utils'
3 | import Accordion from './RAccordion.vue'
4 |
5 | describe('Accordion', () => {
6 | it('should render correctly', () => {
7 | const wrapper = mount(Accordion, {
8 | props: {
9 | accordions: [
10 | {
11 | title: 'Accordion Title',
12 | content: 'Accordion Description',
13 | },
14 | ],
15 | },
16 | })
17 |
18 | expect(wrapper.exists()).toBe(true)
19 | expect(wrapper.find('.accordion__title').text()).toBe('Accordion Title')
20 | expect(wrapper.find('.accordion__content').text()).toBe(
21 | 'Accordion Description',
22 | )
23 | expect(
24 | wrapper
25 | .find('.accordion__header')
26 | .trigger('click')
27 | .then(() => {
28 | expect(wrapper.emitted('update:modelValue')).toBeTruthy()
29 | }),
30 | )
31 | })
32 |
33 | it('should render correctly with multiple accordions', () => {
34 | const wrapper = mount(Accordion, {
35 | props: {
36 | accordions: [
37 | {
38 | title: 'Accordion Title 1',
39 | content: 'Accordion Description 1',
40 | },
41 | {
42 | title: 'Accordion Title 2',
43 | content: 'Accordion Description 2',
44 | },
45 | ],
46 | },
47 | })
48 |
49 | expect(wrapper.exists()).toBe(true)
50 | expect(wrapper.findAll('.accordion__title').length).toBe(2)
51 | expect(wrapper.findAll('.accordion__content').length).toBe(2)
52 | expect(wrapper.findAll('.accordion__title')[0].text()).toBe(
53 | 'Accordion Title 1',
54 | )
55 | expect(wrapper.findAll('.accordion__content')[0].text()).toBe(
56 | 'Accordion Description 1',
57 | )
58 | expect(wrapper.findAll('.accordion__title')[1].text()).toBe(
59 | 'Accordion Title 2',
60 | )
61 | expect(wrapper.findAll('.accordion__content')[1].text()).toBe(
62 | 'Accordion Description 2',
63 | )
64 | })
65 |
66 | it('should render correctly with multiple accordions and one expanded', () => {
67 | const wrapper = mount(Accordion, {
68 | props: {
69 | accordions: [
70 | {
71 | title: 'Accordion Title 1',
72 | content: 'Accordion Description 1',
73 | isExpanded: true,
74 | },
75 | {
76 | title: 'Accordion Title 2',
77 | content: 'Accordion Description 2',
78 | },
79 | ],
80 | },
81 | })
82 |
83 | expect(wrapper.exists()).toBe(true)
84 | expect(wrapper.findAll('.accordion__title').length).toBe(2)
85 | expect(wrapper.findAll('.accordion__content').length).toBe(2)
86 |
87 | expect(wrapper.findAll('.accordion')[0].classes()).toContain(
88 | 'accordion--expanded',
89 | )
90 | expect(wrapper.findAll('.accordion')[1].classes()).not.toContain(
91 | 'accordion--expanded',
92 | )
93 | })
94 |
95 | it('should render correctly with multiple accordions and one expanded and one disabled', () => {
96 | const wrapper = mount(Accordion, {
97 | props: {
98 | accordions: [
99 | {
100 | title: 'Accordion Title 1',
101 | content: 'Accordion Description 1',
102 | isExpanded: true,
103 | },
104 | {
105 | title: 'Accordion Title 2',
106 | content: 'Accordion Description 2',
107 | isDisabled: true,
108 | },
109 | ],
110 | },
111 | })
112 |
113 | expect(wrapper.exists()).toBe(true)
114 | expect(wrapper.findAll('.accordion__title').length).toBe(2)
115 | expect(wrapper.findAll('.accordion__content').length).toBe(2)
116 | expect(wrapper.findAll('.accordion')[0].classes()).toContain(
117 | 'accordion--expanded',
118 | )
119 | expect(wrapper.findAll('.accordion')[1].classes()).toContain(
120 | 'accordion--disabled',
121 | )
122 | })
123 | })
124 |
--------------------------------------------------------------------------------