mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-14 19:22:14 +01:00
Add Field component system and migrate UI
This commit is contained in:
@@ -18,12 +18,11 @@ export const buttonVariants = cva(
|
||||
input: 'border-input-border border bg-input-background text-text-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent shadow-sm',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
xs: 'h-7 rounded px-2',
|
||||
default: 'h-9 px-3 text-sm',
|
||||
xs: 'h-7 rounded px-2 text-xs',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
lg: 'h-10 rounded-md px-4',
|
||||
icon: 'h-9 w-9',
|
||||
input: 'h-[42px] px-3 py-2 text-base',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import EstimatedTimeInput from '@/packages/ui/src/Input/EstimatedTimeInput.vue';
|
||||
import { ClockIcon } from '@heroicons/vue/20/solid';
|
||||
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
||||
import { Field, FieldLabel } from './field';
|
||||
|
||||
const model = defineModel<number | null>();
|
||||
const emit = defineEmits(['submit']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pt-6">
|
||||
<div class="flex items-center space-x-1 mb-2">
|
||||
<Field class="pt-6">
|
||||
<div class="flex items-center space-x-1">
|
||||
<ClockIcon class="text-text-quaternary w-4"></ClockIcon>
|
||||
<InputLabel for="billable" value="Time Estimated" />
|
||||
<FieldLabel for="billable">Time Estimated</FieldLabel>
|
||||
</div>
|
||||
<EstimatedTimeInput v-model="model" @submit="emit('submit')"></EstimatedTimeInput>
|
||||
<div class="flex items-center text-text-secondary text-xs pt-2 pl-1">
|
||||
@@ -21,7 +21,7 @@ const emit = defineEmits(['submit']);
|
||||
<span class="font-semibold">2h 30m</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -75,12 +75,12 @@ const emit = defineEmits(['update:modelValue', 'changed', 'submit']);
|
||||
<template #content>
|
||||
<ComboboxRoot
|
||||
v-model:search-term="searchValue"
|
||||
:open="open"
|
||||
v-model:open="open"
|
||||
class="p-2"
|
||||
:filter-function="(val: string[]) => val">
|
||||
<ComboboxAnchor>
|
||||
<ComboboxInput
|
||||
class="w-full rounded-md border border-input-border bg-input-background px-3 py-1.5 text-sm text-text-primary placeholder:text-text-tertiary focus:outline-none"
|
||||
class="w-full h-8 rounded-md border border-input-border bg-input-background px-3 text-sm text-text-primary placeholder:text-text-tertiary focus:outline-none"
|
||||
:placeholder="searchPlaceholder" />
|
||||
</ComboboxAnchor>
|
||||
<ComboboxContent
|
||||
|
||||
@@ -25,7 +25,7 @@ const model = defineModel();
|
||||
v-model="model"
|
||||
:class="
|
||||
twMerge(
|
||||
'border-input-border border bg-input-background text-text-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent rounded-md shadow-sm',
|
||||
'h-9 px-3 py-1 text-sm border-input-border border bg-input-background text-text-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent rounded-md shadow-sm',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ref, watch } from 'vue';
|
||||
import { getLocalizedDayJs } from '@/packages/ui/src/utils/time';
|
||||
import { useFocus } from '@vueuse/core';
|
||||
import { TextInput } from '@/packages/ui/src';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
// This has to be a localized timestamp, not UTC
|
||||
const model = defineModel<string | null>({
|
||||
@@ -12,11 +11,9 @@ const model = defineModel<string | null>({
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
size?: 'base' | 'large';
|
||||
focus?: boolean;
|
||||
}>(),
|
||||
{
|
||||
size: 'base',
|
||||
focus: false,
|
||||
}
|
||||
);
|
||||
@@ -101,7 +98,7 @@ const inputValue = ref(model.value ? getLocalizedDayJs(model.value).format('HH:m
|
||||
v-bind="$attrs"
|
||||
ref="timeInput"
|
||||
v-model="inputValue"
|
||||
:class="twMerge('text-center w-24 px-3 py-2', size === 'large' && 'w-28')"
|
||||
class="text-center w-full"
|
||||
data-testid="time_picker_input"
|
||||
type="text"
|
||||
@blur="updateTime"
|
||||
|
||||
@@ -12,7 +12,7 @@ import Badge from '@/packages/ui/src/Badge.vue';
|
||||
import ProjectColorSelector from '@/packages/ui/src/Project/ProjectColorSelector.vue';
|
||||
import { UserCircleIcon } from '@heroicons/vue/20/solid';
|
||||
import EstimatedTimeSection from '@/packages/ui/src/EstimatedTimeSection.vue';
|
||||
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
||||
import { Field, FieldLabel } from '../field';
|
||||
import ProjectEditBillableSection from '@/packages/ui/src/Project/ProjectEditBillableSection.vue';
|
||||
import type { Client } from '@/packages/api/src';
|
||||
|
||||
@@ -76,14 +76,12 @@ const currentClientName = computed(() => {
|
||||
<template #content>
|
||||
<div class="sm:flex items-center space-y-2 sm:space-y-0 sm:space-x-4">
|
||||
<div class="flex-1 flex items-center">
|
||||
<div class="text-center pr-5">
|
||||
<InputLabel for="color" value="Color" />
|
||||
<ProjectColorSelector
|
||||
v-model="project.color"
|
||||
class="mt-2.5"></ProjectColorSelector>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<InputLabel for="projectName" value="Project name" />
|
||||
<Field class="text-center pr-5">
|
||||
<FieldLabel for="color">Color</FieldLabel>
|
||||
<ProjectColorSelector v-model="project.color"></ProjectColorSelector>
|
||||
</Field>
|
||||
<Field class="w-full">
|
||||
<FieldLabel for="projectName">Project name</FieldLabel>
|
||||
<TextInput
|
||||
id="projectName"
|
||||
ref="projectNameInput"
|
||||
@@ -91,19 +89,18 @@ const currentClientName = computed(() => {
|
||||
name="projectName"
|
||||
type="text"
|
||||
placeholder="The next big thing"
|
||||
class="mt-2 block w-full"
|
||||
class="block w-full"
|
||||
required
|
||||
autocomplete="projectName"
|
||||
@keydown.enter="submit()" />
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="client" value="Client" />
|
||||
<Field>
|
||||
<FieldLabel for="client">Client</FieldLabel>
|
||||
<ClientDropdown
|
||||
v-model="project.client_id"
|
||||
:create-client="createClient"
|
||||
:clients="activeClients"
|
||||
class="mt-2">
|
||||
:clients="activeClients">
|
||||
<template #trigger>
|
||||
<Badge
|
||||
tag="button"
|
||||
@@ -118,7 +115,7 @@ const currentClientName = computed(() => {
|
||||
</Badge>
|
||||
</template>
|
||||
</ClientDropdown>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
||||
import { Field, FieldLabel } from '../field';
|
||||
import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue';
|
||||
import ProjectBillableSelect from '@/packages/ui/src/Project/ProjectBillableSelect.vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
@@ -55,23 +55,21 @@ const emit = defineEmits(['submit']);
|
||||
|
||||
<template>
|
||||
<div class="sm:flex items-center space-y-2 sm:space-y-0 sm:space-x-4 pt-6">
|
||||
<div>
|
||||
<div class="flex items-center space-x-1 mb-2">
|
||||
<Field>
|
||||
<div class="flex items-center space-x-1">
|
||||
<BillableIcon class="text-text-quaternary h-4 ml-1 mr-0.5"></BillableIcon>
|
||||
<InputLabel for="billable" value="Billable Default" />
|
||||
<FieldLabel for="billable">Billable Default</FieldLabel>
|
||||
</div>
|
||||
<ProjectBillableSelect
|
||||
v-model="billableRateSelect"
|
||||
class="mt-2"></ProjectBillableSelect>
|
||||
</div>
|
||||
<div v-if="billableRateSelect === 'custom-rate'">
|
||||
<InputLabel for="billableRate" value="Billable Rate" class="mb-2" />
|
||||
<ProjectBillableSelect v-model="billableRateSelect"></ProjectBillableSelect>
|
||||
</Field>
|
||||
<Field v-if="billableRateSelect === 'custom-rate'">
|
||||
<FieldLabel for="billableRate">Billable Rate</FieldLabel>
|
||||
<BillableRateInput
|
||||
v-model="billableRate"
|
||||
:currency="currency"
|
||||
name="billableRate"
|
||||
@keydown.enter="emit('submit')" />
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="flex items-center text-text-secondary text-xs pt-2 pl-1">
|
||||
<span>
|
||||
|
||||
@@ -5,7 +5,7 @@ import DialogModal from '@/packages/ui/src/DialogModal.vue';
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
||||
import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
|
||||
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
||||
import { Field, FieldLabel } from '../field';
|
||||
import { TagIcon } from '@heroicons/vue/20/solid';
|
||||
import { getDayJsInstance, getLocalizedDayJs } from '@/packages/ui/src/utils/time';
|
||||
import type {
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/Components/ui/select';
|
||||
import { Button } from '@/Components/ui/button';
|
||||
import { Button } from '@/packages/ui/src/Buttons';
|
||||
import DatePicker from '@/packages/ui/src/Input/DatePicker.vue';
|
||||
import DurationHumanInput from '@/packages/ui/src/Input/DurationHumanInput.vue';
|
||||
|
||||
@@ -165,6 +165,7 @@ const billableProxy = computed({
|
||||
v-model:project="timeEntry.project_id"
|
||||
v-model:task="timeEntry.task_id"
|
||||
variant="input"
|
||||
size="default"
|
||||
:clients
|
||||
:create-project
|
||||
:create-client
|
||||
@@ -181,7 +182,7 @@ const billableProxy = computed({
|
||||
:tags="tags"
|
||||
:show-no-tag-option="false">
|
||||
<template #trigger>
|
||||
<Button variant="input" size="sm">
|
||||
<Button variant="input">
|
||||
<TagIcon class="h-4 text-icon-default" />
|
||||
<span>{{
|
||||
timeEntry.tags.length === 0
|
||||
@@ -192,7 +193,7 @@ const billableProxy = computed({
|
||||
</template>
|
||||
</TagDropdown>
|
||||
<Select v-model="billableProxy">
|
||||
<SelectTrigger size="small" :show-chevron="false">
|
||||
<SelectTrigger :show-chevron="false">
|
||||
<SelectValue class="flex items-center gap-2">
|
||||
<BillableIcon class="h-4 text-icon-default" />
|
||||
<span>{{ timeEntry.billable ? 'Billable' : 'Non-Billable' }}</span>
|
||||
@@ -206,9 +207,9 @@ const billableProxy = computed({
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row gap-4 pt-4">
|
||||
<div class="flex-1">
|
||||
<InputLabel>Duration</InputLabel>
|
||||
<div class="space-y-2 mt-1 flex flex-col">
|
||||
<Field class="flex-1">
|
||||
<FieldLabel>Duration</FieldLabel>
|
||||
<div class="space-y-2 flex flex-col">
|
||||
<DurationHumanInput
|
||||
v-model:start="localStart"
|
||||
v-model:end="localEnd"
|
||||
@@ -222,31 +223,27 @@ const billableProxy = computed({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
<div class="grid grid-cols-2 sm:flex gap-4">
|
||||
<div>
|
||||
<InputLabel>Start</InputLabel>
|
||||
<div class="flex flex-col gap-2 mt-1 sm:w-28">
|
||||
<Field>
|
||||
<FieldLabel>Start</FieldLabel>
|
||||
<div class="flex flex-col gap-2 sm:w-28">
|
||||
<TimePickerSimple
|
||||
v-model="localStart"
|
||||
class="w-full"
|
||||
size="large"></TimePickerSimple>
|
||||
class="w-full"></TimePickerSimple>
|
||||
<DatePicker
|
||||
v-model="localStart"
|
||||
class="w-full"
|
||||
tabindex="1"></DatePicker>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel>End</InputLabel>
|
||||
<div class="flex flex-col gap-2 mt-1 sm:w-28">
|
||||
<TimePickerSimple
|
||||
v-model="localEnd"
|
||||
class="w-full"
|
||||
size="large"></TimePickerSimple>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>End</FieldLabel>
|
||||
<div class="flex flex-col gap-2 sm:w-28">
|
||||
<TimePickerSimple v-model="localEnd" class="w-full"></TimePickerSimple>
|
||||
<DatePicker v-model="localEnd" class="w-full" tabindex="1"></DatePicker>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,7 +5,7 @@ import DialogModal from '@/packages/ui/src/DialogModal.vue';
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
||||
import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
|
||||
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
||||
import { Field, FieldLabel } from '../field';
|
||||
import { TagIcon } from '@heroicons/vue/20/solid';
|
||||
import { getLocalizedDayJs } from '@/packages/ui/src/utils/time';
|
||||
import type {
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/Components/ui/select';
|
||||
import { Button } from '@/Components/ui/button';
|
||||
import { Button } from '@/packages/ui/src/Buttons';
|
||||
import DatePicker from '@/packages/ui/src/Input/DatePicker.vue';
|
||||
import DurationHumanInput from '@/packages/ui/src/Input/DurationHumanInput.vue';
|
||||
|
||||
@@ -199,7 +199,7 @@ const billableProxy = computed({
|
||||
</template>
|
||||
</TagDropdown>
|
||||
<Select v-model="billableProxy">
|
||||
<SelectTrigger size="small" :show-chevron="false">
|
||||
<SelectTrigger size="sm" :show-chevron="false">
|
||||
<SelectValue class="flex items-center gap-2">
|
||||
<BillableIcon class="h-4 text-icon-default" />
|
||||
<span>{{
|
||||
@@ -215,9 +215,9 @@ const billableProxy = computed({
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row gap-4 pt-4">
|
||||
<div class="flex-1">
|
||||
<InputLabel>Duration</InputLabel>
|
||||
<div class="space-y-2 mt-1 flex flex-col">
|
||||
<Field class="flex-1">
|
||||
<FieldLabel>Duration</FieldLabel>
|
||||
<div class="space-y-2 flex flex-col">
|
||||
<DurationHumanInput
|
||||
v-model:start="localStart"
|
||||
v-model:end="localEnd"
|
||||
@@ -231,34 +231,32 @@ const billableProxy = computed({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
<div class="grid grid-cols-2 sm:flex gap-4">
|
||||
<div>
|
||||
<InputLabel>Start</InputLabel>
|
||||
<div class="flex flex-col gap-2 mt-1 sm:w-28">
|
||||
<Field>
|
||||
<FieldLabel>Start</FieldLabel>
|
||||
<div class="flex flex-col gap-2 sm:w-28">
|
||||
<TimePickerSimple
|
||||
v-model="localStart"
|
||||
class="w-full"
|
||||
size="large"></TimePickerSimple>
|
||||
class="w-full"></TimePickerSimple>
|
||||
<DatePicker
|
||||
v-model="localStart"
|
||||
class="w-full"
|
||||
tabindex="1"></DatePicker>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel>End</InputLabel>
|
||||
<div class="flex flex-col gap-2 mt-1 sm:w-28">
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>End</FieldLabel>
|
||||
<div class="flex flex-col gap-2 sm:w-28">
|
||||
<TimePickerSimple
|
||||
v-model="localEnd"
|
||||
class="w-full"
|
||||
size="large"></TimePickerSimple>
|
||||
class="w-full"></TimePickerSimple>
|
||||
<DatePicker
|
||||
v-model="localEnd"
|
||||
class="w-full"
|
||||
tabindex="1"></DatePicker>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,8 @@ import type {
|
||||
} from '@/packages/api/src';
|
||||
import { ref } from 'vue';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { Checkbox, InputLabel } from '@/packages/ui/src';
|
||||
import { Checkbox } from '@/packages/ui/src';
|
||||
import { FieldLabel } from '../field';
|
||||
|
||||
const props = defineProps<{
|
||||
selectedTimeEntries: TimeEntry[];
|
||||
@@ -71,14 +72,14 @@ const showMassUpdateModal = ref(false);
|
||||
:checked="allSelected"
|
||||
@update:checked="allSelected ? emit('unselectAll') : emit('selectAll')">
|
||||
</Checkbox>
|
||||
<InputLabel
|
||||
<FieldLabel
|
||||
v-if="selectedTimeEntries.length > 0"
|
||||
for="selectAll"
|
||||
class="select-none text-text-secondary">
|
||||
{{ selectedTimeEntries.length }} selected
|
||||
</InputLabel>
|
||||
<InputLabel v-else for="selectAll" class="text-text-secondary select-none"
|
||||
>Select All</InputLabel
|
||||
</FieldLabel>
|
||||
<FieldLabel v-else for="selectAll" class="text-text-secondary select-none"
|
||||
>Select All</FieldLabel
|
||||
>
|
||||
<button
|
||||
v-if="selectedTimeEntries.length"
|
||||
|
||||
@@ -5,7 +5,7 @@ import DialogModal from '@/packages/ui/src/DialogModal.vue';
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import PrimaryButton from '../Buttons/PrimaryButton.vue';
|
||||
import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
|
||||
import InputLabel from '../Input/InputLabel.vue';
|
||||
import { Field, FieldLabel } from '../field';
|
||||
import {
|
||||
type CreateClientBody,
|
||||
type CreateProjectBody,
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/Components/ui/select';
|
||||
import { Button } from '@/Components/ui/button';
|
||||
import { Button } from '@/packages/ui/src/Buttons';
|
||||
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
|
||||
import type { Tag, Task } from '@/packages/api/src';
|
||||
|
||||
@@ -146,18 +146,18 @@ watch(removeAllTags, () => {
|
||||
|
||||
<template #content>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<InputLabel for="description" value="Description" />
|
||||
<Field>
|
||||
<FieldLabel for="description">Description</FieldLabel>
|
||||
<TextInput
|
||||
id="description"
|
||||
ref="descriptionInput"
|
||||
v-model="description"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
class="block w-full"
|
||||
@keydown.enter="submit" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<InputLabel for="project" value="Project" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="project">Project</FieldLabel>
|
||||
<TimeTrackerProjectTaskDropdown
|
||||
v-model:project="projectId"
|
||||
v-model:task="taskId"
|
||||
@@ -169,15 +169,14 @@ watch(removeAllTags, () => {
|
||||
:create-client
|
||||
:currency="currency"
|
||||
:can-create-project
|
||||
class="mt-1"
|
||||
empty-placeholder="Select project..."
|
||||
allow-reset
|
||||
:enable-estimated-time
|
||||
:projects="projects"
|
||||
:tasks="tasks"></TimeTrackerProjectTaskDropdown>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<InputLabel for="project" value="Tag" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Tag</FieldLabel>
|
||||
<div class="flex space-x-5">
|
||||
<TagDropdown
|
||||
v-model="selectedTags"
|
||||
@@ -194,33 +193,31 @@ watch(removeAllTags, () => {
|
||||
</Button>
|
||||
</template>
|
||||
</TagDropdown>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="no_tags" v-model:checked="removeAllTags"></Checkbox>
|
||||
<InputLabel for="no_tags" value="Remove all tags" />
|
||||
</div>
|
||||
<FieldLabel for="no_tags">Remove all tags</FieldLabel>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<InputLabel for="project" value="Billable" />
|
||||
<div class="flex">
|
||||
<Select v-model="timeEntryBillable">
|
||||
<SelectTrigger>
|
||||
<SelectValue>
|
||||
<span v-if="billable === undefined">Set billable status</span>
|
||||
<span v-else-if="billable === true">Billable</span>
|
||||
<span v-else>Non Billable</span>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="do-not-update">
|
||||
Keep current billable status
|
||||
</SelectItem>
|
||||
<SelectItem value="billable">Billable</SelectItem>
|
||||
<SelectItem value="non-billable">Non Billable</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Billable</FieldLabel>
|
||||
<Select v-model="timeEntryBillable">
|
||||
<SelectTrigger>
|
||||
<SelectValue>
|
||||
<span v-if="billable === undefined">Set billable status</span>
|
||||
<span v-else-if="billable === true">Billable</span>
|
||||
<span v-else>Non Billable</span>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="do-not-update">
|
||||
Keep current billable status
|
||||
</SelectItem>
|
||||
<SelectItem value="billable">Billable</SelectItem>
|
||||
<SelectItem value="non-billable">Non Billable</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
|
||||
21
resources/js/packages/ui/src/field/Field.vue
Normal file
21
resources/js/packages/ui/src/field/Field.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
import type { FieldVariants } from '.';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { fieldVariants } from '.';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class'];
|
||||
orientation?: FieldVariants['orientation'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
role="group"
|
||||
data-slot="field"
|
||||
:data-orientation="orientation"
|
||||
:class="cn(fieldVariants({ orientation }), props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
16
resources/js/packages/ui/src/field/FieldContent.vue
Normal file
16
resources/js/packages/ui/src/field/FieldContent.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="field-content"
|
||||
:class="cn('group/field-content flex flex-1 flex-col gap-1.5 leading-snug', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
23
resources/js/packages/ui/src/field/FieldDescription.vue
Normal file
23
resources/js/packages/ui/src/field/FieldDescription.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p
|
||||
data-slot="field-description"
|
||||
:class="
|
||||
cn(
|
||||
'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
|
||||
'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
|
||||
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
|
||||
props.class
|
||||
)
|
||||
">
|
||||
<slot />
|
||||
</p>
|
||||
</template>
|
||||
40
resources/js/packages/ui/src/field/FieldError.vue
Normal file
40
resources/js/packages/ui/src/field/FieldError.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class'];
|
||||
errors?: Array<{ message?: string } | undefined>;
|
||||
}>();
|
||||
|
||||
const content = computed(() => {
|
||||
if (!props.errors || props.errors.length === 0) return null;
|
||||
|
||||
if (props.errors.length === 1 && props.errors[0]?.message) {
|
||||
return props.errors[0].message;
|
||||
}
|
||||
|
||||
return props.errors.some((e) => e?.message) ? props.errors : null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="$slots.default || content"
|
||||
role="alert"
|
||||
data-slot="field-error"
|
||||
:class="cn('text-destructive text-sm font-normal', props.class)">
|
||||
<slot v-if="$slots.default" />
|
||||
|
||||
<template v-else-if="typeof content === 'string'">
|
||||
{{ content }}
|
||||
</template>
|
||||
|
||||
<ul v-else-if="Array.isArray(content)" class="ml-4 flex list-disc flex-col gap-1">
|
||||
<li v-for="(error, index) in content" :key="index">
|
||||
{{ error?.message }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
21
resources/js/packages/ui/src/field/FieldGroup.vue
Normal file
21
resources/js/packages/ui/src/field/FieldGroup.vue
Normal file
@@ -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
|
||||
data-slot="field-group"
|
||||
:class="
|
||||
cn(
|
||||
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
|
||||
props.class
|
||||
)
|
||||
">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
24
resources/js/packages/ui/src/field/FieldLabel.vue
Normal file
24
resources/js/packages/ui/src/field/FieldLabel.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Label } from '@/Components/ui/label';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Label
|
||||
data-slot="field-label"
|
||||
:class="
|
||||
cn(
|
||||
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
|
||||
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&_>[data-slot=field]]:p-3',
|
||||
'has-[[data-state=checked]]:bg-primary/5 has-[[data-state=checked]]:border-primary dark:has-[[data-state=checked]]:bg-primary/10',
|
||||
props.class
|
||||
)
|
||||
">
|
||||
<slot />
|
||||
</Label>
|
||||
</template>
|
||||
25
resources/js/packages/ui/src/field/FieldLegend.vue
Normal file
25
resources/js/packages/ui/src/field/FieldLegend.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class'];
|
||||
variant?: 'legend' | 'label';
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<legend
|
||||
data-slot="field-legend"
|
||||
:data-variant="variant"
|
||||
:class="
|
||||
cn(
|
||||
'mb-3 font-medium',
|
||||
'data-[variant=legend]:text-base',
|
||||
'data-[variant=label]:text-sm',
|
||||
props.class
|
||||
)
|
||||
">
|
||||
<slot />
|
||||
</legend>
|
||||
</template>
|
||||
29
resources/js/packages/ui/src/field/FieldSeparator.vue
Normal file
29
resources/js/packages/ui/src/field/FieldSeparator.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Separator } from '../separator';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="field-separator"
|
||||
:data-content="!!$slots.default"
|
||||
:class="
|
||||
cn(
|
||||
'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
|
||||
props.class
|
||||
)
|
||||
">
|
||||
<Separator class="absolute inset-0 top-1/2" />
|
||||
<span
|
||||
v-if="$slots.default"
|
||||
class="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
||||
data-slot="field-separator-content">
|
||||
<slot />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
22
resources/js/packages/ui/src/field/FieldSet.vue
Normal file
22
resources/js/packages/ui/src/field/FieldSet.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fieldset
|
||||
data-slot="field-set"
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-col gap-6',
|
||||
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
|
||||
props.class
|
||||
)
|
||||
">
|
||||
<slot />
|
||||
</fieldset>
|
||||
</template>
|
||||
21
resources/js/packages/ui/src/field/FieldTitle.vue
Normal file
21
resources/js/packages/ui/src/field/FieldTitle.vue
Normal file
@@ -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
|
||||
data-slot="field-label"
|
||||
:class="
|
||||
cn(
|
||||
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
|
||||
props.class
|
||||
)
|
||||
">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
39
resources/js/packages/ui/src/field/index.ts
Normal file
39
resources/js/packages/ui/src/field/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
export const fieldVariants = cva(
|
||||
'group/field flex w-full gap-3 data-[invalid=true]:text-destructive',
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],
|
||||
horizontal: [
|
||||
'flex-row items-center',
|
||||
'[&>[data-slot=field-label]]:flex-auto',
|
||||
'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
|
||||
],
|
||||
responsive: [
|
||||
'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto',
|
||||
'@md/field-group:[&>[data-slot=field-label]]:flex-auto',
|
||||
'@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: 'vertical',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export type FieldVariants = VariantProps<typeof fieldVariants>;
|
||||
|
||||
export { default as Field } from './Field.vue';
|
||||
export { default as FieldContent } from './FieldContent.vue';
|
||||
export { default as FieldDescription } from './FieldDescription.vue';
|
||||
export { default as FieldError } from './FieldError.vue';
|
||||
export { default as FieldGroup } from './FieldGroup.vue';
|
||||
export { default as FieldLabel } from './FieldLabel.vue';
|
||||
export { default as FieldLegend } from './FieldLegend.vue';
|
||||
export { default as FieldSeparator } from './FieldSeparator.vue';
|
||||
export { default as FieldSet } from './FieldSet.vue';
|
||||
export { default as FieldTitle } from './FieldTitle.vue';
|
||||
@@ -43,6 +43,21 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './
|
||||
import { Popover, PopoverContent, PopoverTrigger, PopoverAnchor } from './popover/index';
|
||||
import { RangeCalendar } from './range-calendar/index';
|
||||
import { CommandPalette } from './CommandPalette/index';
|
||||
import { Separator } from './separator/index';
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
fieldVariants,
|
||||
} from './field/index';
|
||||
export type { FieldVariants } from './field/index';
|
||||
export type { ActivityPeriod } from './FullCalendar/idleStatusPlugin';
|
||||
export type {
|
||||
CommandPaletteCommand,
|
||||
@@ -93,4 +108,16 @@ export {
|
||||
PopoverAnchor,
|
||||
RangeCalendar,
|
||||
CommandPalette,
|
||||
Separator,
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
fieldVariants,
|
||||
};
|
||||
|
||||
26
resources/js/packages/ui/src/separator/Separator.vue
Normal file
26
resources/js/packages/ui/src/separator/Separator.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import type { SeparatorProps } from 'reka-ui';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { Separator } from 'reka-ui';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const props = withDefaults(defineProps<SeparatorProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
orientation: 'horizontal',
|
||||
decorative: true,
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Separator
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'shrink-0 bg-border',
|
||||
props.orientation === 'horizontal' ? 'h-px w-full' : 'w-px h-full',
|
||||
props.class
|
||||
)
|
||||
" />
|
||||
</template>
|
||||
1
resources/js/packages/ui/src/separator/index.ts
Normal file
1
resources/js/packages/ui/src/separator/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Separator } from './Separator.vue';
|
||||
Reference in New Issue
Block a user