Skip to content

feat(packages): ui: add tags-input #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/ui/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export * from './skeleton';
export * from './slider';
export * from './switch';
export * from './tabs';
export * from './tags-input';
export * from './textarea';
export * from './toast';
export * from './toggle';
Expand Down
8 changes: 8 additions & 0 deletions packages/ui/src/components/tags-input/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import STagsInputRoot from './tags-input-root.vue';
import STagsInputInput from './tags-input-input.vue';
import STagsInputItem from './tags-input-item.vue';
import STagsInputItemText from './tags-input-item-text.vue';
import STagsInputItemDelete from './tags-input-item-delete.vue';
import STagsInput from './tags-input.vue';

export { STagsInputRoot, STagsInputInput, STagsInputItem, STagsInputItemText, STagsInputItemDelete, STagsInput };
24 changes: 24 additions & 0 deletions packages/ui/src/components/tags-input/tags-input-input.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup lang="ts">
import { computed } from 'vue';
import { TagsInputInput } from '@soybean-ui/primitives';
import { cn, tagsInputVariants } from '@soybean-ui/variants';
import type { TagsInputInputProps } from './types';

defineOptions({
name: 'STagsInputInput'
});

const { class: cls, size, ...delegatedProps } = defineProps<TagsInputInputProps>();

const mergedCls = computed(() => {
const { input } = tagsInputVariants({ size });

return cn(input(), cls);
});
</script>

<template>
<TagsInputInput v-bind="delegatedProps" :class="mergedCls">
<slot />
</TagsInputInput>
</template>
32 changes: 32 additions & 0 deletions packages/ui/src/components/tags-input/tags-input-item-delete.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script setup lang="ts">
import { computed } from 'vue';
import { Slot, TagsInputItemDelete } from '@soybean-ui/primitives';
import { cn, tagsInputVariants } from '@soybean-ui/variants';
import { X } from 'lucide-vue-next';
import type { TagsInputItemDeleteProps } from './types';

defineOptions({
name: 'STagsInputItemDelete'
});

const { class: cls, size, iconClass } = defineProps<TagsInputItemDeleteProps>();

const mergedCls = computed(() => {
const { itemDelete, itemDeleteIcon } = tagsInputVariants({ size });

return {
cls: cn(itemDelete(), cls),
icon: cn(itemDeleteIcon(), iconClass)
};
});
</script>

<template>
<TagsInputItemDelete :class="mergedCls.cls">
<Slot :class="mergedCls.icon">
<slot>
<X />
</slot>
</Slot>
</TagsInputItemDelete>
</template>
24 changes: 24 additions & 0 deletions packages/ui/src/components/tags-input/tags-input-item-text.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup lang="ts">
import { computed } from 'vue';
import { TagsInputItemText } from '@soybean-ui/primitives';
import { cn, tagsInputVariants } from '@soybean-ui/variants';
import type { TagsInputItemTextProps } from './types';

defineOptions({
name: 'STagsInputItemText'
});

const { class: cls, size } = defineProps<TagsInputItemTextProps>();

const mergedCls = computed(() => {
const { itemText } = tagsInputVariants({ size });

return cn(itemText(), cls);
});
</script>

<template>
<TagsInputItemText :class="mergedCls">
<slot />
</TagsInputItemText>
</template>
24 changes: 24 additions & 0 deletions packages/ui/src/components/tags-input/tags-input-item.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup lang="ts">
import { computed } from 'vue';
import { TagsInputItem } from '@soybean-ui/primitives';
import { cn, tagsInputVariants } from '@soybean-ui/variants';
import type { TagsInputItemProps } from './types';

defineOptions({
name: 'STagsInputItem'
});

const { class: cls, size, ...delegatedProps } = defineProps<TagsInputItemProps>();

const mergedCls = computed(() => {
const { item } = tagsInputVariants({ size });

return cn(item(), cls);
});
</script>

<template>
<TagsInputItem v-bind="delegatedProps" :class="mergedCls">
<slot />
</TagsInputItem>
</template>
29 changes: 29 additions & 0 deletions packages/ui/src/components/tags-input/tags-input-root.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts" generic="T extends AcceptableInputValue = AcceptableInputValue">
import { computed } from 'vue';
import { TagsInputRoot, useForwardPropsEmits } from '@soybean-ui/primitives';
import type { AcceptableInputValue } from '@soybean-ui/primitives';
import { cn, tagsInputVariants } from '@soybean-ui/variants';
import type { TagsInputRootEmits, TagsInputRootProps } from './types';

defineOptions({
name: 'STagsInputRoot'
});

const { class: cls, size, ...delegatedProps } = defineProps<TagsInputRootProps<T>>();

const emit = defineEmits<TagsInputRootEmits<T>>();

const mergedCls = computed(() => {
const { root } = tagsInputVariants({ size });

return cn(root(), cls);
});

const forwarded = useForwardPropsEmits(delegatedProps, emit);
</script>

<template>
<TagsInputRoot v-bind="forwarded" :class="mergedCls">
<slot />
</TagsInputRoot>
</template>
55 changes: 55 additions & 0 deletions packages/ui/src/components/tags-input/tags-input.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<script setup lang="ts" generic="T extends AcceptableInputValue = AcceptableInputValue">
import { useForwardPropsEmits } from '@soybean-ui/primitives';
import type { AcceptableInputValue } from '@soybean-ui/primitives';
import STagsInputRoot from './tags-input-root.vue';
import STagsInputInput from './tags-input-input.vue';
import STagsInputItem from './tags-input-item.vue';
import STagsInputItemText from './tags-input-item-text.vue';
import STagsInputItemDelete from './tags-input-item-delete.vue';
import type { TagsInputEmits, TagsInputProps } from './types';

defineOptions({
name: 'STagsInput'
});

const {
class: cls,
ui,
disabledValue,
placeholder,
autoFocus,
maxLength,
...delegatedProps
} = defineProps<TagsInputProps<T>>();

const emit = defineEmits<TagsInputEmits<T>>();

const forwarded = useForwardPropsEmits(delegatedProps, emit);
</script>

<template>
<STagsInputRoot v-bind="forwarded" :class="cls || ui?.root">
<template v-for="item in modelValue" :key="item">
<slot name="item" :value="item">
<STagsInputItem
:class="ui?.item"
:size="size"
:value="item"
:disabled="disabled || disabledValue?.includes(item)"
>
<STagsInputItemText :class="ui?.itemText" :size="size" />
<STagsInputItemDelete :class="ui?.itemDelete" :icon-class="ui?.itemDeleteIcon" :size="size">
<slot name="item-delete-icon" :value="item" />
</STagsInputItemDelete>
</STagsInputItem>
</slot>
</template>
<STagsInputInput
:class="ui?.input"
:placeholder="placeholder"
:auto-focus="autoFocus"
:max-length="maxLength"
:size="size"
/>
</STagsInputRoot>
</template>
43 changes: 43 additions & 0 deletions packages/ui/src/components/tags-input/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type {
AcceptableInputValue,
ClassValue,
TagsInputRootEmits,
TagsInputInputProps as _TagsInputInputProps,
TagsInputItemDeleteProps as _TagsInputItemDeleteProps,
TagsInputItemProps as _TagsInputItemProps,
TagsInputItemTextProps as _TagsInputItemTextProps,
TagsInputRootProps as _TagsInputRootProps
} from '@soybean-ui/primitives';
import type { TagsInputSlots, ThemeSize } from '@soybean-ui/variants';

export interface TagsInputRootProps<T extends AcceptableInputValue> extends _TagsInputRootProps<T> {
size?: ThemeSize;
}

export interface TagsInputInputProps extends _TagsInputInputProps {
size?: ThemeSize;
}

export interface TagsInputItemProps extends _TagsInputItemProps {
size?: ThemeSize;
}

export interface TagsInputItemTextProps extends _TagsInputItemTextProps {
size?: ThemeSize;
}

export interface TagsInputItemDeleteProps extends _TagsInputItemDeleteProps {
size?: ThemeSize;
iconClass?: ClassValue;
}

export type TagsInputUi = Partial<Record<TagsInputSlots, ClassValue>>;

export interface TagsInputProps<T extends AcceptableInputValue> extends TagsInputRootProps<T>, TagsInputInputProps {
disabledValue?: T[];
ui?: TagsInputUi;
}

export type TagsInputEmits<T extends AcceptableInputValue> = TagsInputRootEmits<T>;

export type { TagsInputRootEmits };
1 change: 1 addition & 0 deletions packages/variants/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export * from './variants/skeleton';
export * from './variants/slider';
export * from './variants/switch';
export * from './variants/tabs';
export * from './variants/tags-input';
export * from './variants/textarea';
export * from './variants/toast';
export * from './variants/toggle';
Expand Down
73 changes: 73 additions & 0 deletions packages/variants/src/variants/tags-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// @unocss-include
import { tv } from 'tailwind-variants';

export const tagsInputVariants = tv({
slots: {
root: [
`flex flex-wrap gap-2 items-center rounded-md border border-input bg-background`,
`focus-within:(ring-2 ring-offset-2 ring-primary) disabled:(cursor-not-allowed opacity-50)`
],
input: `focus:outline-none flex-1 bg-transparent`,
item: `flex items-center rounded-md bg-accent data-[state=active]:(ring-2 ring-offset-2 ring-accent-foreground/20) ring-offset-background`,
itemText: `rounded bg-transparent`,
itemDelete: `flex rounded bg-transparent mr-1 text-foreground hover:text-foreground/80`,
itemDeleteIcon: `cursor-pointer`
},
variants: {
size: {
xs: {
root: 'px-1.5 py-0.75 text-xs',
input: 'min-h-4 px-1.5 text-xs',
item: 'h-4',
itemText: 'py-0.75 px-1.5 text-xs',
itemDelete: 'mr-1.5',
itemDeleteIcon: 'size-3'
},
sm: {
root: 'px-2 py-0.75 text-sm',
input: 'min-h-5 px-2 text-sm',
item: 'h-5',
itemText: 'py-0.75 px-2 text-sm',
itemDelete: 'mr-2',
itemDeleteIcon: 'size-3.5'
},
md: {
root: 'px-2.5 py-1 text-sm',
input: 'min-h-5.5 px-2.5 text-sm',
item: 'h-5.5',
itemText: 'py-1 px-2.5 text-sm',
itemDelete: 'mr-2.5',
itemDeleteIcon: 'size-4'
},
lg: {
root: 'px-3 py-1.25 text-base',
input: 'min-h-6 px-3 text-base',
item: 'h-6',
itemText: 'py-1.25 px-3 text-base',
itemDelete: 'mr-3',
itemDeleteIcon: 'size-4.5'
},
xl: {
root: 'px-3.5 py-1.5 text-base',
input: 'min-h-6.5 px-3.5 text-base',
item: 'h-6.5',
itemText: 'py-1.5 px-3.5 text-base',
itemDelete: 'mr-3.5',
itemDeleteIcon: 'size-5'
},
xxl: {
root: 'px-4 py-1.75 text-lg',
input: 'min-h-8 px-4 text-lg',
item: 'h-8',
itemText: 'py-1.75 px-4 text-lg',
itemDelete: 'mr-4',
itemDeleteIcon: 'size-5.5'
}
}
},
defaultVariants: {
size: 'md'
}
});

export type TagsInputSlots = keyof typeof tagsInputVariants.slots;
6 changes: 6 additions & 0 deletions src/views/ui/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import UiSlider from './modules/slider.vue';
import UiSonner from './modules/sonner.vue';
import UiSwitch from './modules/switch.vue';
import UiTabs from './modules/tabs.vue';
import UiTagsInput from './modules/tags-input.vue';
import UiTextarea from './modules/textarea.vue';
import UiToast from './modules/toast.vue';
import UiToggle from './modules/toggle.vue';
Expand Down Expand Up @@ -268,6 +269,11 @@ const tabs: TabConfig[] = [
label: 'Tabs',
component: UiTabs
},
{
value: 'tags-input',
label: 'TagsInput',
component: UiTagsInput
},
{
value: 'textarea',
label: 'Textarea',
Expand Down
25 changes: 25 additions & 0 deletions src/views/ui/modules/tags-input.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script setup lang="ts">
import { ref } from 'vue';
import { STagsInput } from 'soy-ui';
import type { ThemeSize } from 'soy-ui';

const tags = ref(['Apple', 'Banana', 'Cherry']);

const sizes: ThemeSize[] = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'];

defineOptions({
name: 'UiTagsInput'
});
</script>

<template>
<div>
<div class="py-12px text-18px">Size</div>
<div v-for="size in sizes" :key="size">
<div class="py-12px text-18px">{{ size }}</div>
<STagsInput v-model="tags" :size="size" />
</div>
</div>
</template>

<style scoped></style>