├── public └── favicon.ico ├── src ├── assets │ └── tailwind.css ├── types │ ├── helpers.ts │ ├── components │ │ ├── t-tag.ts │ │ ├── t-input.ts │ │ ├── t-textarea.ts │ │ ├── t-card.ts │ │ ├── t-alert.ts │ │ ├── t-button.ts │ │ ├── t-radio.ts │ │ ├── t-toggle.ts │ │ ├── t-checkbox.ts │ │ ├── t-input-group.ts │ │ ├── t-select.ts │ │ ├── t-dropdown.ts │ │ ├── t-modal.ts │ │ ├── t-dialog.ts │ │ └── t-rich-select.ts │ ├── utils.ts │ ├── misc.ts │ ├── index.ts │ ├── vueRouter.ts │ └── variantCore.ts ├── development │ ├── TSubmit.vue │ ├── About.vue │ ├── App.vue │ ├── AppMenu.vue │ ├── router.ts │ ├── Attributes.vue │ ├── Multioptions.vue │ ├── Alert.vue │ ├── Home.vue │ ├── Check.vue │ ├── Options.vue │ ├── Modal.vue │ └── RichSelect.vue ├── shims-vue.d.ts ├── use │ ├── useInjectsConfiguration.ts │ ├── useInjectsClassesList.ts │ ├── useInjectsClassesListClass.ts │ ├── useVModel.ts │ ├── useMultioptions.ts │ ├── useConfigurationWithClassesList.ts │ ├── useMulipleableVModel.ts │ ├── useConfiguration.ts │ ├── useActivableOption.ts │ └── useSelectableOption.ts ├── __tests │ ├── components │ │ ├── misc │ │ │ ├── Transitionable.spec.ts │ │ │ └── TextPlaceholder.spec.ts │ │ ├── TRichSelect │ │ │ ├── RichSelectClearButton.spec.ts │ │ │ ├── RichSelectTriggerTags.spec.ts │ │ │ ├── RichSelectDropdown.spec.ts │ │ │ ├── RichSelectState.spec.ts │ │ │ └── RichSelectSearchInput.spec.ts │ │ ├── icons │ │ │ └── CustomIcon.spec.ts │ │ ├── TSelect.integration.spec.ts │ │ ├── TTag.spec.ts │ │ ├── TSelect │ │ │ └── TSelectOption.spec.ts │ │ ├── TCheckbox.integration.spec.ts │ │ ├── TInput.integration.spec.ts │ │ ├── TTextarea.integration.spec.ts │ │ └── TRadio.integration.spec.ts │ ├── use │ │ ├── useVModel.spec.ts │ │ ├── useInjectsClassesList.spec.ts │ │ ├── useInjectsConfiguration.spec.ts │ │ ├── useSetup.ts │ │ ├── useMulipleableVModel.spec.ts │ │ ├── useMultioptions.spec.ts │ │ └── useConfigurationWithClassesList.spec.ts │ ├── utils │ │ ├── getVariantProps.spec.ts │ │ ├── svgToVueComponent.spec.ts │ │ ├── popper.spec.ts │ │ ├── emitter.spec.ts │ │ └── createDialogProgramatically.spec.ts │ ├── testUtils.ts │ ├── index.spec.ts │ └── plugin.spec.ts ├── icons │ ├── CloseIcon.vue │ ├── CheckCircleIcon.vue │ ├── CheckmarkIcon.vue │ ├── CrossCircleIcon.vue │ ├── SolidCheckCircleIcon.vue │ ├── SolidInformationCircleIcon.vue │ ├── InformationCircleIcon.vue │ ├── ExclamationIcon.vue │ ├── SelectorIcon.vue │ ├── SolidQuestionMarkCircleIcon.vue │ ├── QuestionMarkCircleIcon.vue │ ├── SolidExclamationIcon.vue │ ├── SolidCrossCircleIcon.vue │ ├── CustomIcon.vue │ └── LoadingIcon.vue ├── main.ts ├── utils │ ├── popper.ts │ ├── svgToVueComponent.ts │ ├── emitter.ts │ ├── getVariantProps.ts │ └── createDialogProgramatically.ts ├── components │ ├── TRichSelect │ │ ├── RichSelectClearButton.vue │ │ ├── RichSelectTriggerTags.vue │ │ ├── RichSelectState.vue │ │ ├── RichSelectSearchInput.vue │ │ ├── RichSelectDropdown.vue │ │ ├── RichSelectTriggerTagsTag.vue │ │ ├── RichSelectOptionsList.vue │ │ └── RichSelectTrigger.vue │ ├── misc │ │ ├── TextPlaceholder.vue │ │ └── Transitionable.vue │ ├── TTag.vue │ ├── TRadio.vue │ ├── TCheckbox.vue │ ├── TSelect │ │ └── TSelectOption.vue │ ├── TInput.vue │ ├── TTextarea.vue │ ├── TCard.vue │ ├── TSelect.vue │ ├── TButton.vue │ ├── TInputGroup.vue │ └── TAlert.vue ├── index.ts └── plugin.ts ├── .gitignore ├── postcss.config.js ├── .github ├── FUNDING.yml └── workflows │ └── yarn.yml ├── vite.demo.config.ts ├── tailwind.config.js ├── index.html ├── jest.config.js ├── tsconfig.json ├── vite.config.ts ├── .eslintrc.json └── package.json /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/variantjs/vue/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | coverage 6 | *.local 7 | yarn-error.log -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: variantjs 3 | custom: https://www.buymeacoffee.com/alfonsobries 4 | -------------------------------------------------------------------------------- /vite.demo.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/types/helpers.ts: -------------------------------------------------------------------------------- 1 | type ObjectWithProperties

= Record; 2 | 3 | type KeysOfType = { [P in keyof T]: T[P] extends TProp? P : never }[keyof T]; 4 | 5 | export { ObjectWithProperties, KeysOfType }; 6 | -------------------------------------------------------------------------------- /src/types/components/t-tag.ts: -------------------------------------------------------------------------------- 1 | import { Data, WithVariantProps } from '@variantjs/core'; 2 | import { HTMLAttributes } from 'vue'; 3 | 4 | export type TTagOptions = WithVariantProps<{ 5 | tagName?: string 6 | text?: string 7 | } & HTMLAttributes & Data>; 8 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | './src/development/**/*.{html,js,vue,ts}', 4 | './node_modules/@variantjs/core/src/config/**/*.ts' 5 | ], 6 | plugins: [ 7 | require('@tailwindcss/forms'), 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/yarn.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Install modules 9 | run: yarn 10 | - name: Run tests 11 | run: yarn test --coverage -------------------------------------------------------------------------------- /src/development/TSubmit.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | declare module '*.vue' { 3 | import { DefineComponent } from 'vue'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | -------------------------------------------------------------------------------- /src/use/useInjectsConfiguration.ts: -------------------------------------------------------------------------------- 1 | import { Data, WithVariantPropsAndClassesList } from '@variantjs/core'; 2 | import { inject } from 'vue'; 3 | 4 | export default function useInjectsConfiguration

>(): P { 5 | return inject

('configuration', {} as P); 6 | } 7 | -------------------------------------------------------------------------------- /src/__tests/components/misc/Transitionable.spec.ts: -------------------------------------------------------------------------------- 1 | import Transitionable from '../../../components/misc/Transitionable.vue'; 2 | 3 | describe('Transitionable', () => { 4 | it('defaults the classes list to an empty object', () => { 5 | expect(Transitionable.props.classesList.default()).toEqual({}); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/types/components/t-input.ts: -------------------------------------------------------------------------------- 1 | import { Data, WithVariantProps } from '@variantjs/core'; 2 | import { InputHTMLAttributes } from 'vue'; 3 | 4 | export type TInputValue = string | number | string[] | undefined; 5 | 6 | export type TInputOptions = WithVariantProps<{ 7 | modelValue?: TInputValue, 8 | } & InputHTMLAttributes & Data>; 9 | -------------------------------------------------------------------------------- /src/icons/CloseIcon.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/types/components/t-textarea.ts: -------------------------------------------------------------------------------- 1 | import { Data, WithVariantProps } from '@variantjs/core'; 2 | import { InputHTMLAttributes } from 'vue'; 3 | 4 | export type TTextareaValue = string | number | string[] | undefined; 5 | 6 | export type TTextareaOptions = WithVariantProps<{ 7 | modelValue?: TTextareaValue, 8 | } & InputHTMLAttributes & Data>; 9 | -------------------------------------------------------------------------------- /src/types/components/t-card.ts: -------------------------------------------------------------------------------- 1 | import { WithVariantPropsAndClassesList, TCardClassesValidKeys, Data } from '@variantjs/core'; 2 | import { HTMLAttributes } from 'vue'; 3 | 4 | export type TCardOptions = WithVariantPropsAndClassesList<{ 5 | tagName?: string 6 | body?: string 7 | header?: string 8 | footer?: string 9 | } & HTMLAttributes & Data, TCardClassesValidKeys>; 10 | -------------------------------------------------------------------------------- /src/icons/CheckCircleIcon.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |

11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/CheckmarkIcon.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/icons/CrossCircleIcon.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/icons/SolidCheckCircleIcon.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/icons/SolidInformationCircleIcon.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/__tests/components/TRichSelect/RichSelectClearButton.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import RichSelectClearButton from '../../../components/TRichSelect/RichSelectClearButton.vue'; 3 | 4 | describe('RichSelectClearButton', () => { 5 | it('renders the component', () => { 6 | const wrapper = shallowMount(RichSelectClearButton); 7 | expect(wrapper.vm.$el.tagName).toBe('BUTTON'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/icons/InformationCircleIcon.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/use/useInjectsClassesList.ts: -------------------------------------------------------------------------------- 1 | import { CSSClassesList } from '@variantjs/core'; 2 | import { ComputedRef, computed } from 'vue'; 3 | import useInjectsConfiguration from './useInjectsConfiguration'; 4 | 5 | export default function useInjectsClassesList(): ComputedRef { 6 | const configuration = useInjectsConfiguration(); 7 | 8 | return computed((): CSSClassesList => configuration.classesList || {}); 9 | } 10 | -------------------------------------------------------------------------------- /src/use/useInjectsClassesListClass.ts: -------------------------------------------------------------------------------- 1 | import { CSSClass, get } from '@variantjs/core'; 2 | import { ComputedRef, computed } from 'vue'; 3 | import useInjectsConfiguration from './useInjectsConfiguration'; 4 | 5 | export default function useInjectsClassesListClass(property: string): ComputedRef { 6 | const configuration = useInjectsConfiguration(); 7 | 8 | return computed((): CSSClass => get(configuration.classesList, property, '')); 9 | } 10 | -------------------------------------------------------------------------------- /src/icons/ExclamationIcon.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/icons/SelectorIcon.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /src/icons/SolidQuestionMarkCircleIcon.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/icons/QuestionMarkCircleIcon.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/icons/SolidExclamationIcon.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/icons/SolidCrossCircleIcon.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | preset: '@vue/cli-plugin-unit-jest/presets/typescript', 4 | collectCoverageFrom: [ 5 | "src/**/*.{vue,ts}", 6 | "!**/node_modules/**", 7 | "!**/*.d.ts" 8 | ], 9 | coverageReporters: ["text", "json", "html"], 10 | testMatch: [ "**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)" ], 11 | coveragePathIgnorePatterns: [ 12 | "/node_modules/", 13 | "/src/main.ts", 14 | "/src/App.vue", 15 | "/src/development/", 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/types/components/t-alert.ts: -------------------------------------------------------------------------------- 1 | import { WithVariantPropsAndClassesList, TAlertClassesValidKeys, Data } from '@variantjs/core'; 2 | import { HTMLAttributes } from 'vue'; 3 | import { IconProp } from '../misc'; 4 | 5 | export type TAlertOptions = WithVariantPropsAndClassesList<{ 6 | text?: string, 7 | tagName?: string, 8 | bodyTagName?: string, 9 | dismissible?: boolean, 10 | show?: boolean, 11 | timeout?: number, 12 | animate?: boolean, 13 | closeIcon?: IconProp, 14 | } & HTMLAttributes & Data, TAlertClassesValidKeys>; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "lib": ["esnext", "dom"], 14 | "types": ["vite/client", "jest"], 15 | }, 16 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /src/use/useVModel.ts: -------------------------------------------------------------------------------- 1 | import { Data } from '@variantjs/core'; 2 | import { 3 | getCurrentInstance, Ref, watch, ref, 4 | } from 'vue'; 5 | 6 | export default function useVModel

( 7 | props: P, 8 | key: K, 9 | ): Ref { 10 | const vm = getCurrentInstance(); 11 | 12 | const localValue = ref(props[key]) as Ref; 13 | 14 | watch(localValue, (value) => { 15 | vm?.emit(`update:${key}`, value); 16 | }); 17 | 18 | watch(() => props[key], (value) => { 19 | localValue.value = value; 20 | }); 21 | 22 | return localValue; 23 | } 24 | -------------------------------------------------------------------------------- /src/development/About.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './assets/tailwind.css'; 2 | import { createApp } from 'vue'; 3 | import plugin from './plugin'; 4 | import { VariantJSConfiguration } from './types/variantCore'; 5 | 6 | import App from './development/App.vue'; 7 | 8 | import router from './development/router'; 9 | 10 | const app = createApp(App); 11 | 12 | app.use(router); 13 | 14 | const configuration: VariantJSConfiguration = { 15 | TCard: { 16 | classes: { 17 | wrapper: 'bg-white border border-gray-100 rounded shadow-sm w-full', 18 | }, 19 | }, 20 | }; 21 | 22 | app.use(plugin, configuration); 23 | 24 | app.mount('#app'); 25 | -------------------------------------------------------------------------------- /src/types/components/t-button.ts: -------------------------------------------------------------------------------- 1 | import { Data, WithVariantProps } from '@variantjs/core'; 2 | import { ButtonHTMLAttributes } from 'vue'; 3 | import { VueRouteAriaCurrentValue, VueRouteRouteLocationRaw } from '../vueRouter'; 4 | 5 | type RouterLinkProps = { 6 | to?: VueRouteRouteLocationRaw, 7 | replace?: boolean, 8 | activeClass?: string, 9 | exactActiveClass?: string, 10 | custom?: boolean, 11 | ariaCurrentValue?: VueRouteAriaCurrentValue, 12 | }; 13 | 14 | export type TButtonOptions = WithVariantProps<{ 15 | tagName?: string 16 | href?: string 17 | } & RouterLinkProps & ButtonHTMLAttributes & Data>; 18 | -------------------------------------------------------------------------------- /src/types/components/t-radio.ts: -------------------------------------------------------------------------------- 1 | import { Data, WithVariantProps } from '@variantjs/core'; 2 | import { InputHTMLAttributes } from 'vue'; 3 | import { ObjectWithProperties } from '../helpers'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/ban-types 6 | type TRadioSimpleValue = string | number | boolean | undefined | null | Date | Function | symbol; 7 | export type TRadioValue = TRadioSimpleValue | TRadioSimpleValue[] | ObjectWithProperties; 8 | 9 | export type TRadioOptions = WithVariantProps<{ 10 | modelValue?: TRadioValue 11 | } & InputHTMLAttributes & { 12 | type?: 'radio' 13 | } & Data>; 14 | -------------------------------------------------------------------------------- /src/types/components/t-toggle.ts: -------------------------------------------------------------------------------- 1 | import { WithVariantPropsAndClassesList, TToggleClassesValidKeys, Data } from '@variantjs/core'; 2 | import { HTMLAttributes } from 'vue'; 3 | import { TCheckboxValue } from './t-checkbox'; 4 | 5 | export type TToggleValue = TCheckboxValue; 6 | 7 | export type TToggleOptions = WithVariantPropsAndClassesList<{ 8 | name?: string, 9 | modelValue?: TToggleValue, 10 | value?: TToggleValue, 11 | uncheckedValue?: TToggleValue, 12 | checked?: boolean, 13 | disabled?: boolean, 14 | checkedPlaceholder?: string, 15 | uncheckedPlaceholder?: string, 16 | } & HTMLAttributes & Data, TToggleClassesValidKeys>; 17 | -------------------------------------------------------------------------------- /src/types/components/t-checkbox.ts: -------------------------------------------------------------------------------- 1 | import { Data, WithVariantProps } from '@variantjs/core'; 2 | import { InputHTMLAttributes } from 'vue'; 3 | import { ObjectWithProperties } from '../helpers'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/ban-types 6 | type TCheckboxSimpleValue = string | number | boolean | undefined | null | Date | Function | symbol; 7 | export type TCheckboxValue = TCheckboxSimpleValue | TCheckboxSimpleValue[] | ObjectWithProperties; 8 | 9 | export type TCheckboxOptions = WithVariantProps<{ 10 | modelValue?: TCheckboxValue 11 | } & InputHTMLAttributes & { 12 | type?: 'checkbox' 13 | } & Data>; 14 | -------------------------------------------------------------------------------- /src/types/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | type EmitterFunction = (...args : any[]) => void; 3 | 4 | type EmitterEvents = { 5 | [key: string]: EmitterFunction[] 6 | }; 7 | 8 | interface EmitterInterface { 9 | on(name: keyof EmitterEvents, callback: EmitterFunction): void; 10 | once(name: keyof EmitterEvents, callback: EmitterFunction): void; 11 | emit(name: keyof EmitterEvents, ...args: any[]): void; 12 | emit(name: keyof EmitterEvents, ...args: any[]): void; 13 | off(name: keyof EmitterEvents, callback: EmitterFunction): void; 14 | } 15 | 16 | export { EmitterEvents, EmitterFunction, EmitterInterface }; 17 | -------------------------------------------------------------------------------- /src/types/components/t-input-group.ts: -------------------------------------------------------------------------------- 1 | import { WithVariantPropsAndClassesList, TInputGroupClassesValidKeys, Data } from '@variantjs/core'; 2 | import { HTMLAttributes } from 'vue'; 3 | 4 | export type TInputGroupValidChilElementsKeys = ('label' | 'default' | 'feedback' | 'description')[]; 5 | 6 | export type TInputGroupOptions = WithVariantPropsAndClassesList<{ 7 | label?: string 8 | description?: string 9 | feedback?: string 10 | body?: string 11 | sortedElements?: TInputGroupValidChilElementsKeys, 12 | tagName?: string, 13 | bodyTagName?: string, 14 | labelTagName?: string, 15 | feedbackTagName?: string, 16 | descriptionTagName?: string, 17 | } & HTMLAttributes & Data, TInputGroupClassesValidKeys>; 18 | -------------------------------------------------------------------------------- /src/__tests/use/useVModel.spec.ts: -------------------------------------------------------------------------------- 1 | import useVModel from '../../use/useVModel'; 2 | import { useSetup } from './useSetup'; 3 | 4 | describe('useVModel', () => { 5 | const defaultValue = 'default'; 6 | 7 | it('should work with default value', () => { 8 | useSetup(() => { 9 | const data = useVModel({ 10 | modelValue: defaultValue, 11 | }, 'modelValue'); 12 | expect(data.value).toBe(defaultValue); 13 | }); 14 | }); 15 | 16 | it('should work with a different value from the default', () => { 17 | useSetup(() => { 18 | const data = useVModel({ 19 | otherValue: defaultValue, 20 | }, 'otherValue'); 21 | expect(data.value).toBe(defaultValue); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/types/components/t-select.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Data, InputOptions, NormalizedOption, NormalizedOptions, WithVariantProps, 3 | } from '@variantjs/core'; 4 | import { SelectHTMLAttributes } from 'vue'; 5 | import { Truthy } from '../misc'; 6 | 7 | // eslint-disable-next-line @typescript-eslint/ban-types 8 | export type TSelectValue = string | number | boolean | undefined | null | Date | Function | symbol | TSelectValue[]; 9 | 10 | export type TSelectOptions = WithVariantProps<{ 11 | modelValue?: TSelectValue, 12 | options?: InputOptions | NormalizedOption[] | NormalizedOptions, 13 | multiple?: Truthy, 14 | normalizeOptions?: boolean, 15 | valueAttribute?: string 16 | textAttribute?: string 17 | } & SelectHTMLAttributes & Data>; 18 | -------------------------------------------------------------------------------- /src/icons/CustomIcon.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /src/utils/popper.ts: -------------------------------------------------------------------------------- 1 | import { Modifier, ModifierArguments } from '@popperjs/core'; 2 | import { Data } from '@variantjs/core'; 3 | 4 | const sameWidthModifier: Modifier<'sameWidth', Data> = { 5 | name: 'sameWidth', 6 | enabled: true, 7 | phase: 'beforeWrite', 8 | requires: ['computeStyles'], 9 | fn: (options: ModifierArguments): void => { 10 | const { state } = options; 11 | state.styles.popper.width = `${state.rects.reference.width}px`; 12 | }, 13 | effect: (options: ModifierArguments): void => { 14 | const { state } = options; 15 | const reference = state.elements.reference as HTMLElement; 16 | state.elements.popper.style.width = `${reference.offsetWidth}px`; 17 | }, 18 | }; 19 | 20 | export { sameWidthModifier }; 21 | -------------------------------------------------------------------------------- /src/__tests/utils/getVariantProps.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable vue/one-component-per-file */ 2 | 3 | import { getVariantProps } from '../../utils/getVariantProps'; 4 | 5 | describe('getVariantProps()', () => { 6 | it('get the default variant props', () => { 7 | const props = getVariantProps(); 8 | expect(props).toEqual({ 9 | classes: { 10 | type: [String, Array, Object], 11 | default: undefined, 12 | }, 13 | fixedClasses: { 14 | type: [String, Array, Object], 15 | default: undefined, 16 | }, 17 | variants: { 18 | type: Object, 19 | default: undefined, 20 | }, 21 | variant: { 22 | type: String, 23 | default: undefined, 24 | }, 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/types/components/t-dropdown.ts: -------------------------------------------------------------------------------- 1 | import { Placement, Options } from '@popperjs/core'; 2 | import { WithVariantPropsAndClassesList, TDropdownClassesValidKeys, Data } from '@variantjs/core'; 3 | import { HTMLAttributes } from 'vue'; 4 | 5 | export type TDropdownOptions = WithVariantPropsAndClassesList<{ 6 | text?: string, 7 | disabled?: boolean, 8 | 9 | tagName?: string, 10 | dropdownTagName?: string, 11 | dropdownAttributes?: Data, 12 | 13 | toggleOnFocus?: boolean, 14 | toggleOnClick?: boolean, 15 | toggleOnHover?: boolean, 16 | 17 | show?: boolean, 18 | 19 | hideOnLeaveTimeout?: number, 20 | 21 | teleport?: boolean, 22 | teleportTo?: string | HTMLElement, 23 | 24 | placement?: Placement, 25 | popperOptions?: Options, 26 | } & HTMLAttributes & Data, TDropdownClassesValidKeys>; 27 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import typescript from '@rollup/plugin-typescript' 5 | 6 | export default defineConfig({ 7 | plugins: [vue()], 8 | build: { 9 | minify: false, 10 | sourcemap: true, 11 | lib: { 12 | entry: path.resolve(__dirname, 'src/index.ts'), 13 | name: 'VariantJS', 14 | fileName: (format) => `index.${format}.js` 15 | }, 16 | rollupOptions: { 17 | plugins: [ 18 | typescript({ 19 | "exclude": ["node_modules", 'src/__tests/**/*'] 20 | }), 21 | ], 22 | external: ['vue', '@popperjs/core', '@variantjs/core'], 23 | output: { 24 | globals: { 25 | vue: 'Vue' 26 | }, 27 | } 28 | } 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /src/types/components/t-modal.ts: -------------------------------------------------------------------------------- 1 | import { WithVariantPropsAndClassesList, Data, TModalClassesValidKeys } from '@variantjs/core'; 2 | import { BodyScrollOptions } from 'body-scroll-lock'; 3 | import { HTMLAttributes } from 'vue'; 4 | 5 | export type TModalOptions = WithVariantPropsAndClassesList<{ 6 | name?: string, 7 | modelValue?: boolean, 8 | modalAttributes?: HTMLAttributes & Data, 9 | tagName?: string 10 | body?: string 11 | header?: string 12 | footer?: string 13 | clickToClose?: boolean, 14 | escToClose?: boolean, 15 | focusOnOpen?: boolean, 16 | disableBodyScroll?: boolean, 17 | bodyScrollLockOptions?: BodyScrollOptions, 18 | teleport?: boolean, 19 | teleportTo?: string | HTMLElement, 20 | noBody?: boolean, 21 | hideCloseButton?: boolean, 22 | } & HTMLAttributes & Data, TModalClassesValidKeys>; 23 | -------------------------------------------------------------------------------- /src/types/misc.ts: -------------------------------------------------------------------------------- 1 | import { Data, InputOptions } from '@variantjs/core'; 2 | 3 | type Truthy = boolean | string; 4 | 5 | // eslint-disable-next-line @typescript-eslint/ban-types 6 | type IconProp = Element | string | (Data & { render?: Function }); 7 | 8 | type FetchedOptions = Promise<{ 9 | results: InputOptions; 10 | hasMorePages?: boolean; 11 | }>; 12 | 13 | type FetchOptionsFn = (query?: string, nextPage?: number) => FetchedOptions; 14 | 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | type PreFetchOptionsFn = (currentValue?: any) => Promise; 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | type PromiseRejectFn = ((reason?: any) => void); 20 | 21 | export { 22 | Truthy, IconProp, FetchOptionsFn, FetchedOptions, PromiseRejectFn, PreFetchOptionsFn, 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/TRichSelect/RichSelectClearButton.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 33 | -------------------------------------------------------------------------------- /src/components/misc/TextPlaceholder.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 36 | -------------------------------------------------------------------------------- /src/components/misc/Transitionable.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 36 | -------------------------------------------------------------------------------- /src/icons/LoadingIcon.vue: -------------------------------------------------------------------------------- 1 | 40 | -------------------------------------------------------------------------------- /src/__tests/components/TRichSelect/RichSelectTriggerTags.spec.ts: -------------------------------------------------------------------------------- 1 | import { NormalizedOption } from '@variantjs/core'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import { computed } from 'vue'; 4 | import RichSelectTriggerTags from '../../../components/TRichSelect/RichSelectTriggerTags.vue'; 5 | 6 | describe('RichSelectTriggerTags', () => { 7 | it('it renders every option', () => { 8 | const selectedOptions = computed(() => [ 9 | { value: 'a', text: 'Value A' }, 10 | { value: 'b', text: 'Value B' }, 11 | { value: 'c', text: 'Value C' }, 12 | ]); 13 | 14 | const wrapper = shallowMount(RichSelectTriggerTags, { 15 | global: { 16 | provide: { 17 | selectedOption: selectedOptions, 18 | }, 19 | }, 20 | }); 21 | 22 | expect(wrapper.findAll('rich-select-trigger-tags-tag-stub')).toHaveLength(3); 23 | expect(Object.keys(wrapper.find('rich-select-trigger-tags-tag-stub').attributes())).toEqual(['option']); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/components/TTag.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 44 | -------------------------------------------------------------------------------- /src/development/App.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 43 | -------------------------------------------------------------------------------- /src/use/useMultioptions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | flattenOptions, 3 | InputOptions, 4 | NormalizedOption, 5 | NormalizedOptions, 6 | normalizeOptions, 7 | } from '@variantjs/core'; 8 | import { computed, ComputedRef, Ref } from 'vue'; 9 | 10 | export default function useMultioptions( 11 | options: Ref, 12 | textAttribute: Ref, 13 | valueAttribute: Ref, 14 | normalize: Ref, 15 | ): { 16 | normalizedOptions: ComputedRef 17 | flattenedOptions: ComputedRef 18 | } { 19 | const normalizedOptions = computed(() => { 20 | if (!normalize.value) { 21 | return options.value as NormalizedOptions; 22 | } 23 | 24 | return normalizeOptions( 25 | options.value, 26 | textAttribute.value, 27 | valueAttribute.value, 28 | ); 29 | }); 30 | 31 | // Flattened array with all posible options 32 | const flattenedOptions = computed((): NormalizedOption[] => flattenOptions(normalizedOptions.value)); 33 | 34 | return { 35 | normalizedOptions, 36 | flattenedOptions, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/__tests/use/useInjectsClassesList.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import { defineComponent } from 'vue'; 3 | import useInjectsClassesList from '../../use/useInjectsClassesList'; 4 | 5 | describe('useInjectsClassesList', () => { 6 | const configurationToProvide = { 7 | classesList: { 8 | test: 'test', 9 | foo: 'bar', 10 | }, 11 | }; 12 | 13 | const component = defineComponent({ 14 | setup() { 15 | const classesList = useInjectsClassesList(); 16 | 17 | return { classesList }; 18 | }, 19 | template: '

', 20 | }); 21 | 22 | it('returns the provided configuration option', () => { 23 | const wrapper = shallowMount(component, { 24 | global: { 25 | provide: { 26 | configuration: configurationToProvide, 27 | }, 28 | }, 29 | }); 30 | 31 | expect(wrapper.vm.classesList).toEqual(configurationToProvide.classesList); 32 | }); 33 | 34 | it('returns empty object if classeslist are not provided', () => { 35 | const wrapper = shallowMount(component); 36 | 37 | expect(wrapper.vm.classesList).toEqual({}); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/__tests/utils/svgToVueComponent.spec.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from 'vue'; 2 | import { svgToVueComponent } from '../../utils/svgToVueComponent'; 3 | 4 | describe('svgToVueComponent', () => { 5 | const svg = ``; 16 | 17 | it('should return a Vnode from an SVG string', () => { 18 | const component: VNode = svgToVueComponent(svg); 19 | expect(component.type).toBe('svg'); 20 | 21 | expect(svgToVueComponent(svg).type).toBe('svg'); 22 | }); 23 | 24 | it('handle html element values', () => { 25 | const div = document.createElement('div'); 26 | div.innerHTML = 'Hello World'; 27 | 28 | const component: VNode = svgToVueComponent(div); 29 | 30 | expect(component.type).toBe('DIV'); 31 | }); 32 | 33 | it('handles invalid element values', () => { 34 | const component: VNode = svgToVueComponent('sfsd'); 35 | 36 | expect(component.type).toBe('span'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/__tests/use/useInjectsConfiguration.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import { defineComponent } from 'vue'; 3 | import useInjectsConfiguration from '../../use/useInjectsConfiguration'; 4 | 5 | describe('useInjectsConfiguration', () => { 6 | const configurationToProvide = { 7 | name: 'test', 8 | foo: 'bar', 9 | classesList: { 10 | test: 'test', 11 | }, 12 | }; 13 | 14 | const component = defineComponent({ 15 | setup() { 16 | const configuration = useInjectsConfiguration(); 17 | 18 | return { configuration }; 19 | }, 20 | template: '
', 21 | }); 22 | 23 | it('returns the provided configuration option', () => { 24 | const wrapper = shallowMount(component, { 25 | global: { 26 | provide: { 27 | configuration: configurationToProvide, 28 | }, 29 | }, 30 | }); 31 | 32 | expect(wrapper.vm.configuration).toEqual(configurationToProvide); 33 | }); 34 | 35 | it('returns the an empty configuration if no provide', () => { 36 | const wrapper = shallowMount(component); 37 | 38 | expect(wrapper.vm.configuration).toEqual({}); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "plugin:vue/vue3-recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "airbnb-typescript/base" 10 | ], 11 | "parser": "vue-eslint-parser", 12 | "parserOptions": { 13 | "ecmaVersion": 2018, 14 | "parser": "@typescript-eslint/parser", 15 | "sourceType": "module", 16 | "project": "tsconfig.json", 17 | "tsconfigRootDir": "./", 18 | "extraFileExtensions": [ ".vue" ] 19 | }, 20 | "plugins": [ 21 | "vue", 22 | "@typescript-eslint", 23 | "tree-shaking" 24 | ], 25 | "rules": { 26 | "max-len": "off", 27 | "import/extensions": "off", 28 | "import/prefer-default-export": "off", 29 | "import/no-extraneous-dependencies": "off", 30 | "@typescript-eslint/no-non-null-assertion": "off", 31 | "tree-shaking/no-side-effects-in-initialization": 2 32 | }, 33 | "overrides": [ 34 | { 35 | "files": ["*.spec.ts", "src/main.ts", "src/development/*"], 36 | "rules": { 37 | "tree-shaking/no-side-effects-in-initialization": "off" 38 | } 39 | } 40 | ] 41 | } 42 | 43 | -------------------------------------------------------------------------------- /src/components/TRadio.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 39 | -------------------------------------------------------------------------------- /src/development/AppMenu.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 53 | -------------------------------------------------------------------------------- /src/__tests/testUtils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import { Data } from '@variantjs/core'; 4 | 5 | export const scopedParamsAsString = (params: Data) : string => { 6 | const keys = Object.keys(params); 7 | const result: Data = {}; 8 | keys.filter((key) => key !== 'key').forEach((key) => { 9 | result[key] = typeof params[key]; 10 | }); 11 | return JSON.stringify(result); 12 | }; 13 | 14 | export const parseScopedParams = (paramsAsString: string) : Data => JSON.parse(paramsAsString); 15 | 16 | export const getChildComponentNameByRef = (wrapper: any, refName: string): string | undefined => { 17 | const component = wrapper.vm.$refs[refName]; 18 | 19 | return component?.$?.type.name; 20 | }; 21 | 22 | export const componentHasAttributeWithValue = (component: any, attributeName: string, attributeValue: any): boolean => component.$.attrs[attributeName] === attributeValue; 23 | 24 | export const componentHasAttributeWithInlineHandlerAndParameter = (component: any, attributeName: string, parameterName: any): boolean => { 25 | const functionAsString: string = component.$.attrs[attributeName].toString(); 26 | return functionAsString.includes(parameterName); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/TCheckbox.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 44 | -------------------------------------------------------------------------------- /src/components/TSelect/TSelectOption.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 45 | -------------------------------------------------------------------------------- /src/development/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router'; 2 | 3 | import Home from './Home.vue'; 4 | import About from './About.vue'; 5 | import Options from './Options.vue'; 6 | import Multioptions from './Multioptions.vue'; 7 | import Dropdown from './Dropdown.vue'; 8 | import Alert from './Alert.vue'; 9 | import Modal from './Modal.vue'; 10 | import Dialog from './Dialog.vue'; 11 | import Checkbox from './Check.vue'; 12 | import Theme from './Theme.vue'; 13 | import RichSelect from './RichSelect.vue'; 14 | import Attributes from './Attributes.vue'; 15 | 16 | const routes = [ 17 | { path: '/', component: Home }, 18 | { path: '/about', component: About }, 19 | { path: '/options', component: Options }, 20 | { path: '/multioptions', component: Multioptions }, 21 | { path: '/dropdown', component: Dropdown }, 22 | { path: '/alert', component: Alert }, 23 | { path: '/modal', component: Modal }, 24 | { path: '/dialog', component: Dialog }, 25 | { path: '/theme', component: Theme }, 26 | { path: '/attributes', component: Attributes }, 27 | { path: '/checkbox', component: Checkbox }, 28 | { path: '/rich-select', component: RichSelect }, 29 | ]; 30 | 31 | const router = createRouter({ 32 | history: createWebHashHistory(), 33 | routes, 34 | }); 35 | 36 | export default router; 37 | -------------------------------------------------------------------------------- /src/use/useConfigurationWithClassesList.ts: -------------------------------------------------------------------------------- 1 | import { 2 | computed, getCurrentInstance, reactive, watch, 3 | } from 'vue'; 4 | import { Data, parseVariantWithClassesList } from '@variantjs/core'; 5 | import { useAttributes, useConfigurationParts } from './useConfiguration'; 6 | 7 | export default function useConfigurationWithClassesList(defaultConfiguration: ComponentOptions, classesListKeys: string[]): { 8 | configuration: ComponentOptions, 9 | attributes: Data, 10 | } { 11 | const vm = getCurrentInstance()!; 12 | 13 | const { propsValues, componentGlobalConfiguration } = useConfigurationParts(); 14 | 15 | const computedConfiguration = computed(() => ({ 16 | ...vm.props, 17 | ...parseVariantWithClassesList( 18 | propsValues.value, 19 | classesListKeys, 20 | componentGlobalConfiguration, 21 | defaultConfiguration, 22 | ), 23 | })); 24 | 25 | const configuration = reactive(computedConfiguration.value); 26 | 27 | watch(computedConfiguration, (newValue) => { 28 | Object.keys(newValue).forEach((key) => { 29 | configuration[key] = newValue[key]; 30 | }); 31 | }); 32 | 33 | const attributes = useAttributes(configuration); 34 | 35 | return { 36 | configuration: configuration as ComponentOptions, 37 | attributes, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/__tests/utils/popper.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import { ModifierArguments } from '@popperjs/core'; 4 | import { Data } from '@variantjs/core'; 5 | import { sameWidthModifier } from '../../utils/popper'; 6 | 7 | describe('sameWidthModifier', () => { 8 | it('sets the popper width from the reference width', () => { 9 | const options = { 10 | state: { 11 | styles: { 12 | popper: {}, 13 | }, 14 | rects: { 15 | reference: { 16 | width: 100, 17 | }, 18 | }, 19 | }, 20 | 21 | } as any as ModifierArguments; 22 | 23 | sameWidthModifier.fn(options); 24 | 25 | expect(options.state.styles.popper.width).toBe('100px'); 26 | }); 27 | 28 | it('sets the popper width from the reference width on `effect` function', () => { 29 | const options = { 30 | state: { 31 | elements: { 32 | popper: { 33 | style: {}, 34 | }, 35 | reference: { 36 | offsetWidth: 100, 37 | }, 38 | }, 39 | }, 40 | 41 | } as any as ModifierArguments; 42 | 43 | sameWidthModifier.effect!(options); 44 | 45 | expect(options.state.elements.popper.style.width).toBe('100px'); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/use/useMulipleableVModel.ts: -------------------------------------------------------------------------------- 1 | import { Data } from '@variantjs/core'; 2 | import { 3 | computed, ref, getCurrentInstance, Ref, watch, 4 | } from 'vue'; 5 | 6 | export default function useMulipleableVModel

( 7 | props: P, 8 | key: K, 9 | configuration?: C, 10 | ): { 11 | localValue: Ref; 12 | clearValue: () => void 13 | } { 14 | const vm = getCurrentInstance(); 15 | 16 | const isMultiple = computed((): boolean => (configuration === undefined ? false : configuration.multiple !== null && configuration.multiple !== undefined && configuration.multiple !== false)); 17 | 18 | const getDefaultValue = (): P[K] => { 19 | if (isMultiple.value) { 20 | return [] as P[K]; 21 | } 22 | 23 | return undefined as P[K]; 24 | }; 25 | 26 | const initialValue = props[key]; 27 | 28 | const localValue = ref(initialValue === undefined ? getDefaultValue() : initialValue) as Ref; 29 | 30 | watch(localValue, (value) => { 31 | vm?.emit(`update:${key}`, value); 32 | }); 33 | 34 | watch(() => props[key], (value) => { 35 | localValue.value = value; 36 | }); 37 | 38 | const clearValue = () : void => { 39 | localValue.value = getDefaultValue(); 40 | }; 41 | 42 | watch(isMultiple, () => { 43 | clearValue(); 44 | }); 45 | 46 | return { 47 | localValue, 48 | clearValue, 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /src/components/TInput.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 50 | -------------------------------------------------------------------------------- /src/components/TTextarea.vue: -------------------------------------------------------------------------------- 1 |