diff --git a/docs/src/pages/components/avatar.md b/docs/src/pages/components/avatar.md new file mode 100644 index 0000000..32d345b --- /dev/null +++ b/docs/src/pages/components/avatar.md @@ -0,0 +1,245 @@ +# Avatar + +## Examples + +avatar + +```html :::demo +
+ + + + + +
+``` + +avatar with mask + +```html :::demo +
+ + + + +
+``` + +avatar with border(by css). Circle & square types only, but the others are not supported + +```html :::demo +
+ + + +
+
+ + + +
+``` + +avatar-group, default gap(half size) + +```html :::demo + + + + + + + + + + + + + + + + + + + + + +``` + +avatar-group, custom gap + +```html :::demo + + + + + + +99 + + +``` + +avatar with presense indicator + +```html :::demo +
+ + + + +
+ +
+ + + + +
+``` + +avatar placeholder + +```html :::demo +
+ + +99 + + + + + AA + +
+``` + +## Avatar + +### Attributes + +## AvatarGroup + +### Attributes diff --git a/docs/src/pages/components/mask.md b/docs/src/pages/components/mask.md new file mode 100644 index 0000000..92080e2 --- /dev/null +++ b/docs/src/pages/components/mask.md @@ -0,0 +1,53 @@ +# Mask + +## Examples + +mask + +```tsx :::run +export default { + setup: () => { + const types = [ + 'squircle', + 'heart', + 'hexagon', + 'hexagon-2', + 'decagon', + 'pentagon', + 'diamond', + 'square', + 'circle', + 'parallelogram', + 'parallelogram-2', + 'parallelogram-3', + 'parallelogram-4', + 'star', + 'star-2', + 'triangle', + 'triangle-2', + 'triangle-3', + 'triangle-4', + ]; + return () => ( +
+ {types.map((type) => ( +
+

mask-{type}

+ + + +
+ ))} +
+ ); + }, +}; +``` + +## Mask + +### Attributes + +| name | description | type | default | +| ---- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| type | the shape type of content | squircle, heart, hexagon, hexagon-2, decagon, pentagon, diamond, square, circle, parallelogram, parallelogram-2, parallelogram-3, parallelogram-4, star, star-2, triangle, triangle-2, triangle-3, triangle-4 | squircle | diff --git a/src/components/avatar/avatar-group.tsx b/src/components/avatar/avatar-group.tsx new file mode 100644 index 0000000..91deca0 --- /dev/null +++ b/src/components/avatar/avatar-group.tsx @@ -0,0 +1,63 @@ +import { componentV2 } from '@/shared/styled'; +import { ExtractFromProps, ISize } from '@/shared/types/common'; +import { + cloneVNode, + computed, + HTMLAttributes, + PropType, + provide, + reactive, + toRef, +} from 'vue'; +import { ctxAvatarGroupKey, IAvatarGroupCtx, sizeMap } from './state'; +import style from './style'; + +const props = { + size: { + type: [Number, String] as PropType, + default: 'md', + }, + gap: { + type: [Number, String] as PropType, + default: void 0, + }, +}; + +export const AvatrGroup = componentV2< + ExtractFromProps, + HTMLAttributes +>( + { + name: 'AvatarGroup', + props, + setup: (props, { slots }) => { + provide( + ctxAvatarGroupKey, + reactive({ + size: toRef(props, 'size'), + }), + ); + + const gapValue = computed(() => { + const res = props.gap || sizeMap[props.size] / 2 || 24; + return typeof res === 'number' ? `-${res}px` : '-' + res; + }); + + return () => ( +
+ {(slots.default?.() || []).map((v, i) => + cloneVNode(v, { + style: + i === 0 + ? {} + : { + marginLeft: gapValue.value, + }, + }), + )} +
+ ); + }, + }, + style, +); diff --git a/src/components/avatar/avatar.tsx b/src/components/avatar/avatar.tsx new file mode 100644 index 0000000..e5f6817 --- /dev/null +++ b/src/components/avatar/avatar.tsx @@ -0,0 +1,71 @@ +import { IMaskType, Mask } from '../mask'; +import { componentV2 } from '@/shared/styled'; +import { ExtractFromProps, ISize } from '@/shared/types/common'; +import { computed, HTMLAttributes, inject, PropType } from 'vue'; +import style from './style'; +import { ctxAvatarGroupKey, getSizeValue, IAvatarGroupCtx } from './state'; +import { isUndefined } from '@/shared/utils'; + +const props = { + src: String, + placeholder: { + type: String, + default: void 0, + }, + size: { + type: [Number, String] as PropType, + default: 'md', + }, + type: { type: String as PropType, default: 'circle' }, + status: { + type: String as PropType<'online' | 'offline'>, + default: '', + }, +}; + +export type IAvatarProps = ExtractFromProps; + +export const Avatar = componentV2( + { + name: 'Avatar', + props, + setup: (props, { slots }) => { + const ctx = inject(ctxAvatarGroupKey, { size: '' }); + const merged = computed(() => ({ + size: ctx.size || props.size, + })); + + const sizeStyle = computed(() => { + const sizeValue = getSizeValue(merged.value.size); + return { + width: sizeValue, + height: sizeValue, + }; + }); + return () => { + const children = slots.default?.(); + const renderPls = () => + props.placeholder ? {props.placeholder} : null; + const renderImg = () => (props.src ? : null); + + return ( +
+ +
+ {children || renderImg() || renderPls()} +
+
+
+ ); + }; + }, + }, + style, +); diff --git a/src/components/avatar/index.tsx b/src/components/avatar/index.tsx new file mode 100644 index 0000000..238edc2 --- /dev/null +++ b/src/components/avatar/index.tsx @@ -0,0 +1,2 @@ +export * from './avatar'; +export * from './avatar-group'; diff --git a/src/components/avatar/state.ts b/src/components/avatar/state.ts new file mode 100644 index 0000000..2671819 --- /dev/null +++ b/src/components/avatar/state.ts @@ -0,0 +1,19 @@ +import { ISize } from '@/shared/types/common'; +import { cssUnit } from '@/shared/utils'; + +export const ctxAvatarGroupKey = Symbol('AvatarGroup'); + +export const sizeMap: Record = { + xs: 24, + sm: 32, + md: 40, + lg: 64, +}; + +export interface IAvatarGroupCtx { + size: string | number; +} + +export function getSizeValue(size: string | number) { + return cssUnit(sizeMap[size] || size); +} diff --git a/src/components/avatar/style/index.ts b/src/components/avatar/style/index.ts new file mode 100644 index 0000000..b0102eb --- /dev/null +++ b/src/components/avatar/style/index.ts @@ -0,0 +1,8 @@ +import s1 from '@styles/components/unstyled/avatar.css'; +import s2 from '@styles/components/styled/avatar.css'; + +import s3 from '@styles/utilities/styled/avatar.css'; + +import s from './style.less'; + +export default [s1, s2, s3, s]; diff --git a/src/components/avatar/style/style.less b/src/components/avatar/style/style.less new file mode 100644 index 0000000..d1fa23f --- /dev/null +++ b/src/components/avatar/style/style.less @@ -0,0 +1,13 @@ +.dv-avatar { + &-circle { + @apply rounded-full; + } + &.placeholder { + & > .mask { + --tw-text-opacity: 1; + color: hsla(var(--nc) / var(--tw-text-opacity, 1)); + --tw-bg-opacity: 1; + background-color: hsla(var(--nf) / var(--tw-bg-opacity, 1)); + } + } +} diff --git a/src/components/index.tsx b/src/components/index.tsx index 3d8ae1b..84e7393 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -1,9 +1,12 @@ export * from './alert'; export * from './artboard'; +export * from './avatar'; +export * from './avatar'; export * from './badge'; export * from './breadcrumb'; export * from './button'; export * from './drawer'; +export * from './mask'; export * from './menu'; export * from './navbar'; export * from './tab'; diff --git a/src/components/mask/index.tsx b/src/components/mask/index.tsx new file mode 100644 index 0000000..5022c86 --- /dev/null +++ b/src/components/mask/index.tsx @@ -0,0 +1,51 @@ +import { component } from '@/shared/styled'; +import { cloneVNode, PropType } from 'vue'; +import style from './style'; + +export type IMaskType = + | 'squircle' + | 'heart' + | 'hexagon' + | 'hexagon-2' + | 'decagon' + | 'pentagon' + | 'diamond' + | 'square' + | 'circle' + | 'parallelogram' + | 'parallelogram-2' + | 'parallelogram-3' + | 'parallelogram-4' + | 'star' + | 'star-2' + | 'triangle' + | 'triangle-2' + | 'triangle-3' + | 'triangle-4'; + +const props = { + type: { + type: String as PropType, + default: 'squircle', + }, +}; + +export interface IMaskProps { + type?: IMaskType; +} + +export const Mask = component( + { + name: 'Mask', + props, + setup: (props, { slots }) => { + return () => { + const child = slots.default?.()?.[0]; + return cloneVNode(child, { + class: `dv-mask mask mask-${props.type}`, + }); + }; + }, + }, + style, +); diff --git a/src/components/mask/style/index.tsx b/src/components/mask/style/index.tsx new file mode 100644 index 0000000..29e791d --- /dev/null +++ b/src/components/mask/style/index.tsx @@ -0,0 +1,4 @@ +import s1 from '@styles/components/unstyled/mask.css'; +import s2 from '@styles/components/styled/mask.css'; + +export default [s1, s2]; diff --git a/src/shared/styled.ts b/src/shared/styled.ts index fb42b03..d0eba8f 100644 --- a/src/shared/styled.ts +++ b/src/shared/styled.ts @@ -24,7 +24,7 @@ export function insertCss(css: string | string[]) { }); } -export function component( +type IComponentOptions = [ options: { name: string; props?: any; @@ -33,7 +33,7 @@ export function component( setup: ( props: P, ctx: { - attrs: Omit; + attrs: Omit; slots: Slots; emit: any; expose: (exposed?: Record) => void; @@ -41,8 +41,36 @@ export function component( ) => any; [k: string]: any; }, - styles: string[] = [], + styles?: string[], +]; + +/** + * - Attar 组件所有属性类型 + * - Props 仅 props 中的类型 + * @deprecated + * @param options + * @param styles + * @returns + */ +export function component( + ...args: IComponentOptions ): DefineComponent { + const [options, styles = []] = args; insertCss(styles); return defineComponent(options as any); } + +/** + * - Props 类型 + * - Attar props 外的类型,如原生 div 的一些属性 + * @param args + * @returns + */ +export function componentV2( + ...args: IComponentOptions +) { + return component( + // @ts-ignore + ...args, + ); +} diff --git a/src/shared/types/common.ts b/src/shared/types/common.ts index 06e82c4..8b70c10 100644 --- a/src/shared/types/common.ts +++ b/src/shared/types/common.ts @@ -1,3 +1,5 @@ +import { PropType } from 'vue'; + export type ISize = 'xs' | 'sm' | 'md' | 'lg'; export type IStateColor = 'info' | 'success' | 'warning' | 'error'; @@ -12,3 +14,18 @@ export type IColorType = IBrandColor | IStateColor; export type BoolConstructorToBase = { [k in keyof T]: T[k] extends BooleanConstructor ? boolean : T[k]; }; + +type ExtractFromPropType = T extends PropType ? U : T; + +/** + * 从 props 对象类型中反推出类型 + */ +export type ExtractFromProps = { + [k in keyof T]?: ExtractFromPropType< + T[k] extends { + type: any; + } + ? T[k]['type'] + : T[k] + >; +}; diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 823d38d..e28baea 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -1,6 +1,8 @@ +import { VNode } from 'vue'; + export function cssUnit(unit: string | number) { - if (typeof unit === 'number' && Number.isFinite(unit)) { - return unit + 'px'; + if (typeof unit === 'number') { + return (unit || 0) + 'px'; } return unit || ''; } @@ -12,3 +14,18 @@ export function isBool(v: any): boolean { export function isUndefined(v: any): boolean { return typeof v === 'undefined'; } + +export function addClass(str: string, newClass: string | string[]) { + const arr = Array.isArray(newClass) ? newClass : [newClass]; + const classes = new Set(str.split(' ')); + arr.forEach((c) => { + !classes.has(c) && classes.add(c); + }); + + return Array.from(classes).join(' '); +} + +export function removeClass(str: string, removeClass: string) { + const classes = str.split(' '); + return classes.filter((c) => c !== removeClass).join(' '); +}