Skip to content

Commit

Permalink
feat: Add UI components for input, label, card, form, and form control.
Browse files Browse the repository at this point in the history
 These components are necessary for implementing the User Registration feature.

Implementations not finished
  • Loading branch information
lskellerm committed Aug 14, 2024
1 parent 04fdccb commit da41d6e
Show file tree
Hide file tree
Showing 21 changed files with 367 additions and 0 deletions.
29 changes: 29 additions & 0 deletions frontend/components/ui/button/Button.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { Primitive, type PrimitiveProps } from 'radix-vue';
import { type ButtonVariants, buttonVariants } from '.';
import { cn } from '@/lib/utils';
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant'];
size?: ButtonVariants['size'];
class?: HTMLAttributes['class'];
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
variant: 'default',
size: 'default',
class: ''
});
</script>

<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>
37 changes: 37 additions & 0 deletions frontend/components/ui/button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { type VariantProps, cva } from 'class-variance-authority';

export { default as Button } from './Button.vue';

export const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-heading font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-slate-300',
{
variants: {
variant: {
default:
'bg-primaryColor text-white shadow hover:bg-primaryColor/90 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50/90',
destructive:
'bg-red-500 text-slate-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-slate-50 dark:hover:bg-red-900/90',
outline:
'border border-slate-200 bg-white shadow-sm hover:bg-slate-100 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50',
secondary:
'bg-slate-100 text-slate-900 shadow-sm hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-50 dark:hover:bg-slate-800/80',
ghost:
'hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-slate-50',
link: 'text-slate-900 underline-offset-4 hover:underline dark:text-slate-50'
},
size: {
default: 'h-9 px-4 py-2',
xs: 'h-7 rounded-md px-2 text-xs',
sm: 'h-8 rounded-lg px-3 text-xs',
lg: 'h-10 rounded-lg px-8',
icon: 'h-9 w-9'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
);

export type ButtonVariants = VariantProps<typeof buttonVariants>;
21 changes: 21 additions & 0 deletions frontend/components/ui/card/Card.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>

<template>
<div
:class="
cn(
'rounded-xl border border-slate-200 bg-white text-slate-950 shadow dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50',
props.class
)
"
>
<slot />
</div>
</template>
14 changes: 14 additions & 0 deletions frontend/components/ui/card/CardContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>

<template>
<div :class="cn('p-6 pt-0', props.class)">
<slot />
</div>
</template>
14 changes: 14 additions & 0 deletions frontend/components/ui/card/CardDescription.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>

<template>
<p :class="cn('text-sm text-slate-500 dark:text-slate-400', props.class)">
<slot />
</p>
</template>
14 changes: 14 additions & 0 deletions frontend/components/ui/card/CardFooter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>

<template>
<div :class="cn('flex items-center p-6 pt-0', props.class)">
<slot />
</div>
</template>
14 changes: 14 additions & 0 deletions frontend/components/ui/card/CardHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>

<template>
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
<slot />
</div>
</template>
14 changes: 14 additions & 0 deletions frontend/components/ui/card/CardTitle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>

<template>
<h3 :class="cn('font-semibold leading-none tracking-tight', props.class)">
<slot />
</h3>
</template>
6 changes: 6 additions & 0 deletions frontend/components/ui/card/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { default as Card } from './Card.vue';
export { default as CardHeader } from './CardHeader.vue';
export { default as CardTitle } from './CardTitle.vue';
export { default as CardDescription } from './CardDescription.vue';
export { default as CardContent } from './CardContent.vue';
export { default as CardFooter } from './CardFooter.vue';
18 changes: 18 additions & 0 deletions frontend/components/ui/form/FormControl.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script lang="ts" setup>
import { Slot } from 'radix-vue';
import { useFormField } from './useFormField';
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
</script>

<template>
<Slot
:id="formItemId"
:aria-describedby="
!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`
"
:aria-invalid="!!error"
>
<slot />
</Slot>
</template>
20 changes: 20 additions & 0 deletions frontend/components/ui/form/FormDescription.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { useFormField } from './useFormField';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
const { formDescriptionId } = useFormField();
</script>

<template>
<p
:id="formDescriptionId"
:class="cn('text-sm text-slate-500 dark:text-slate-400', props.class)"
>
<slot />
</p>
</template>
19 changes: 19 additions & 0 deletions frontend/components/ui/form/FormItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script lang="ts" setup>
import { type HTMLAttributes, provide } from 'vue';
import { useId } from 'radix-vue';
import { FORM_ITEM_INJECTION_KEY } from './injectionKeys';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
const id = useId();
provide(FORM_ITEM_INJECTION_KEY, id);
</script>

<template>
<div :class="cn('space-y-2', props.class)">
<slot />
</div>
</template>
20 changes: 20 additions & 0 deletions frontend/components/ui/form/FormLabel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import type { LabelProps } from 'radix-vue';
import { useFormField } from './useFormField';
import { cn } from '@/lib/utils';
import { Label } from '@/components/ui/label';
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>();
const { error, formItemId } = useFormField();
</script>

<template>
<Label
:class="cn(error && 'text-red-500 dark:text-red-900', props.class)"
:for="formItemId"
>
<slot />
</Label>
</template>
16 changes: 16 additions & 0 deletions frontend/components/ui/form/FormMessage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts" setup>
import { ErrorMessage } from 'vee-validate';
import { toValue } from 'vue';
import { useFormField } from './useFormField';
const { name, formMessageId } = useFormField();
</script>

<template>
<ErrorMessage
:id="formMessageId"
as="p"
:name="toValue(name)"
class="text-[0.8rem] font-medium text-red-500 dark:text-red-900"
/>
</template>
11 changes: 11 additions & 0 deletions frontend/components/ui/form/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export {
Form,
Field as FormField,
FieldArray as FormFieldArray
} from 'vee-validate';
export { default as FormItem } from './FormItem.vue';
export { default as FormLabel } from './FormLabel.vue';
export { default as FormControl } from './FormControl.vue';
export { default as FormMessage } from './FormMessage.vue';
export { default as FormDescription } from './FormDescription.vue';
export { FORM_ITEM_INJECTION_KEY } from './injectionKeys';
3 changes: 3 additions & 0 deletions frontend/components/ui/form/injectionKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { InjectionKey } from 'vue';

export const FORM_ITEM_INJECTION_KEY = Symbol() as InjectionKey<string>;
36 changes: 36 additions & 0 deletions frontend/components/ui/form/useFormField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
FieldContextKey,
useFieldError,
useIsFieldDirty,
useIsFieldTouched,
useIsFieldValid
} from 'vee-validate';
import { inject } from 'vue';
import { FORM_ITEM_INJECTION_KEY } from './injectionKeys';

export function useFormField() {
const fieldContext = inject(FieldContextKey);
const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY);

if (!fieldContext)
throw new Error('useFormField should be used within <FormField>');

const { name } = fieldContext;
const id = fieldItemContext;

const fieldState = {
valid: useIsFieldValid(name),
isDirty: useIsFieldDirty(name),
isTouched: useIsFieldTouched(name),
error: useFieldError(name)
};

return {
id,
name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState
};
}
32 changes: 32 additions & 0 deletions frontend/components/ui/input/Input.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { useVModel } from '@vueuse/core';
import { cn } from '@/lib/utils';
const props = defineProps<{
defaultValue?: string | number;
modelValue?: string | number;
class?: HTMLAttributes['class'];
}>();
const emits = defineEmits<{
(e: 'update:modelValue', payload: string | number): void; // eslint-disable-line
}>();
const modelValue = useVModel(props, 'modelValue', emits, {
passive: true,
defaultValue: props.defaultValue
});
</script>

<template>
<input
v-model="modelValue"
:class="
cn(
'flex h-9 w-full rounded-md border border-slate-200 bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-800 dark:placeholder:text-slate-400 dark:focus-visible:ring-slate-300',
props.class
)
"
/>
</template>
1 change: 1 addition & 0 deletions frontend/components/ui/input/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Input } from './Input.vue';
27 changes: 27 additions & 0 deletions frontend/components/ui/label/Label.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue';
import { Label, type LabelProps } from 'radix-vue';
import { cn } from '@/lib/utils';
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props; // eslint-disable-line
return delegated;
});
</script>

<template>
<Label
v-bind="delegatedProps"
:class="
cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
props.class
)
"
>
<slot />
</Label>
</template>
1 change: 1 addition & 0 deletions frontend/components/ui/label/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Label } from './Label.vue';

0 comments on commit da41d6e

Please sign in to comment.