├── .gitattributes ├── .husky ├── pre-commit └── pre-push ├── packages ├── cli │ ├── src │ │ └── index.ts │ ├── build.config.ts │ └── package.json └── docs │ ├── public │ ├── robots.txt │ ├── _headers │ ├── favicon.ico │ ├── favicon-dark.ico │ ├── book-texture.avif │ └── favicon-light.ico │ ├── src │ ├── consts │ │ ├── link.ts │ │ └── components.ts │ ├── components │ │ ├── demos │ │ │ ├── Static.vue │ │ │ └── Counter.vue │ │ ├── OverviewCard.vue │ │ ├── SiteLogo.vue │ │ ├── Menus.vue │ │ └── SiteFooter.vue │ ├── layouts │ │ └── default.vue │ ├── main.ts │ ├── pages │ │ ├── components │ │ │ ├── browser.md │ │ │ ├── pagination.md │ │ │ ├── description.md │ │ │ ├── spinner.md │ │ │ ├── backtop.md │ │ │ ├── kbd.md │ │ │ ├── loading-dots.md │ │ │ ├── placeholder.md │ │ │ ├── more-button.md │ │ │ ├── teleport.md │ │ │ ├── command-menu.md │ │ │ ├── dash-line.md │ │ │ ├── material.md │ │ │ ├── theme-switcher.md │ │ │ ├── overlay.md │ │ │ ├── error.md │ │ │ ├── status-dot.md │ │ │ ├── virtual-list.md │ │ │ ├── chip.md │ │ │ ├── fader.md │ │ │ ├── radio.md │ │ │ ├── config-provider.md │ │ │ ├── empty-state.md │ │ │ ├── link-button.md │ │ │ ├── time-picker.md │ │ │ ├── snippet.md │ │ │ ├── pin-input.md │ │ │ ├── stack.md │ │ │ ├── index.vue │ │ │ ├── number-input.md │ │ │ ├── skeleton.md │ │ │ ├── button.md │ │ │ ├── progress.md │ │ │ ├── resizable.md │ │ │ ├── text.md │ │ │ ├── badge.md │ │ │ ├── textarea.md │ │ │ └── avatar.md │ │ ├── [...all].vue │ │ └── guide │ │ │ ├── why.md │ │ │ ├── faq.md │ │ │ ├── problems.md │ │ │ └── introduction.md │ ├── App.vue │ ├── styles │ │ └── index.css │ └── router │ │ └── index.ts │ ├── tsconfig.json │ ├── package.json │ └── scripts │ └── vite-plugin-file-create-watcher.ts ├── src ├── utils │ ├── debounce.ts │ ├── throttle.ts │ ├── uid.ts │ ├── regexp.ts │ ├── responsive.ts │ ├── ref.ts │ ├── is.ts │ └── context.ts ├── types │ ├── shared │ │ ├── index.d.ts │ │ ├── utils.d.ts │ │ └── props.d.ts │ └── components │ │ ├── popover.d.ts │ │ ├── time-picker.ts │ │ ├── error.d.ts │ │ ├── avatar.d.ts │ │ ├── switch.d.ts │ │ ├── radio.d.ts │ │ ├── button.d.ts │ │ ├── list.d.ts │ │ ├── carousel.d.ts │ │ ├── checkbox.d.ts │ │ ├── choicebox.d.ts │ │ └── input.d.ts ├── locales │ ├── index.ts │ ├── zh-cn.ts │ └── en-us.ts ├── styles │ └── styles.css ├── contexts │ ├── radio.ts │ ├── avatar.ts │ ├── checkbox.ts │ ├── collapse.ts │ ├── switch.ts │ ├── list.ts │ ├── choicebox.ts │ ├── carousel.ts │ └── resizable.ts ├── components │ ├── command-menu-item │ │ └── index.vue │ ├── keep-alive-container │ │ └── index.vue │ ├── command-menu-group │ │ └── index.vue │ ├── config-provider │ │ └── index.vue │ ├── spinner │ │ └── index.vue │ ├── description │ │ └── index.vue │ ├── virtual-list │ │ └── index.vue │ ├── theme-switcher │ │ └── index.vue │ ├── loading-dots │ │ └── index.vue │ ├── placeholder │ │ └── index.vue │ ├── empty-state │ │ └── index.vue │ ├── status-dot │ │ └── index.vue │ ├── avatar-group │ │ └── index.vue │ ├── radio-group │ │ └── index.vue │ ├── kbd │ │ └── index.vue │ ├── carousel │ │ └── index.vue │ ├── material │ │ └── index.vue │ ├── checkbox-group │ │ └── index.vue │ ├── countdown │ │ └── index.vue │ ├── switch-group │ │ └── index.vue │ ├── chip │ │ └── index.vue │ ├── more-button │ │ └── index.vue │ ├── error │ │ └── index.vue │ ├── resizable-panel │ │ └── index.vue │ ├── collapse-group │ │ └── index.vue │ ├── pagination │ │ └── index.vue │ ├── grid-item │ │ └── index.vue │ ├── choicebox │ │ └── index.vue │ ├── dash-line │ │ └── index.vue │ ├── menu │ │ └── index.vue │ ├── skeleton │ │ └── index.vue │ ├── switch │ │ └── index.vue │ └── choicebox-group │ │ └── index.vue ├── index.ts ├── composables │ ├── use-unique-id-context.ts │ ├── use-config-provider-context.ts │ ├── use-model-value.ts │ ├── index.ts │ ├── use-delay-change.ts │ ├── use-copy-click.ts │ ├── use-deferred-value.ts │ ├── use-outside-click.ts │ ├── use-loading-bar.ts │ ├── use-delay-destroy.ts │ ├── use-repeat-action.ts │ └── use-media-query.ts └── plugins │ ├── resolver.ts │ └── dayjs-millisecond-token.ts ├── .npmrc ├── netlify.toml ├── .editorconfig ├── tsconfig.json ├── tsconfig.app.json ├── tsconfig.test.json ├── env.d.ts ├── scripts ├── utils.js ├── gen-css-files.js └── gen-component-dts.js ├── tests ├── components │ ├── config-provider.test.ts │ ├── description.test.ts │ ├── progress.test.ts │ ├── kbd.test.ts │ ├── theme-switcher.test.ts │ ├── avatar-group.test.ts │ ├── active-graph.test.ts │ ├── material.test.ts │ ├── gauge.test.ts │ ├── note.test.ts │ ├── loading-dots.test.ts │ ├── spinner.test.ts │ ├── badge.test.ts │ ├── link-button.test.ts │ ├── avatar.test.ts │ ├── button.test.ts │ ├── textarea.test.ts │ ├── checkbox.test.ts │ ├── pagination.test.ts │ ├── error.test.ts │ └── toggle.test.ts └── helpers │ ├── provide-inject.ts │ └── setup.ts ├── tsconfig.node.json ├── .gitignore ├── README-CN.md ├── vitest.setup.ts ├── vitest.config.ts ├── README.md ├── eslint.config.ts ├── pnpm-workspace.yaml └── .vscode └── settings.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx --no-install lint-staged 2 | -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | export const one = 1 2 | -------------------------------------------------------------------------------- /src/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | export * from './debounce/index' 2 | -------------------------------------------------------------------------------- /src/utils/throttle.ts: -------------------------------------------------------------------------------- 1 | export * from './throttle/index' 2 | -------------------------------------------------------------------------------- /packages/docs/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | pnpm run update-exports 2 | pnpm run typecheck 3 | pnpm run test 4 | -------------------------------------------------------------------------------- /src/types/shared/index.d.ts: -------------------------------------------------------------------------------- 1 | export type * from './props' 2 | export type * from './utils' 3 | -------------------------------------------------------------------------------- /packages/docs/src/consts/link.ts: -------------------------------------------------------------------------------- 1 | export const githubLink = 'https://github.com/libondev/pxd' 2 | -------------------------------------------------------------------------------- /src/utils/uid.ts: -------------------------------------------------------------------------------- 1 | let _id = 0 2 | 3 | export function getUniqueId() { 4 | return `_pid_${_id++}` 5 | } 6 | -------------------------------------------------------------------------------- /packages/docs/public/_headers: -------------------------------------------------------------------------------- 1 | /assets/* 2 | cache-control: max-age=31536000 3 | cache-control: immutable 4 | -------------------------------------------------------------------------------- /packages/docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libondev/pxd/HEAD/packages/docs/public/favicon.ico -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | export { default as enUS } from './en-us.js' 2 | export { default as zhCN } from './zh-cn.js' 3 | -------------------------------------------------------------------------------- /packages/docs/public/favicon-dark.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libondev/pxd/HEAD/packages/docs/public/favicon-dark.ico -------------------------------------------------------------------------------- /src/styles/styles.css: -------------------------------------------------------------------------------- 1 | /* ‼️ DO NOT MODIFY THIS FILE, It will automatically update the content when it is published. */ 2 | -------------------------------------------------------------------------------- /src/types/components/popover.d.ts: -------------------------------------------------------------------------------- 1 | export type PopoverTrigger = 'click' | 'hover' | 'focus' | 'contextmenu' | 'manual' 2 | -------------------------------------------------------------------------------- /packages/docs/public/book-texture.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libondev/pxd/HEAD/packages/docs/public/book-texture.avif -------------------------------------------------------------------------------- /packages/docs/public/favicon-light.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libondev/pxd/HEAD/packages/docs/public/favicon-light.ico -------------------------------------------------------------------------------- /src/types/components/time-picker.ts: -------------------------------------------------------------------------------- 1 | export interface DateTimePreset { 2 | label: string 3 | getDate: () => T 4 | } 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | prefer-frozen-lockfile=true 3 | shared-workspace-lockfile=true 4 | strict-peer-dependencies=false 5 | -------------------------------------------------------------------------------- /src/types/components/error.d.ts: -------------------------------------------------------------------------------- 1 | export interface ErrorType { 2 | message: string 3 | action?: string 4 | link?: string 5 | label?: string 6 | } 7 | -------------------------------------------------------------------------------- /packages/docs/src/components/demos/Static.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "./" 3 | publish = "./packages/docs/dist" 4 | command = "pnpm -w run build:only" 5 | 6 | [build.environment] 7 | NODE_VERSION = "22" 8 | 9 | [[redirects]] 10 | from = "/*" 11 | to = "/index.html" 12 | status = 200 13 | -------------------------------------------------------------------------------- /src/types/components/avatar.d.ts: -------------------------------------------------------------------------------- 1 | interface Options { 2 | src?: string 3 | alt?: string 4 | loading?: boolean 5 | } 6 | 7 | export interface AvatarGroupProps { 8 | max?: number 9 | size?: number | string 10 | options?: Options[] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}] 4 | charset = utf-8 5 | indent_size = 2 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | quote_type = single 10 | end_of_line = lf 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "./tsconfig.app.json" 5 | }, 6 | { 7 | "path": "./tsconfig.node.json" 8 | }, 9 | { 10 | "path": "./tsconfig.test.json" 11 | } 12 | ], 13 | "files": [] 14 | } 15 | -------------------------------------------------------------------------------- /src/contexts/radio.ts: -------------------------------------------------------------------------------- 1 | import type { RadioGroupProps } from '../types/components/radio' 2 | import { createContext } from '../utils/context' 3 | 4 | export const [ 5 | provideRadioGroupContext, 6 | useRadioGroupContext, 7 | ] = createContext('RadioGroup', null) 8 | -------------------------------------------------------------------------------- /src/contexts/avatar.ts: -------------------------------------------------------------------------------- 1 | import type { AvatarGroupProps } from '../types/components/avatar' 2 | import { createContext } from '../utils/context' 3 | 4 | export const [ 5 | provideAvatarGroupContext, 6 | useAvatarGroupContext, 7 | ] = createContext('AvatarGroup', null) 8 | -------------------------------------------------------------------------------- /src/contexts/checkbox.ts: -------------------------------------------------------------------------------- 1 | import type { CheckboxGroupProps } from '../types/components/checkbox' 2 | import { createContext } from '../utils/context' 3 | 4 | export const [ 5 | provideCheckboxGroupContext, 6 | useCheckboxGroupContext, 7 | ] = createContext('CheckboxGroup', null) 8 | -------------------------------------------------------------------------------- /packages/docs/src/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /src/types/components/switch.d.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentOption, ComponentSize } from '../shared' 2 | 3 | export interface SwitchGroupProps { 4 | block?: boolean 5 | disabled?: boolean 6 | required?: boolean 7 | size?: ComponentSize 8 | options?: ComponentOption[] 9 | modelValue?: string | number 10 | } 11 | -------------------------------------------------------------------------------- /src/components/command-menu-item/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /packages/docs/src/components/demos/Counter.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /packages/docs/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createHead } from '@unhead/vue/client' 2 | import { createApp } from 'vue' 3 | import App from './App.vue' 4 | import router from './router' 5 | 6 | import './styles/index.css' 7 | 8 | const app = createApp(App) 9 | const head = createHead() 10 | 11 | app.use(head) 12 | app.use(router) 13 | 14 | app.mount('#app') 15 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/browser.md: -------------------------------------------------------------------------------- 1 | # Browser 2 | The Browser component lets you showcase website screenshots or any other content within a realistic browser-style frame. 3 | 4 | ## Composition 5 | 6 | ```vue demo 7 | 12 | ``` 13 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "baseUrl": "./", 6 | "rootDir": "./", 7 | "types": ["vite/client"] 8 | }, 9 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 10 | "exclude": ["tests/**/*.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/cli/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | clean: true, 5 | declaration: true, 6 | entries: [ 7 | { 8 | builder: 'mkdist', 9 | input: './src', 10 | pattern: ['**/*.ts'], 11 | format: 'esm', 12 | loaders: ['js'], 13 | ext: 'js', 14 | }, 15 | ], 16 | }) 17 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", 5 | "lib": ["DOM"], 6 | "baseUrl": "./", 7 | "rootDir": "./", 8 | "types": ["node", "vitest/jsdom"], 9 | "skipLibCheck": true 10 | }, 11 | "include": ["tests/**/*.test.ts", "env.d.ts"], 12 | "exclude": [] 13 | } 14 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/pagination.md: -------------------------------------------------------------------------------- 1 | # Pagination 2 | Navigate to the previous or next page. 3 | 4 | ## Default 5 | 6 | ```vue demo 7 | 18 | 19 | 22 | ``` 23 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | 6 | const component: DefineComponent, Record, any> 7 | export default component 8 | } 9 | 10 | declare module '*.md' { 11 | import type { ComponentOptions } from 'vue' 12 | 13 | const Component: ComponentOptions 14 | export default Component 15 | } 16 | -------------------------------------------------------------------------------- /src/contexts/collapse.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { createContext } from '../utils/context' 3 | 4 | interface CollapseGroupContext { 5 | multiple: Ref 6 | isExpanded: (id: string) => boolean 7 | toggleItem: (id: string, expanded: boolean) => void 8 | } 9 | 10 | export const [ 11 | provideCollapseGroupContext, 12 | useCollapseGroupContext, 13 | ] = createContext('CollapseGroup') 14 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/description.md: -------------------------------------------------------------------------------- 1 | # Description 2 | Displays a brief heading and subheading to communicate any additional information or context a user needs to continue. 3 | 4 | ## Default 5 | 6 | ```vue demo 7 | 14 | ``` 15 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import * as components from './components/index' 3 | 4 | export * from './components/index.js' 5 | export * from './composables/index.js' 6 | export type * from './types/shared' 7 | 8 | export const version = '0.0.44' 9 | 10 | export default function install(app: App, prefix = 'P') { 11 | Object.entries(components).forEach(([key, component]) => { 12 | app.component(`${prefix}${key}`, component) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/types/components/radio.d.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentLabel, ComponentOption, ComponentValue } from '../shared' 2 | 3 | export interface RadioGroupProps { 4 | disabled?: boolean 5 | required?: boolean 6 | modelValue?: ComponentValue 7 | options?: ComponentOption[] 8 | } 9 | 10 | export interface RadioProps { 11 | label?: ComponentLabel 12 | value: ComponentValue 13 | required?: boolean 14 | disabled?: boolean 15 | modelValue?: ComponentValue 16 | } 17 | -------------------------------------------------------------------------------- /src/types/components/button.d.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentAs, ComponentSizeWithXs, ComponentVariantWithDefault } from '../shared' 2 | 3 | export interface ButtonProps { 4 | as?: ComponentAs 5 | variant?: ComponentVariantWithDefault | 'ghost' | 'simple' | 'icon' 6 | size?: ComponentSizeWithXs 7 | shape?: 'square' | 'rounded' 8 | align?: 'left' | 'center' | 'right' 9 | icon?: boolean 10 | block?: boolean 11 | loading?: boolean 12 | disabled?: boolean 13 | } 14 | -------------------------------------------------------------------------------- /packages/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "@/*": ["./src/*"] 6 | }, 7 | "types": ["vite/client", "unplugin-vue-router/client", "vite-plugin-vue-meta-layouts/client"] 8 | }, 9 | "include": [ 10 | "../../env.d.ts", 11 | "shims/*.d.ts", 12 | "src/**/*.ts", 13 | "src/**/*.tsx", 14 | "src/**/*.vue" 15 | ], 16 | "exclude": ["node_modules", "dist"] 17 | } 18 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | import { camelize } from '@vue/shared' 2 | 3 | /** 4 | * @param {string} name 5 | * @returns {string} 转换后的字符串 6 | */ 7 | export function pascalize(name) { 8 | const camelized = camelize(name) 9 | return camelized.charAt(0).toUpperCase() + camelized.slice(1) 10 | } 11 | 12 | // 'kabab-case' -> 'Kabab Case' 13 | export function humanize(name) { 14 | return name 15 | .replace(/-/g, ' ') 16 | .replace(/\b\w/g, char => char.toUpperCase()) 17 | } 18 | -------------------------------------------------------------------------------- /src/components/keep-alive-container/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 12 | -------------------------------------------------------------------------------- /src/types/components/list.d.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentAs, ComponentLabel } from '../shared' 2 | 3 | export interface ListOption extends Record { 4 | as?: ComponentAs 5 | type?: 'default' | 'error' | 'warning' | 'separator' 6 | label?: ComponentLabel 7 | disabled?: boolean 8 | description?: ComponentLabel 9 | onClick?: (ev: MouseEvent, item: ListOptionSelected) => void 10 | } 11 | 12 | export type ListOptionSelected = Omit 13 | -------------------------------------------------------------------------------- /packages/docs/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /tests/components/config-provider.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { describe, expect, it } from 'vitest' 3 | import ConfigProvider from '../../src/components/config-provider/index.vue' 4 | 5 | describe('config-provider', () => { 6 | it('should render properly', () => { 7 | const wrapper = mount(ConfigProvider) 8 | 9 | expect(wrapper.html()).toMatchInlineSnapshot(`"
"`) 10 | 11 | wrapper.unmount() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/contexts/switch.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import type { SwitchGroupProps } from './../types/components/switch' 3 | import { createContext } from '../utils/context' 4 | 5 | export const [ 6 | provideSwitchGroupContext, 7 | useSwitchGroupContext, 8 | ] = createContext('SwitchGroup') 9 | 10 | export const [ 11 | provideSwitchGroupModelValue, 12 | useSwitchGroupModelValue, 13 | ] = createContext>('SwitchGroupModalValue') 14 | -------------------------------------------------------------------------------- /src/types/components/carousel.d.ts: -------------------------------------------------------------------------------- 1 | import type { BasePosition, ComponentDirection } from '../shared' 2 | 3 | export interface CarouselGroupProps { 4 | index?: number 5 | loop?: boolean 6 | arrow?: boolean 7 | height?: number | string 8 | autoplay?: boolean 9 | interval?: number 10 | indicator?: boolean 11 | direction?: ComponentDirection 12 | indicatorType?: 'dot' | 'line' 13 | indicatorPosition?: BasePosition 14 | pauseOnHover?: boolean 15 | toggleOnWheel?: boolean 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/regexp.ts: -------------------------------------------------------------------------------- 1 | export const INTEGER_REGEX = /^-?\d+$/ 2 | export const FLOATING_REGEX = /^-?\d+\.?\d*/ 3 | export const POSITIVE_INTEGER_REGEX = /^\d+$/ 4 | export const SCIENCE_NUMERIC_REGEX = /^-?\d+(?:\.\d*)?(e-?\d+)?$/i 5 | export const DATE_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/ 6 | export const TIME_REGEX = /^(\d{2}):(\d{2}):(\d{2})$/ 7 | export const DATE_TIME_REGEX = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/ 8 | export const TIME_REGEX_WITH_MILLISECONDS = /^(\d{2}):(\d{2}):(\d{2})\.(\d{1,3})$/ 9 | -------------------------------------------------------------------------------- /src/composables/use-unique-id-context.ts: -------------------------------------------------------------------------------- 1 | import { inject, provide } from 'vue' 2 | import { getUniqueId } from '../utils/uid' 3 | 4 | export function provideUniqueId(injectionKey: string = 'uniqueId'): string { 5 | const uniqueId = getUniqueId() 6 | 7 | provide(injectionKey, uniqueId) 8 | 9 | return uniqueId 10 | } 11 | 12 | export function useUniqueId(injectionKey: string = 'uniqueId'): string { 13 | const injectedValue = inject(injectionKey, getUniqueId()) 14 | 15 | return injectedValue 16 | } 17 | -------------------------------------------------------------------------------- /src/types/components/checkbox.d.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentLabel, ComponentOption, ComponentValue } from '../shared' 2 | 3 | export interface CheckboxGroupProps { 4 | disabled?: boolean 5 | required?: boolean 6 | modelValue?: ComponentValue[] 7 | options?: ComponentOption[] 8 | } 9 | 10 | export interface CheckboxProps { 11 | label?: ComponentLabel 12 | value?: ComponentValue 13 | disabled?: boolean 14 | required?: boolean 15 | modelValue?: ComponentValue | ComponentValue[] 16 | indeterminate?: boolean 17 | } 18 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/spinner.md: -------------------------------------------------------------------------------- 1 | # Spinner 2 | 3 | Indicate an action running in the background. Unlike the loading dots, this should generally be used to indicate loading feedback in response to a user action, like for buttons, pagination, etc. 4 | 5 | ## Default 6 | 7 | ```vue demo 8 | 11 | ``` 12 | 13 | ## Sizes 14 | 15 | The size of `spinner` is determined by the `font-size`. 16 | 17 | ```vue demo 18 | 21 | ``` 22 | -------------------------------------------------------------------------------- /src/contexts/list.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import type { ListOption } from '../types/components/list' 3 | import { createContext } from '../utils/context' 4 | 5 | export interface ListContext { 6 | activeValue: Ref 7 | onOptionClick: ListOption['onClick'] 8 | } 9 | 10 | export const [ 11 | provideListContext, 12 | useListContext, 13 | ] = createContext('List') 14 | 15 | export const [ 16 | provideListFilterValue, 17 | useListFilterValue, 18 | ] = createContext>('ListFilterValue', null) 19 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "baseUrl": "./", 6 | "rootDir": "./", 7 | "module": "ESNext", 8 | "moduleResolution": "Bundler", 9 | "types": ["node"], 10 | "noEmit": true 11 | }, 12 | "include": [ 13 | "vite.config.*", 14 | "vitest.config.*", 15 | "cypress.config.*", 16 | "nightwatch.conf.*", 17 | "playwright.config.*", 18 | "eslint.config.*" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/contexts/choicebox.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import type { ChoiceboxGroupProps } from '../types/components/choicebox' 3 | import { createContext } from '../utils/context' 4 | 5 | export const [ 6 | provideChoiceboxGroupContext, 7 | useChoiceboxGroupContext, 8 | ] = createContext('ChoiceboxGroup', { 9 | multiple: false, 10 | }) 11 | 12 | export const [ 13 | provideChoiceboxGroupModelValue, 14 | useChoiceboxGroupModelValue, 15 | ] = createContext>('ChoiceboxGroupModalValue') 16 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/backtop.md: -------------------------------------------------------------------------------- 1 | # Backtop 2 | A button to back to top. 3 | 4 | ## Default 5 | 6 | ```vue demo 7 | 12 | ``` 13 | 14 | ## Customizations 15 | 16 | ```vue demo 17 | 24 | ``` 25 | -------------------------------------------------------------------------------- /src/composables/use-config-provider-context.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentSize } from '../types/shared' 2 | import enUS from '../locales/en-us' 3 | import { createContext } from '../utils/context' 4 | 5 | export const injectionKey = 'ConfigProvider' 6 | 7 | export interface ConfigProviderProps { 8 | size?: ComponentSize 9 | locale?: Record 10 | } 11 | 12 | export const [ 13 | provideConfigProvider, 14 | useConfigProvider, 15 | ] = createContext>( 16 | injectionKey, 17 | { 18 | size: 'md', 19 | locale: enUS, 20 | }, 21 | ) 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | shims/components.d.ts 17 | 18 | /cypress/videos/ 19 | /cypress/screenshots/ 20 | 21 | # Editor directories and files 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | .nuxt 31 | components.d.ts 32 | auto-imports.d.ts 33 | typed-router.d.ts 34 | **/pnpm-lock.yaml 35 | .vite-ssg-temp 36 | 37 | src/styles/styles.css 38 | -------------------------------------------------------------------------------- /src/components/command-menu-group/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 24 | -------------------------------------------------------------------------------- /src/types/components/choicebox.d.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentLabel, ComponentOption, ComponentValue } from '../shared' 2 | 3 | interface Option extends ComponentOption { 4 | description?: string 5 | } 6 | 7 | export interface ChoiceboxGroupProps { 8 | label?: ComponentLabel 9 | multiple?: boolean 10 | required?: boolean 11 | disabled?: boolean 12 | options?: Option[] 13 | modelValue?: ComponentValue | ComponentValue[] 14 | } 15 | 16 | export interface ChoiceboxProps { 17 | label?: ComponentLabel 18 | value?: ComponentValue 19 | disabled?: boolean 20 | required?: boolean 21 | description?: string 22 | } 23 | -------------------------------------------------------------------------------- /tests/components/description.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { describe, expect, it } from 'vitest' 3 | import Description from '../../src/components/description/index.vue' 4 | 5 | describe('description', () => { 6 | it('should render properly', () => { 7 | const wrapper = mount(Description, { 8 | props: { 9 | title: 'Title', 10 | content: 'Content', 11 | }, 12 | }) 13 | 14 | expect(wrapper.find('.pxd-description--title').text()).toBe('Title') 15 | expect(wrapper.find('.pxd-description--content').text()).toBe('Content') 16 | 17 | wrapper.unmount() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/kbd.md: -------------------------------------------------------------------------------- 1 | # Kbd 2 | Display keyboard input that triggers an action. 3 | 4 | ## Modifiers 5 | 6 | ```vue demo 7 | 15 | ``` 16 | 17 | ## Combination 18 | 19 | ```vue demo 20 | 23 | ``` 24 | 25 | ## Sizes 26 | 27 | ```vue demo 28 | 35 | ``` 36 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/loading-dots.md: -------------------------------------------------------------------------------- 1 | # Loading Dots 2 | Indicate an action running in the background. 3 | 4 | ## Default 5 | 6 | ```vue demo 7 | 13 | ``` 14 | 15 | ## With prefix and suffix 16 | 17 | ```vue demo 18 | 28 | ``` 29 | -------------------------------------------------------------------------------- /tests/components/progress.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { describe, expect, it } from 'vitest' 3 | import Progress from '../../src/components/progress/index.vue' 4 | 5 | describe('progress', () => { 6 | it('renders properly', () => { 7 | const wrapper = mount(Progress, { 8 | props: { 9 | modelValue: 30, 10 | max: 80, 11 | min: 20, 12 | }, 13 | }) 14 | 15 | expect(wrapper.attributes('aria-valuemin')).toBe('20') 16 | expect(wrapper.attributes('aria-valuemax')).toBe('80') 17 | expect(wrapper.attributes('aria-valuenow')).toBe('30') 18 | 19 | wrapper.unmount() 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/utils/responsive.ts: -------------------------------------------------------------------------------- 1 | import type { ResponsiveValue } from '../types/shared/props' 2 | import type { Nullable } from '../types/shared/utils' 3 | 4 | export function getResponsiveValue( 5 | prop: ResponsiveValue | undefined, 6 | xsValue: Nullable, 7 | valueSetter: (acc: Record, bp: any, v: V) => void, 8 | ) { 9 | const formatted = Object.assign( 10 | xsValue ? { xs: xsValue } : {}, 11 | typeof prop === 'object' ? prop : {}, 12 | ) 13 | 14 | return Object.entries(formatted).reduce((acc, [bp, value]) => { 15 | valueSetter(acc, bp, value) 16 | 17 | return acc 18 | }, {} as Parameters[0]) 19 | } 20 | -------------------------------------------------------------------------------- /src/contexts/carousel.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import type { CarouselGroupProps } from '../types/components/carousel' 3 | import { createContext } from '../utils/context' 4 | 5 | export interface CarouselState { 6 | uid: string 7 | translateItem: (index: number, activeIndex: number) => void 8 | } 9 | 10 | export interface CarouselGroupContext { 11 | props: CarouselGroupProps 12 | carousels: Ref 13 | registerCarousel: (state: CarouselState) => void 14 | unregisterCarousel: (id: string) => void 15 | } 16 | 17 | export const [ 18 | provideCarouselGroupContext, 19 | useCarouselGroupContext, 20 | ] = createContext('CarouselGroup') 21 | -------------------------------------------------------------------------------- /src/types/shared/utils.d.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentPublicInstance, MaybeRefOrGetter } from 'vue' 2 | 3 | export type Awaitable = T | PromiseLike 4 | export type Callback = (...args: any[]) => any 5 | export type Nullable = T | null | undefined 6 | export type Numeric = number | `${number}` 7 | 8 | // e.g.: 10 | '10' | '10px' | '-10px' 9 | export type CSSValue = Numeric | CSSUnitValue 10 | 11 | export type MaybeElement = Nullable 12 | export type MaybeElementRef = MaybeRefOrGetter> 13 | export type MaybeComputedElementRef = MaybeRefOrGetter> 14 | -------------------------------------------------------------------------------- /tests/components/kbd.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { describe, expect, it } from 'vitest' 3 | import Kbd from '../../src/components/kbd/index.vue' 4 | 5 | describe('kbd', () => { 6 | it('renders properly', async () => { 7 | const wrapper = mount(Kbd, { 8 | props: { 9 | ctrl: true, 10 | }, 11 | }) 12 | 13 | expect(wrapper.text()).toBe('Ctrl') 14 | 15 | wrapper.unmount() 16 | }) 17 | 18 | it('custom label', async () => { 19 | const wrapper = mount(Kbd, { 20 | slots: { 21 | default: 'custom label', 22 | }, 23 | }) 24 | 25 | expect(wrapper.text()).toBe('custom label') 26 | 27 | wrapper.unmount() 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/components/theme-switcher.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { describe, expect, it } from 'vitest' 3 | import ThemeSwitcher from '../../src/components/theme-switcher/index.vue' 4 | 5 | describe('theme-switcher', () => { 6 | it('should emit toggle event when button is clicked', async () => { 7 | const wrapper = mount(ThemeSwitcher) 8 | 9 | await wrapper.find('button').trigger('click') 10 | 11 | expect(wrapper.emitted().toggle).toBeTruthy() 12 | expect(wrapper.emitted().toggle[0]).toEqual(['dark']) 13 | 14 | await wrapper.find('button').trigger('click') 15 | 16 | expect(wrapper.emitted().toggle[1]).toEqual(['light']) 17 | 18 | wrapper.unmount() 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /src/composables/use-model-value.ts: -------------------------------------------------------------------------------- 1 | import type { WritableComputedRef } from 'vue' 2 | import { computed } from 'vue' 3 | 4 | interface Options { 5 | get?: (value: any) => any 6 | set?: (value: any) => void 7 | } 8 | 9 | export function useModelValue< 10 | P extends { modelValue: any }, 11 | E extends { (event: 'update:modelValue', ...args: any[]): void }, 12 | >(props: P, emits: E, options: Options = {}): WritableComputedRef> { 13 | type V = NonNullable 14 | 15 | const modelValue = computed({ 16 | get: options.get || (() => props.modelValue), 17 | set: options.set || ((value: V) => emits('update:modelValue', value)), 18 | }) 19 | 20 | return modelValue 21 | } 22 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/placeholder.md: -------------------------------------------------------------------------------- 1 | # Placeholder 2 | Indicates that it is not empty here. 3 | 4 | ## Default 5 | 6 | The color of the placeholder is controlled by the font color. 7 | 8 | ```vue demo 9 | 12 | ``` 13 | 14 | ## Invert 15 | Customize its size and line color. 16 | 17 | ```vue demo 18 | 21 | ``` 22 | 23 | ## Custom 24 | Customize its size and line color. 25 | 26 | ```vue demo 27 | 30 | ``` 31 | -------------------------------------------------------------------------------- /src/locales/zh-cn.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | date: { 3 | now: '此刻', 4 | day: { 5 | 0: '周日', 6 | 1: '周一', 7 | 2: '周二', 8 | 3: '周三', 9 | 4: '周四', 10 | 5: '周五', 11 | 6: '周六', 12 | }, 13 | month: { 14 | 0: '一月', 15 | 1: '二月', 16 | 2: '三月', 17 | 3: '四月', 18 | 4: '五月', 19 | 5: '六月', 20 | 6: '七月', 21 | 7: '八月', 22 | 8: '九月', 23 | 9: '十月', 24 | 10: '十一月', 25 | 11: '十二月', 26 | }, 27 | }, 28 | compare: { 29 | less: '少', 30 | more: '多', 31 | next: '之后', 32 | prev: '之前', 33 | }, 34 | empty: { 35 | search: '未找到结果', 36 | }, 37 | confirm: { 38 | ok: '确定', 39 | cancel: '取消', 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | # PXD 2 | 基于 Geist Design System(略有调整)实现的 Vue2&3 通用组件库 3 | 4 | - [Geist Design System](https://vercel.com/geist/introduction) 5 | - [Figma(Community)](https://www.figma.com/design/1234567890/PXD-UI?node-id=0-1&t=1234567890-0) 6 | 7 | [Online Preview](https://pxd-ui.netlify.app/) 8 | 9 | > [!WARNING] 10 | > 项目正在积极开发中,尚未做好投入生产的准备 11 | 12 | ## 特性 13 | 14 | - 使用 Vue 3 Composition API 15 | - 100% 兼容 Vue2&3 16 | - 完整的 tree-shaking 支持 17 | 18 | ## 贡献指南 19 | 20 | ### 启动开发环境 21 | 22 | ```shell 23 | pnpm install 24 | 25 | pnpm dev 26 | ``` 27 | 28 | ### 构建 29 | 30 | #### 组件 31 | 32 | ```shell 33 | pnpm build:lib 34 | ``` 35 | 36 | #### 文档 37 | 38 | ```shell 39 | pnpm build:docs 40 | ``` 41 | 42 | #### 部署 43 | ```shell 44 | pnpm build 45 | ``` 46 | -------------------------------------------------------------------------------- /packages/docs/src/pages/[...all].vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | 26 | 27 | meta: 28 | layout: false 29 | 30 | -------------------------------------------------------------------------------- /vitest.setup.ts: -------------------------------------------------------------------------------- 1 | // import { afterAll, beforeAll, beforeEach } from 'vitest' 2 | 3 | // beforeAll(() => { 4 | // globalThis.something = 'something' 5 | // }) 6 | 7 | // beforeAll(async () => { 8 | // await new Promise((resolve) => { 9 | // setTimeout(() => { 10 | // resolve(null) 11 | // }, 300) 12 | // }) 13 | // }) 14 | 15 | // beforeEach(async () => { 16 | // await new Promise((resolve) => { 17 | // setTimeout(() => { 18 | // resolve(null) 19 | // }, 10) 20 | // }) 21 | // }) 22 | 23 | // afterAll(() => { 24 | // delete globalThis.something 25 | // }) 26 | 27 | // afterAll(async () => { 28 | // await new Promise((resolve) => { 29 | // setTimeout(() => { 30 | // resolve(null) 31 | // }, 500) 32 | // }) 33 | // }) 34 | -------------------------------------------------------------------------------- /packages/docs/src/components/OverviewCard.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 28 | -------------------------------------------------------------------------------- /src/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-browser-observer.js' 2 | export * from './use-color-scheme.js' 3 | export * from './use-config-provider-context.js' 4 | export * from './use-copy-click.js' 5 | export * from './use-countdown.js' 6 | export * from './use-deferred-value.js' 7 | export * from './use-delay-change.js' 8 | export * from './use-delay-destroy.js' 9 | export * from './use-focus-trap.js' 10 | export * from './use-loading-bar.js' 11 | export * from './use-media-query.js' 12 | export * from './use-message.js' 13 | export * from './use-model-value.js' 14 | export * from './use-outside-click.js' 15 | export * from './use-pointer-gesture.js' 16 | export * from './use-repeat-action.js' 17 | export * from './use-unique-id-context.js' 18 | export * from './use-virtual-list.js' 19 | -------------------------------------------------------------------------------- /src/locales/en-us.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | date: { 3 | now: 'Now', 4 | day: { 5 | 0: 'Sun', 6 | 1: 'Mon', 7 | 2: 'Tue', 8 | 3: 'Wed', 9 | 4: 'Thu', 10 | 5: 'Fri', 11 | 6: 'Sat', 12 | }, 13 | month: { 14 | 0: 'Jan', 15 | 1: 'Feb', 16 | 2: 'Mar', 17 | 3: 'Apr', 18 | 4: 'May', 19 | 5: 'Jun', 20 | 6: 'Jul', 21 | 7: 'Aug', 22 | 8: 'Sep', 23 | 9: 'Oct', 24 | 10: 'Nov', 25 | 11: 'Dec', 26 | }, 27 | }, 28 | compare: { 29 | less: 'Less', 30 | more: 'More', 31 | next: 'Next', 32 | prev: 'Previous', 33 | }, 34 | empty: { 35 | search: 'No results found for', 36 | }, 37 | confirm: { 38 | ok: 'OK', 39 | cancel: 'Cancel', 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /tests/components/avatar-group.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { describe, expect, it } from 'vitest' 3 | import AvatarGroup from '../../src/components/avatar-group/index.vue' 4 | 5 | const avatarSrc = 'https://example.com/avatar.jpg' 6 | 7 | describe('avatar-group', () => { 8 | it('renders properly', async () => { 9 | const wrapper = mount(AvatarGroup, { 10 | props: { 11 | max: 1, 12 | options: [ 13 | { src: avatarSrc }, 14 | { src: avatarSrc }, 15 | ], 16 | }, 17 | }) 18 | 19 | const img = wrapper.find('img') 20 | expect(img.exists()).toBe(true) 21 | expect(img.attributes('src')).toBe(avatarSrc) 22 | expect(wrapper.text()).toContain('+1') 23 | 24 | wrapper.unmount() 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/plugins/resolver.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentResolver } from 'unplugin-vue-components' 2 | 3 | const LIBRARY_NAME = 'pxd' 4 | 5 | const NAMESPACE = LIBRARY_NAME[0]!.toUpperCase() 6 | 7 | function resolver(): ComponentResolver { 8 | const prefixRegex = /^P[A-Z]/ 9 | 10 | return { 11 | type: 'component', 12 | resolve(name: string) { 13 | if (!prefixRegex.test(name)) { 14 | return 15 | } 16 | 17 | const partialName = name 18 | .replace(new RegExp(NAMESPACE, 'i'), '') 19 | .replace(/([A-Z])/g, '-$1') 20 | .toLowerCase() 21 | .slice(1) 22 | 23 | return { 24 | name: 'default', 25 | as: name, 26 | from: `pxd/components/${partialName}`, 27 | } 28 | }, 29 | } 30 | } 31 | 32 | export default resolver 33 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import vue from '@vitejs/plugin-vue' 3 | import { configDefaults, defineConfig } from 'vitest/config' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | vue(), 8 | ], 9 | test: { 10 | globals: false, 11 | fileParallelism: true, 12 | environment: 'happy-dom', 13 | include: ['tests/**/*.test.ts'], 14 | exclude: [...configDefaults.exclude, 'e2e/**'], 15 | root: fileURLToPath(new URL('./', import.meta.url)), 16 | setupFiles: './vitest.setup.ts', 17 | server: { 18 | deps: { 19 | inline: [/@gdsicon\/vue/], 20 | }, 21 | }, 22 | deps: { 23 | optimizer: { 24 | web: { 25 | enabled: false, 26 | }, 27 | }, 28 | }, 29 | pool: 'vmThreads', 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /src/components/config-provider/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 32 | -------------------------------------------------------------------------------- /src/utils/ref.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vueuse/vueuse/blob/main/packages/core/unrefElement/index.ts#L16 2 | 3 | import type { ComponentPublicInstance, MaybeRefOrGetter } from 'vue' 4 | 5 | import type { MaybeElement } from '../types/shared/utils' 6 | import { unref } from 'vue' 7 | 8 | export type UnRefElementReturn = T extends ComponentPublicInstance 9 | ? Exclude 10 | : T | undefined 11 | 12 | export function toValue(source: MaybeRefOrGetter): T { 13 | return typeof source === 'function' ? (source as () => T)() : unref(source) 14 | } 15 | 16 | export function unrefElement(elRef: MaybeRefOrGetter): UnRefElementReturn { 17 | const plain = toValue(elRef) 18 | return (plain as ComponentPublicInstance)?.$el ?? plain 19 | } 20 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/more-button.md: -------------------------------------------------------------------------------- 1 | # More Button 2 | Styling component to show expanded or collapsed content. 3 | 4 | ## Default 5 | Based on the `button` component, props owns all buttons. 6 | 7 | ```vue demo 8 | 13 | 14 | 18 | ``` 19 | 20 | ## Texts 21 | You can modify the button text by setting `lessText` and `moreText`. 22 | 23 | ```vue demo 24 | 29 | 30 | 37 | ``` 38 | -------------------------------------------------------------------------------- /tests/components/active-graph.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { describe, expect, it } from 'vitest' 3 | import ActiveGraph from '../../src/components/active-graph/index.vue' 4 | 5 | describe('active-graph', () => { 6 | it('renders properly', async () => { 7 | const wrapper = mount(ActiveGraph, { 8 | props: { 9 | startDate: '2025-05-01', 10 | endDate: '2025-05-31', 11 | }, 12 | }) 13 | 14 | expect(wrapper.find('table tbody tr:nth-child(5) td:nth-child(2)') 15 | .attributes('data-date')) 16 | .toBe('2025-05-01') 17 | 18 | expect(wrapper.find('table tbody tr:nth-child(1) td:nth-child(3)') 19 | .attributes('data-date')) 20 | .toBe('2025-05-04') 21 | 22 | expect(wrapper.find('.pxd-active-graph--legend').exists()).toBe(true) 23 | 24 | wrapper.unmount() 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/types/components/input.d.ts: -------------------------------------------------------------------------------- 1 | import type { HTMLAttributes } from 'vue' 2 | import type { ComponentLabel, ComponentSizeWithXs } from '../shared/props' 3 | 4 | export interface InputProps { 5 | size?: ComponentSizeWithXs 6 | error?: string 7 | min?: number | string 8 | max?: number | string 9 | align?: 'left' | 'center' | 'right' 10 | label?: ComponentLabel 11 | readonly?: boolean 12 | disabled?: boolean 13 | password?: boolean 14 | required?: boolean 15 | autofocus?: boolean 16 | inputType?: HTMLInputElement['type'] 17 | inputmode?: HTMLAttributes['inputmode'] 18 | minlength?: number | string 19 | maxlength?: number | string 20 | modelValue?: ComponentLabel 21 | allowClear?: boolean 22 | placeholder?: string 23 | prefixStyle?: boolean 24 | suffixStyle?: boolean 25 | parser?: (value?: any) => any 26 | formatter?: (value?: any) => any 27 | } 28 | -------------------------------------------------------------------------------- /tests/components/material.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { describe, expect, it } from 'vitest' 3 | import Material from '../../src/components/material/index.vue' 4 | 5 | describe('material', () => { 6 | it('renders properly', async () => { 7 | const wrapper = mount(Material, { 8 | props: { 9 | variant: 'default', 10 | }, 11 | }) 12 | 13 | expect(wrapper.classes()).toContain('pxd-material') 14 | expect(wrapper.classes()).toContain('default') 15 | 16 | wrapper.unmount() 17 | }) 18 | 19 | it('renders small variant', async () => { 20 | const wrapper = mount(Material, { 21 | props: { 22 | variant: 'small', 23 | }, 24 | }) 25 | 26 | expect(wrapper.classes()).toContain('pxd-material') 27 | expect(wrapper.classes()).toContain('small') 28 | 29 | wrapper.unmount() 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /tests/components/gauge.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { describe, expect, it } from 'vitest' 3 | import Gauge from '../../src/components/gauge/index.vue' 4 | 5 | describe('gauge', () => { 6 | it('should render properly', () => { 7 | const wrapper = mount(Gauge, { 8 | props: { 9 | modelValue: 50, 10 | showValue: true, 11 | }, 12 | }) 13 | 14 | expect(wrapper.find('svg').exists()).toBe(true) 15 | expect(wrapper.findAll('circle').length).toBe(2) 16 | expect(wrapper.find('.pxd-gauge--value').text()).toBe('50') 17 | 18 | wrapper.unmount() 19 | }) 20 | 21 | it('should render indeterminate', () => { 22 | const wrapper = mount(Gauge, { 23 | props: { 24 | indeterminate: true, 25 | }, 26 | }) 27 | 28 | expect(wrapper.find('.pxd-gauge--indeterminate svg').exists()).toBe(true) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PXD 2 | Realizing (slightly adjusting) the general component library of Vue2&3 based on Geist Design System. 3 | 4 | - [Geist Design System](https://vercel.com/geist/introduction) 5 | - [Figma(Community)](https://www.figma.com/design/1234567890/PXD-UI?node-id=0-1&t=1234567890-0) 6 | 7 | [Online Preview](https://pxd-ui.netlify.app/) 8 | 9 | > [!WARNING] 10 | > The project is under active development and is not ready for production. 11 | 12 | ## Features 13 | 14 | - Vue 3 Composition API 15 | - 100% compatible with Vue2&3 16 | - Complete tree-shaking support 17 | 18 | ## Contribution 19 | 20 | ### Dev 21 | 22 | ```shell 23 | pnpm install 24 | 25 | pnpm dev 26 | ``` 27 | 28 | ### Build 29 | 30 | #### Core only 31 | 32 | ```shell 33 | pnpm build:lib 34 | ``` 35 | 36 | #### Docs only 37 | 38 | ```shell 39 | pnpm build:docs 40 | ``` 41 | 42 | #### Deploy 43 | 44 | ```shell 45 | pnpm build 46 | ``` 47 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | import { version } from 'vue' 2 | 3 | export const isVue3 = version.startsWith('3') 4 | 5 | export const isServer = typeof window === 'undefined' 6 | 7 | export const isTouchDevice = () => typeof document === 'undefined' ? false : 'ontouchstart' in document 8 | 9 | // https:// github.com/vueuse/vueuse/blob/main/packages/shared/utils/is.ts#L5 10 | export const isNotNullish = (value: T): value is NonNullable => value != null 11 | 12 | export const isIOS = /* #__PURE__ */ getIsIOS() 13 | 14 | function getIsIOS() { 15 | return !isServer && window?.navigator?.userAgent && ( 16 | (/iP(?:ad|hone|od)/.test(window.navigator.userAgent)) 17 | // The new iPad Pro Gen3 does not identify itself as iPad, but as Macintosh. 18 | // https://github.com/vueuse/vueuse/issues/3577 19 | || (window?.navigator?.maxTouchPoints > 2 && /iPad|Macintosh/.test(window?.navigator.userAgent)) 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/spinner/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 30 | -------------------------------------------------------------------------------- /packages/docs/src/consts/components.ts: -------------------------------------------------------------------------------- 1 | import components from './components.json' 2 | 3 | const componentsMenus = components.map(({ name, camelized }) => { 4 | return { 5 | label: camelized, 6 | path: `/components/${name}`, 7 | } 8 | }) 9 | 10 | export const asideMenus = [ 11 | { 12 | group: 'Guide', 13 | children: [ 14 | { 15 | label: 'Introduction', 16 | path: '/guide/introduction', 17 | }, 18 | { 19 | label: 'Installation', 20 | path: '/guide/installation', 21 | }, 22 | { 23 | label: 'Icons', 24 | path: '/guide/icons', 25 | }, 26 | { 27 | label: 'FAQ', 28 | path: '/guide/faq', 29 | }, 30 | ], 31 | }, 32 | { 33 | group: 'Components', 34 | children: [ 35 | { 36 | label: 'Overview', 37 | path: '/components', 38 | }, 39 | ...componentsMenus, 40 | ], 41 | }, 42 | ] 43 | -------------------------------------------------------------------------------- /packages/docs/src/components/SiteLogo.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /tests/components/note.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { describe, expect, it } from 'vitest' 3 | import Note from '../../src/components/note/index.vue' 4 | 5 | describe('note', () => { 6 | it('should render properly', () => { 7 | const wrapper = mount(Note, { 8 | slots: { 9 | default: 'test', 10 | }, 11 | }) 12 | 13 | expect(wrapper.text()).toBe('test') 14 | 15 | wrapper.unmount() 16 | }) 17 | 18 | it('should render label', () => { 19 | const wrapper = mount(Note, { 20 | slots: { 21 | label: 'label', 22 | }, 23 | }) 24 | 25 | expect(wrapper.text()).toBe('label') 26 | 27 | wrapper.unmount() 28 | }) 29 | 30 | it('should render action', () => { 31 | const wrapper = mount(Note, { 32 | slots: { 33 | action: 'action', 34 | }, 35 | }) 36 | 37 | expect(wrapper.text()).toBe('action') 38 | 39 | wrapper.unmount() 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/teleport.md: -------------------------------------------------------------------------------- 1 | # Teleport 2 | Provide 2.7 with behavior similar to the `` component built in 3. 3 | 4 | ## Default 5 | 6 | ```vue demo 7 | 22 | 23 | 34 | ``` 35 | -------------------------------------------------------------------------------- /tests/helpers/provide-inject.ts: -------------------------------------------------------------------------------- 1 | import type { InjectionKey } from 'vue' 2 | import { defineComponent, h, provide } from 'vue' 3 | import { mount } from './setup' 4 | 5 | interface InjectionConfig { 6 | key: InjectionKey | string 7 | value: any 8 | } 9 | 10 | export function useInjectedSetup( 11 | setup: () => TResult, 12 | injections: InjectionConfig[] = [], 13 | ): TResult & { unmount: () => void } { 14 | let result!: TResult 15 | 16 | const Wrapper = defineComponent({ 17 | setup() { 18 | result = setup() 19 | return () => h('div') 20 | }, 21 | }) 22 | 23 | const Provider = defineComponent({ 24 | setup() { 25 | injections.forEach(({ key, value }) => { 26 | provide(key, value) 27 | }) 28 | return () => h(Wrapper) 29 | }, 30 | }) 31 | 32 | const mounted = mount(Provider) 33 | 34 | return { 35 | ...result, 36 | unmount: mounted.unmount, 37 | } as TResult & { unmount: () => void } 38 | } 39 | -------------------------------------------------------------------------------- /tests/components/loading-dots.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { describe, expect, it } from 'vitest' 3 | import PLoadingDots from '../../src/components/loading-dots/index.vue' 4 | 5 | describe('loading-dots', () => { 6 | it('without slots', () => { 7 | const wrapper = mount(PLoadingDots) 8 | 9 | expect(wrapper.findAll('.pxd-loading--dot').length).toBe(3) 10 | 11 | expect(wrapper.find('.pxd-loading-dots--text').exists()).toBe(false) 12 | 13 | wrapper.unmount() 14 | }) 15 | 16 | it('with both slots', () => { 17 | const wrapper = mount(PLoadingDots, { 18 | slots: { 19 | prefix: 'loading', 20 | suffix: 'please wait', 21 | }, 22 | }) 23 | 24 | const slots = wrapper.findAll('.pxd-loading-dots--text') 25 | expect(slots.length).toBe(2) 26 | 27 | expect(slots[0].text()).toBe('loading') 28 | expect(slots[1].text()).toBe('please wait') 29 | 30 | wrapper.unmount() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/command-menu.md: -------------------------------------------------------------------------------- 1 | # Command Menu 2 | Launch a set of actions as a full-screen overlay. 3 | 4 | ## Default 5 | 6 | ```vue demo 7 | 12 | 13 | 36 | ``` 37 | -------------------------------------------------------------------------------- /packages/docs/src/pages/guide/why.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | ## 这是什么? 4 | 5 | -> `pxd` 是一个同时兼容 Vue@2 和 Vue@3 的 UI 组件库,让开发者无需切换依赖版本即可适配不同 Vue 版本的项目。 6 | 7 | ::: details Vue2 请先看这里 8 | Vue2 需要安装额外插件: [`unplugin-vue-define-options`](https://vue-macros.dev/macros/define-options.html) 以支持 [`defineOptions()`](https://vuejs.org/api/sfc-script-setup.html#defineoptions) 9 | ::: 10 | 11 | 需要注意的是,`pxd` 并非兼容 Vue 的所有版本。为了抹平 Vue2 和 Vue3 之间的差异,项目的 Vue 版本要求不低于 `2.7+`,或在 Vue3 版本不低于 `3.3`。这是因为 ` 17 | 18 | 36 | -------------------------------------------------------------------------------- /packages/docs/src/styles/index.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @import '../../node_modules/pxd/dist/styles/tw.css'; 3 | @source '../../node_modules/pxd'; 4 | @source '../../node_modules/markdown-it-plugins'; 5 | 6 | @theme { 7 | --default-mono-font-family: 'Geist Mono', 'JetBrains Mono', 'MonoLisa', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 8 | } 9 | 10 | :root { 11 | font-feature-settings: 'liga' 1, 'calt' 1; 12 | } 13 | 14 | html { 15 | scroll-padding-top: 60px; 16 | } 17 | 18 | html:has(:is(h1,h2,h3,h4):hover) { 19 | scroll-behavior: smooth; 20 | } 21 | 22 | html::selection { 23 | background-color: var(--color-green-1000); 24 | color: var(--color-green-100); 25 | } 26 | 27 | :not(pre) > code { 28 | font-size: 0.875rem; 29 | margin-right: 0.22em; 30 | padding: 0.15em 0.5em; 31 | border-radius: 0.25em; 32 | white-space: nowrap; 33 | background-color: var(--color-gray-alpha-200); 34 | border: 1px solid var(--color-gray-alpha-300); 35 | color: var(--color-foreground); 36 | } 37 | -------------------------------------------------------------------------------- /src/contexts/resizable.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import type { ComponentDirection } from '../types/shared/props' 3 | import { createContext } from '../utils/context' 4 | 5 | interface PanelConfig { 6 | id: string 7 | order: number 8 | size?: number | null 9 | minSize?: number 10 | } 11 | 12 | export interface ResizableContext { 13 | direction: Ref 14 | panelSizes: Ref 15 | panelConfigs: Ref 16 | getPanelSize: (id: string) => number 17 | onHandleDrag: (id: string, delta: { deltaX: number, deltaY: number }) => void 18 | resetPanels: () => void 19 | registerPanel: (config: { id: string, size?: number | null, minSize?: number }) => void 20 | registerHandle: (config: { id: string, onDrag: (delta: { deltaX: number, deltaY: number }) => void }) => void 21 | unregisterPanel: (id: string) => void 22 | unregisterHandle: (id: string) => void 23 | } 24 | 25 | export const [ 26 | provideResizableContext, 27 | useResizableContext, 28 | ] = createContext('ResizableContext') 29 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/theme-switcher.md: -------------------------------------------------------------------------------- 1 | 2 | # Theme Switcher 3 | Component that allows users to switch between light and dark themes. 4 | 5 | ## Further 6 | Put the following code in your `html > head`to ensure the correctness of the theme when the page is overloaded. (This is not necessary, it is only necessary to perform this operation when this kind of problem occurs.) 7 | 8 | ```js 9 | (()=>{if(typeof window==='undefined')return;let p=matchMedia('(prefers-color-scheme:dark)').matches;let s=localStorage.getItem('fe.system.color-scheme')||'auto';if(s==='dark'||(p&&s==='auto'))document.documentElement.classList.toggle('dark',true)})() 10 | ``` 11 | 12 | ## Default 13 | Support all attributes of the [button](/components/button) component (except slots) 14 | 15 | ```vue demo 16 | 19 | ``` 20 | 21 | ## Variants/Shape 22 | 23 | ```vue demo 24 | 30 | ``` 31 | -------------------------------------------------------------------------------- /tests/components/badge.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { describe, expect, it } from 'vitest' 3 | import Badge from '../../src/components/badge/index.vue' 4 | 5 | describe('badge', () => { 6 | it('renders properly', () => { 7 | const wrapper = mount(Badge, { 8 | slots: { 9 | default: 'Hello PXD!', 10 | }, 11 | }) 12 | 13 | expect(wrapper.text()).toContain('Hello PXD!') 14 | expect(wrapper.element.tagName).toBe('SPAN') 15 | 16 | wrapper.unmount() 17 | }) 18 | 19 | it('should variant equal vue', () => { 20 | const wrapper = mount(Badge, { 21 | props: { 22 | variant: 'vue', 23 | }, 24 | }) 25 | 26 | const classes = wrapper.classes() 27 | expect(classes).toContain('vue') 28 | 29 | wrapper.unmount() 30 | }) 31 | 32 | it('should render as a tag', () => { 33 | const wrapper = mount(Badge, { 34 | props: { 35 | as: 'a', 36 | }, 37 | }) 38 | 39 | expect(wrapper.element.tagName).toBe('A') 40 | 41 | wrapper.unmount() 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /tests/components/link-button.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { describe, expect, it } from 'vitest' 3 | import LinkButton from '../../src/components/link-button/index.vue' 4 | 5 | describe('link-button', () => { 6 | it('should render properly', () => { 7 | const wrapper = mount(LinkButton, { 8 | props: { 9 | href: 'https://vuejs.org/', 10 | target: '_blank', 11 | }, 12 | }) 13 | 14 | const el = wrapper.element as HTMLAnchorElement 15 | 16 | expect(el.tagName).toBe('A') 17 | expect(el.href).toBe('https://vuejs.org/') 18 | expect(el.target).toBe('_blank') 19 | 20 | wrapper.unmount() 21 | }) 22 | 23 | it('should render external link icon when set', () => { 24 | const wrapper = mount(LinkButton, { 25 | props: { 26 | href: '/', 27 | externalIcon: true, 28 | }, 29 | }) 30 | 31 | const children = wrapper.element.children 32 | 33 | expect(children.length).toBe(2) 34 | expect(children[1].tagName).toBe('svg') 35 | 36 | wrapper.unmount() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /tests/helpers/setup.ts: -------------------------------------------------------------------------------- 1 | import { createApp, defineComponent, h } from 'vue' 2 | 3 | type InstanceType = V extends { new (...arg: any[]): infer X } ? X : never 4 | type VM = InstanceType & { unmount: () => void } 5 | 6 | export function useSetupWrapper( 7 | setup: () => TResult, 8 | ): TResult & { unmount: () => void } { 9 | let result!: TResult 10 | 11 | const Wrapper = defineComponent({ 12 | setup() { 13 | result = setup() 14 | return () => h('div') 15 | }, 16 | }) 17 | 18 | const mounted = mount(Wrapper) 19 | 20 | return { 21 | ...result, 22 | unmount: mounted.unmount, 23 | } as TResult & { unmount: () => void } 24 | } 25 | 26 | // Export mount function for reuse in other helpers 27 | export function mount(Comp: V) { 28 | const el = document.createElement('div') 29 | const app = createApp(Comp as any) 30 | const unmount = () => app.unmount() 31 | const comp = app.mount(el) as any as VM 32 | comp.unmount = unmount 33 | return comp 34 | } 35 | 36 | // Export types for reuse 37 | export type { InstanceType, VM } 38 | -------------------------------------------------------------------------------- /src/composables/use-delay-change.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter, Ref } from 'vue' 2 | import { shallowRef } from 'vue' 3 | 4 | interface Options { 5 | delay?: number 6 | valueChange?: (v: T) => void 7 | } 8 | 9 | interface Results { 10 | value: Ref 11 | setValue: (value: T, immediate?: boolean) => void 12 | } 13 | 14 | export function useDelayChange( 15 | value: MaybeRefOrGetter, 16 | options: Options = {}, 17 | ): Results { 18 | const { 19 | delay = 300, 20 | valueChange, 21 | } = options 22 | 23 | let timerId: ReturnType 24 | const delayValue = shallowRef(value as T) 25 | 26 | function setValue(newValue: T, immediate = false) { 27 | clearTimeout(timerId) 28 | 29 | if (immediate) { 30 | delayValue.value = newValue 31 | valueChange?.(delayValue.value) 32 | return 33 | } 34 | 35 | timerId = setTimeout(() => { 36 | delayValue.value = newValue 37 | valueChange?.(delayValue.value) 38 | }, delay) 39 | } 40 | 41 | return { 42 | value: delayValue, 43 | setValue, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/components/avatar.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { describe, expect, it } from 'vitest' 3 | import Avatar from '../../src/components/avatar/index.vue' 4 | 5 | const avatarSrc = 'https://example.com/avatar.jpg' 6 | 7 | describe('avatar', () => { 8 | it('renders properly', async () => { 9 | const wrapper = mount(Avatar, { 10 | props: { 11 | src: avatarSrc, 12 | }, 13 | }) 14 | 15 | const img = wrapper.find('img') 16 | expect(img.exists()).toBe(true) 17 | expect(img.attributes('src')).toBe(avatarSrc) 18 | 19 | wrapper.unmount() 20 | }) 21 | 22 | it('handles image load error', async () => { 23 | const wrapper = mount(Avatar, { 24 | props: { 25 | src: 'invalid-url.jpg', 26 | }, 27 | }) 28 | 29 | const img = wrapper.find('img') 30 | await img.trigger('error') 31 | 32 | expect(wrapper.vm.getLoadingStatus()).toBe('error') 33 | expect(wrapper.emitted('error')).toBeTruthy() 34 | 35 | // 图片加载失败的时候隐藏裂开的图 36 | expect(wrapper.find('img').exists()).toBe(false) 37 | 38 | wrapper.unmount() 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/overlay.md: -------------------------------------------------------------------------------- 1 | # Overlay 2 | Highlight certain contents. 3 | 4 | ## Default 5 | 6 | ```vue demo 7 | 16 | 17 | 21 | ``` 22 | 23 | ## With shown element 24 | Highlight and display a certain element. 25 | 26 | ```vue demo 27 | 37 | 38 | 46 | ``` 47 | -------------------------------------------------------------------------------- /src/components/virtual-list/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 44 | -------------------------------------------------------------------------------- /packages/docs/src/components/Menus.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 42 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/error.md: -------------------------------------------------------------------------------- 1 | # Error 2 | Good error design is clear, useful, and friendly. Designing concise and accurate error messages unblocks users and builds trust by meeting people where they are. 3 | 4 | ## Default 5 | 6 | ```vue demo 7 | 10 | ``` 11 | 12 | ## Custom Label 13 | 14 | ```vue demo 15 | 18 | ``` 19 | 20 | ## Sizes 21 | 22 | ```vue demo 23 | 30 | ``` 31 | 32 | ## With an error property 33 | 34 | ```vue demo 35 | 45 | 46 | 49 | ``` 50 | -------------------------------------------------------------------------------- /src/components/theme-switcher/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 42 | -------------------------------------------------------------------------------- /src/components/loading-dots/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 35 | -------------------------------------------------------------------------------- /src/composables/use-copy-click.ts: -------------------------------------------------------------------------------- 1 | import { shallowRef } from 'vue' 2 | 3 | export function useCopyClick() { 4 | let copiedTimer: ReturnType 5 | let copyPromise: Promise | null = null 6 | 7 | const isCopied = shallowRef(false) 8 | 9 | async function copyText(text: string | undefined = '') { 10 | if (copyPromise) { 11 | return copyPromise 12 | } 13 | 14 | try { 15 | await navigator.clipboard.writeText(text) 16 | } catch { 17 | legacyCopyText(text) 18 | } 19 | 20 | copyPromise = new Promise((resolve) => { 21 | isCopied.value = true 22 | 23 | resolve() 24 | clearTimeout(copiedTimer) 25 | 26 | copiedTimer = setTimeout(() => { 27 | isCopied.value = false 28 | copyPromise = null 29 | }, 1500) 30 | }) 31 | 32 | return copyPromise 33 | } 34 | 35 | return { 36 | isCopied, 37 | copyText, 38 | } 39 | } 40 | 41 | function legacyCopyText(text: string) { 42 | const textarea = document.createElement('textarea') 43 | textarea.value = text 44 | document.body.appendChild(textarea) 45 | textarea.select() 46 | document.execCommand('copy') 47 | document.body.removeChild(textarea) 48 | } 49 | -------------------------------------------------------------------------------- /src/components/placeholder/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 34 | 35 | 52 | -------------------------------------------------------------------------------- /packages/docs/src/pages/guide/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | Here will record some problems encountered in the process of use. If you have no clue after finding the problems, you can come here and have a look. 3 | 4 | ## Use camelCase style in Vue2 but the event doesn't take effect? 5 | 6 | Because the events in vue2 distinguish between camelCase and kebab-case style, but the common style in vue2 is kebab-case style, please use the form of @kebab-case when the events do not take effect. 7 | 8 | ```html 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ``` 17 | 18 | ## No loader is configured for ".vue" files 19 | e.g.: 20 | ```js 21 | import XxxIcon from "@gdsicon/vue/xxx" 22 | ``` 23 | 24 | This is because the source file provided by the logo library has a suffix of `.vue`, and vite will not read the `.vue` file imported by js in the child dependency by default. 25 | 26 | The solution is to set `optimizeDeps.exclude` in vite.config: 27 | 28 | ```js 29 | import { defineConfig } from "vite" 30 | 31 | export default defineConfig({ 32 | optimizeDeps: { 33 | exclude: ["@gdsicon/vue"], 34 | }, 35 | }) 36 | ``` 37 | -------------------------------------------------------------------------------- /src/components/empty-state/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 41 | -------------------------------------------------------------------------------- /scripts/gen-component-dts.js: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'node:fs/promises' 2 | import path from 'node:path' 3 | import process from 'node:process' 4 | import { globSync } from 'tinyglobby' 5 | import { pascalize } from './utils.js' 6 | 7 | const ROOT = process.cwd() 8 | const TYPE_PATH = path.resolve(ROOT, 'volar.d.ts') 9 | const componentsPath = globSync('./src/components/*/index.vue', { cwd: ROOT }) 10 | 11 | /** 12 | * @param {string[]} lines 13 | * @returns {string} 模板内容 14 | */ 15 | function getFileTemplate(lines) { 16 | return `/* eslint-disable */ 17 | /* prettier-ignore */ 18 | // @ts-nocheck 19 | export {} 20 | declare module 'vue' { 21 | export interface GlobalComponents { 22 | ${lines.join('\n ')} 23 | } 24 | } 25 | ` 26 | } 27 | 28 | function processComponentsPath() { 29 | const exports = componentsPath.map((p) => { 30 | const path = p.replace(/src/, 'pxd').replace(/\/index\.vue/, '') 31 | const [,name] = path.match(/.*\/components\/(.*)/) 32 | 33 | return `P${pascalize(name)}: typeof import('${path}')['default']` 34 | }) 35 | 36 | return exports 37 | } 38 | 39 | async function run() { 40 | const fileContent = getFileTemplate(processComponentsPath()) 41 | 42 | await writeFile(TYPE_PATH, fileContent, 'utf-8') 43 | } 44 | 45 | run() 46 | -------------------------------------------------------------------------------- /src/composables/use-deferred-value.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter, Ref } from 'vue' 2 | import { onBeforeUnmount, ref, watch } from 'vue' 3 | import { toValue } from '../utils/ref' 4 | 5 | interface Options { 6 | deep?: boolean 7 | delay?: number 8 | valueChange?: (v: T) => void 9 | } 10 | 11 | interface Results { 12 | value: Ref 13 | deferred: Ref 14 | } 15 | 16 | export function useDeferredValue( 17 | defaultValue: MaybeRefOrGetter, 18 | options: Options = {}, 19 | ): Results { 20 | const { 21 | deep, 22 | delay = 300, 23 | valueChange, 24 | } = options 25 | 26 | const syncValue = ref(toValue(defaultValue)) 27 | const deferredValue = ref(syncValue.value) 28 | 29 | let syncTimeoutId: ReturnType 30 | 31 | const unwatch = watch( 32 | () => syncValue.value, 33 | (v) => { 34 | clearTimeout(syncTimeoutId) 35 | 36 | syncTimeoutId = setTimeout(() => { 37 | deferredValue.value = v 38 | valueChange?.(v) 39 | }, delay) 40 | }, 41 | { deep }, 42 | ) 43 | 44 | onBeforeUnmount(() => { 45 | unwatch() 46 | clearTimeout(syncTimeoutId) 47 | }) 48 | 49 | return { 50 | value: syncValue as Ref, 51 | deferred: deferredValue, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "canvas-confetti": "catalog:", 13 | "pxd": "workspace:*", 14 | "vue": "catalog:" 15 | }, 16 | "devDependencies": { 17 | "@shikijs/markdown-it": "catalog:", 18 | "@sindresorhus/slugify": "catalog:", 19 | "@tailwindcss/vite": "catalog:", 20 | "@types/markdown-it-attrs": "catalog:", 21 | "@unhead/vue": "catalog:", 22 | "@vitejs/plugin-vue": "catalog:", 23 | "@vitejs/plugin-vue-jsx": "catalog:", 24 | "@vue/compiler-sfc": "catalog:", 25 | "@vue/tsconfig": "catalog:", 26 | "markdown-it-anchor": "catalog:", 27 | "markdown-it-attrs": "catalog:", 28 | "markdown-it-plugins": "catalog:", 29 | "shiki": "catalog:", 30 | "tailwindcss": "catalog:", 31 | "typescript": "catalog:", 32 | "unplugin-auto-import": "catalog:", 33 | "unplugin-vue-components": "catalog:", 34 | "unplugin-vue-router": "catalog:", 35 | "vite": "catalog:", 36 | "vite-plugin-vue-meta-layouts": "catalog:", 37 | "vite-vue-md": "catalog:", 38 | "vue-router": "catalog:" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/status-dot/index.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 49 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/status-dot.md: -------------------------------------------------------------------------------- 1 | # Status Dot 2 | Display an indicator of deployment status. 3 | 4 | ## Default 5 | 6 | ```vue demo 7 | 16 | ``` 17 | 18 | ## Label 19 | Set the `label` property to show the status text. 20 | 21 | ```vue demo 22 | 31 | ``` 32 | 33 | ## Custom Label Text 34 | Or pass a `string` to the `label` to customize the text. 35 | 36 | ```vue demo 37 | 46 | ``` 47 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/virtual-list.md: -------------------------------------------------------------------------------- 1 | # Virtual List 2 | Loads infinite lists of data, but doesn't really render them. 3 | 4 | ## Fixed height items 5 | 6 | ```vue demo 7 | 13 | 14 | 23 | ``` 24 | 25 | ## Dynamic height items 26 | Set an approximate height for each item. 27 | 28 | ```vue demo 29 | 36 | 37 | 46 | ``` 47 | -------------------------------------------------------------------------------- /src/components/avatar-group/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 47 | -------------------------------------------------------------------------------- /src/types/shared/props.d.ts: -------------------------------------------------------------------------------- 1 | import type { VNode } from 'vue' 2 | import type { Awaitable } from './utils' 3 | 4 | export type ComponentVariant = 'primary' | 'error' | 'warning' | 'success' 5 | export type ComponentVariantWithDefault = ComponentVariant | 'default' 6 | 7 | export type ComponentSize = 'sm' | 'md' | 'lg' 8 | export type ComponentSizeWithXs = ComponentSize | 'xs' 9 | 10 | export type ComponentBreakpointKeys = 'xs' | 'sm' | 'md' | 'lg' | 'xl' 11 | export type ComponentBreakpoint = Record 12 | 13 | export type ComponentAs = keyof HTMLElementTagNameMap | 'router-link' | 'RouterLink' | VNode 14 | export type ComponentLabel = string | number | readonly string[] | null 15 | export type ComponentValue = string | number | boolean 16 | 17 | export type ComponentClass = string | any[] | Record 18 | 19 | export type ComponentDirection = 'horizontal' | 'vertical' 20 | 21 | export type BasePosition = 'top' | 'bottom' | 'left' | 'right' 22 | export type ComponentPosition 23 | = | Position 24 | | `${Position}-start` 25 | | `${Position}-end` 26 | 27 | export type ResponsiveValue = T | Partial> 28 | 29 | export interface ComponentOption { 30 | label: ComponentLabel 31 | value: string | number 32 | disabled?: boolean 33 | } 34 | 35 | export type ComponentBeforeChange = (value: T) => Awaitable 36 | -------------------------------------------------------------------------------- /tests/components/button.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { describe, expect, it } from 'vitest' 3 | import Button from '../../src/components/button/index.vue' 4 | 5 | describe('button', () => { 6 | it('renders properly', () => { 7 | const wrapper = mount(Button, { 8 | slots: { 9 | default: 'Hello PXD!', 10 | }, 11 | }) 12 | 13 | expect(wrapper.text()).toContain('Hello PXD!') 14 | 15 | wrapper.unmount() 16 | }) 17 | 18 | it('should render an default button', () => { 19 | const wrapper = mount(Button, { 20 | props: { 21 | variant: 'default', 22 | }, 23 | }) 24 | 25 | const classes = wrapper.classes() 26 | expect(classes).toContain('bg-background-100') 27 | expect(classes).toContain('text-foreground') 28 | expect(classes).toContain('border-input') 29 | 30 | wrapper.unmount() 31 | }) 32 | 33 | it('should emit a click event when clicked', async () => { 34 | const wrapper = mount(Button) 35 | 36 | wrapper.find('button').trigger('click') 37 | 38 | expect(wrapper.emitted()).toHaveProperty('click') 39 | 40 | wrapper.unmount() 41 | }) 42 | 43 | it('should render as a div when set as prop', async () => { 44 | const wrapper = mount(Button, { 45 | props: { 46 | as: 'div', 47 | }, 48 | }) 49 | 50 | expect(wrapper.element.tagName).toBe('DIV') 51 | 52 | wrapper.unmount() 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /packages/docs/scripts/vite-plugin-file-create-watcher.ts: -------------------------------------------------------------------------------- 1 | import type { ViteDevServer } from 'vite' 2 | import { execSync } from 'node:child_process' 3 | import { writeFileSync } from 'node:fs' 4 | import path, { sep } from 'node:path' 5 | import process from 'node:process' 6 | import { humanize, pascalize } from '../../../scripts/utils.js' 7 | 8 | export function fileCreateWatcher() { 9 | return { 10 | name: 'file-create-watcher', 11 | configureServer(server: ViteDevServer) { 12 | const watcher = server.watcher 13 | 14 | watcher.add([ 15 | path.resolve(process.cwd(), '../../src/components'), 16 | path.resolve(process.cwd(), '../../src/composables'), 17 | ]) 18 | 19 | watcher.on('add', (filePath: string) => { 20 | execSync(`pnpm -w update-exports`, { cwd: process.cwd() }) 21 | 22 | if (filePath.endsWith('index.vue')) { 23 | const componentName = filePath.split(sep).at(-2) || '' 24 | const componentNamePascal = pascalize(componentName) 25 | 26 | const mdFilePath = path.resolve(process.cwd(), 'src', 'pages', 'components', `${componentName}.md`) 27 | const mdFileContent = `# ${humanize(componentName)}\n\n 28 | ## Default\n 29 | \`\`\`vue demo 30 | 33 | \`\`\` 34 | ` 35 | writeFileSync(mdFilePath, mdFileContent) 36 | } 37 | }) 38 | }, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | import eslintPluginBetterTailwindcss from 'eslint-plugin-better-tailwindcss' 3 | 4 | export default antfu( 5 | { 6 | ignores: [ 7 | 'dist', 8 | 'node_modules/*', 9 | ], 10 | 11 | test: true, 12 | jsonc: true, 13 | markdown: false, 14 | typescript: true, 15 | vue: true, 16 | 17 | rules: { 18 | 'curly': ['error', 'all'], 19 | 'vue/custom-event-name-casing': ['error', 'kebab-case'], 20 | 'style/brace-style': ['error', '1tbs', { allowSingleLine: true }], 21 | }, 22 | }, 23 | { 24 | plugins: { 25 | 'better-tailwindcss': eslintPluginBetterTailwindcss, 26 | }, 27 | rules: { 28 | 'better-tailwindcss/enforce-consistent-class-order': ['error', { order: 'improved' }], 29 | 'better-tailwindcss/no-conflicting-classes': 'error', 30 | 'better-tailwindcss/no-duplicate-classes': 'error', 31 | 'better-tailwindcss/no-unnecessary-whitespace': 'error', 32 | 'better-tailwindcss/no-deprecated-classes': 'error', 33 | 'better-tailwindcss/enforce-consistent-variable-syntax': ['error', { syntax: 'shorthand' }], 34 | 'better-tailwindcss/enforce-consistent-important-position': ['error', { position: 'legacy' }], 35 | 'better-tailwindcss/enforce-shorthand-classes': 'error', 36 | }, 37 | settings: { 38 | 'better-tailwindcss': { 39 | entryPoint: 'src/styles/tw.css', 40 | }, 41 | }, 42 | }, 43 | ) 44 | -------------------------------------------------------------------------------- /packages/docs/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { useLoadingBar } from 'pxd' 2 | import { setupLayouts } from 'virtual:meta-layouts' 3 | import { createRouter, createWebHistory } from 'vue-router' 4 | import { handleHotUpdate, routes } from 'vue-router/auto-routes' 5 | 6 | const router = createRouter({ 7 | history: createWebHistory(import.meta.env.BASE_URL), 8 | routes: setupLayouts(routes), 9 | scrollBehavior(to, from, savedPosition) { 10 | if (savedPosition) { 11 | return savedPosition 12 | } 13 | 14 | if (to.hash) { 15 | return new Promise((resolve) => { 16 | requestAnimationFrame(() => { 17 | const el = document.querySelector(to.hash) 18 | 19 | if (!el) { 20 | return resolve({ left: 0, top: 0 }) 21 | } 22 | 23 | const header = document.querySelector('header') 24 | const offset = header?.offsetHeight ?? 0 25 | const top = el.getBoundingClientRect().top + window.scrollY - offset - 10 26 | 27 | resolve({ top, behavior: 'smooth' }) 28 | }) 29 | }) 30 | } 31 | 32 | return { 33 | top: 0, 34 | left: 0, 35 | } 36 | }, 37 | }) 38 | 39 | router.beforeEach((to, from, next) => { 40 | useLoadingBar.start('website') 41 | next() 42 | }) 43 | 44 | router.afterEach(() => { 45 | useLoadingBar.finish('website') 46 | }) 47 | 48 | if (import.meta.hot) { 49 | handleHotUpdate(router) 50 | } 51 | 52 | export default router 53 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - . 3 | - packages/* 4 | catalog: 5 | '@antfu/eslint-config': ^4.19.0 6 | '@clack/prompts': ^0.11.0 7 | '@gdsicon/vue': ^1.0.7 8 | '@shikijs/markdown-it': ^3.19.0 9 | '@sindresorhus/slugify': ^3.0.0 10 | '@tailwindcss/cli': ^4.1.17 11 | '@tailwindcss/vite': ^4.1.17 12 | '@tsconfig/node22': ^22.0.5 13 | '@types/markdown-it-attrs': ^4.1.3 14 | '@types/node': ^24.10.3 15 | '@unhead/vue': ^2.0.19 16 | '@vitejs/plugin-vue': ^6.0.2 17 | '@vitejs/plugin-vue-jsx': ^5.1.2 18 | '@vue/compiler-sfc': ^3.5.25 19 | '@vue/test-utils': ^2.4.6 20 | '@vue/tsconfig': ^0.8.1 21 | canvas-confetti: ^1.9.4 22 | cross-spawn: ^7.0.6 23 | dayjs: ^1.11.19 24 | eslint: ^9.39.1 25 | eslint-plugin-better-tailwindcss: ^3.8.0 26 | fs-extra: ^11.3.2 27 | happy-dom: ^20.0.11 28 | husky: ^9.1.7 29 | lint-staged: ^16.2.7 30 | markdown-it-anchor: ^9.2.0 31 | markdown-it-attrs: ^4.3.1 32 | markdown-it-plugins: ^0.4.0 33 | mkdist: ^2.4.1 34 | mri: ^1.2.0 35 | npm-run-all2: ^8.0.4 36 | shiki: ^3.19.0 37 | tailwindcss: ^4.1.17 38 | tinyglobby: ^0.2.15 39 | typescript: ^5.9.3 40 | unbuild: ^3.6.1 41 | unplugin-auto-import: ^20.3.0 42 | unplugin-vue-components: ^30.0.0 43 | unplugin-vue-router: ^0.14.0 44 | vite: ^8.0.0-beta.1 45 | vite-plugin-vue-meta-layouts: ^0.6.1 46 | vite-vue-md: ^1.4.0 47 | vitest: ^4.0.15 48 | vue: ^3.5.25 49 | vue-router: ^4.6.3 50 | vue-sfc-transformer: ^0.1.17 51 | vue-tsc: ^3.1.8 52 | -------------------------------------------------------------------------------- /src/components/radio-group/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 50 | -------------------------------------------------------------------------------- /src/composables/use-outside-click.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeElementRef } from '../types/shared/utils' 2 | import { onBeforeUnmount, watch } from 'vue' 3 | import { optimizedOff, optimizedOn } from '../utils/event' 4 | import { toValue } from '../utils/ref' 5 | 6 | interface Options { 7 | isEnabled?: (ev: PointerEvent) => boolean 8 | isOutside?: (ev: PointerEvent) => boolean 9 | onTrigger?: (ev: PointerEvent) => void 10 | } 11 | 12 | export function useOutsideClick( 13 | container: MaybeElementRef, 14 | options: Options = {}, 15 | ) { 16 | function onClick(ev: PointerEvent) { 17 | const { isEnabled, isOutside, onTrigger } = options 18 | 19 | if (typeof isEnabled === 'function' && !isEnabled(ev)) { 20 | return 21 | } 22 | 23 | if ( 24 | (typeof isOutside === 'function' ? !isOutside(ev) : false) 25 | && (toValue(container)?.contains(ev.target as HTMLElement)) 26 | ) { 27 | return 28 | } 29 | 30 | onTrigger?.(ev) 31 | } 32 | 33 | const unwatch = watch(() => toValue(container), (dom, _, onCleanup) => { 34 | if (dom) { 35 | optimizedOn(document, 'click', onClick) 36 | } 37 | 38 | onCleanup(() => { 39 | optimizedOff(document, 'click', onClick) 40 | }) 41 | }, { immediate: true }) 42 | 43 | function stop() { 44 | unwatch() 45 | optimizedOff(document, 'click', onClick) 46 | } 47 | 48 | onBeforeUnmount(() => { 49 | stop() 50 | }) 51 | 52 | return { 53 | stop, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/chip.md: -------------------------------------------------------------------------------- 1 | # Chip 2 | An indicator of a numeric value or a state. 3 | 4 | ## Default 5 | 6 | ```vue demo 7 | 10 | 11 | 30 | ``` 31 | 32 | ## Size 33 | 34 | ```vue demo 35 | 40 | ``` 41 | 42 | ## Text 43 | 44 | ```vue demo 45 | 48 | 49 | 56 | ``` 57 | 58 | ## Inset 59 | Use the inset prop to display the Chip inside the component. This is useful when dealing with rounded components. 60 | 61 | ```vue demo 62 | 67 | ``` 68 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/fader.md: -------------------------------------------------------------------------------- 1 | # Fader 2 | Indicates that there is still something to show in a certain direction. 3 | 4 | ## Default 5 | 6 | ```vue demo 7 | 19 | 20 | 36 | ``` 37 | 38 | ## Stylize 39 | 40 | ```vue demo 41 | 46 | 47 | 59 | ``` 60 | -------------------------------------------------------------------------------- /src/components/kbd/index.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 59 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/radio.md: -------------------------------------------------------------------------------- 1 | # Radio 2 | Provides single user input from a selection of options. 3 | 4 | ## Default 5 | 6 | ```vue demo 7 | 12 | 13 | 19 | ``` 20 | 21 | ## Group 22 | Support all props of `stack` components 23 | 24 | ```vue demo 25 | 35 | 36 | 53 | ``` 54 | 55 | ## Radio standalone 56 | Standalone unlabelled radio input for use in custom UI. 57 | 58 | ```vue demo 59 | 64 | 65 | 71 | ``` 72 | -------------------------------------------------------------------------------- /packages/docs/src/pages/components/config-provider.md: -------------------------------------------------------------------------------- 1 | # Config Provider 2 | Config Provider is used for providing global configurations, 3 | 4 | ## Default 5 | 6 | ```vue demo 7 | 18 | 19 | 32 | ``` 33 | 34 | ## Locale 35 | 36 | ```vue demo 37 | 55 | 56 | 64 | ``` 65 | -------------------------------------------------------------------------------- /src/composables/use-loading-bar.ts: -------------------------------------------------------------------------------- 1 | // Part of the realization comes from: https://github.com/rstacruz/nprogress 2 | 3 | export const START_LOADING_BAR_EVENT_NAME = '#start-loading-bar' 4 | export const ERROR_LOADING_BAR_EVENT_NAME = '#error-loading-bar' 5 | export const FINISH_LOADING_BAR_EVENT_NAME = '#finish-loading-bar' 6 | export const INCREASE_LOADING_BAR_EVENT_NAME = '#increase-loading-bar' 7 | 8 | const LOADING_BAR_EVENTS = { 9 | start: START_LOADING_BAR_EVENT_NAME, 10 | error: ERROR_LOADING_BAR_EVENT_NAME, 11 | finish: FINISH_LOADING_BAR_EVENT_NAME, 12 | increase: INCREASE_LOADING_BAR_EVENT_NAME, 13 | } as const 14 | 15 | interface Options extends Record { 16 | type?: keyof typeof LOADING_BAR_EVENTS 17 | } 18 | 19 | export interface LoadingBarEventParams { 20 | group: string 21 | value?: number 22 | } 23 | 24 | export function useLoadingBar(options: Options = {}) { 25 | const { type = 'start', ...data } = options 26 | const event = LOADING_BAR_EVENTS[type] 27 | 28 | window.dispatchEvent(new CustomEvent(event, { detail: data })) 29 | } 30 | 31 | useLoadingBar.start = function (group: string = 'default') { 32 | useLoadingBar({ type: 'start', group }) 33 | } 34 | 35 | useLoadingBar.error = function (group: string = 'default') { 36 | useLoadingBar({ type: 'error', group }) 37 | } 38 | 39 | useLoadingBar.finish = function (group: string = 'default') { 40 | useLoadingBar({ type: 'finish', group }) 41 | } 42 | 43 | useLoadingBar.increase = function (group: string = 'default', value?: number) { 44 | useLoadingBar({ type: 'increase', group, value }) 45 | } 46 | -------------------------------------------------------------------------------- /packages/docs/src/pages/guide/problems.md: -------------------------------------------------------------------------------- 1 | # 问题记录 2 | 这里记录了实现过程中遇到的一些问题及解决方法,这些问题仅出现在使用当前组件库相同实现方案中,不代表其他方案是否也存在,所以仅供参考 3 | 4 | - 编译成 Vue@2.7 时,`withDefaults` 默认值只能使用对象字面量,也不能传入对象引用, 这个配置只能重新写一个对象 5 | 6 | ```ts 7 | // don't work 8 | const props = withDefaults( 9 | defineProps(), 10 | defaultConfig, 11 | ) 12 | // don't work 13 | const props = withDefaults( 14 | defineProps(), 15 | { ...defaultConfig }, 16 | ) 17 | 18 | // working 19 | const props = withDefaults( 20 | defineProps(), 21 | { sm: 'md' }, 22 | ) 23 | ``` 24 | 25 | - 同一个工作区安装了多个不同版本的 vue 会出现各种奇怪的问题,比如开发时使用的 `provide/inject` 是正常的,但是打包运行以后,`inject` 会无法获取,所以 Vue2 还是单独新建项目进行测试 26 | 27 | - 由于 Vue2 中事件透传的机制与 Vue3 不同(Vue3中不再区分原生事件和自定义事件),所以类似 `Button` 之类包含用户交互的 `click` 事件需要主动使用 emit 声明并向上传递事件 28 | ```html 29 | 34 | 35 | 42 | ``` 43 | 44 | - 同样由于 Vue2 中 v-bind 的行为有所不同,所以可能有些属性不能正常传递和覆盖,如果在组件中遇到用户可以传入属性进行覆盖的就可以先合并再一次性传入: 45 | ```js 46 | function getButtonProps() { 47 | return { 48 | shape: 'rounded', 49 | ...props.buttonProps, 50 | } 51 | } 52 | ``` 53 | 54 | 使用时整个传入 55 | 56 | ```html 57 |