Switch UI components to new design system tokens
Standardizes color values and styling using the new tokens: - Replaces hardcoded colors with semantic variables - Updates background/text/border classes for light/dark mode - Migrates inputs to UInput/USelect/UTextarea components - Removes redundant style declarations
This commit is contained in:
parent
9b45652b83
commit
3fea484585
13 changed files with 788 additions and 785 deletions
|
|
@ -1,62 +1,63 @@
|
|||
<template>
|
||||
<div class="space-y-2">
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="naturalInput"
|
||||
type="text"
|
||||
<UInput
|
||||
v-model="naturalInput"
|
||||
:placeholder="placeholder"
|
||||
:class="[
|
||||
'w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent',
|
||||
inputClass,
|
||||
{
|
||||
'border-green-300 bg-green-50': isValidParse && naturalInput.trim(),
|
||||
'border-red-300 bg-red-50': hasError && naturalInput.trim()
|
||||
}
|
||||
]"
|
||||
:color="
|
||||
hasError && naturalInput.trim()
|
||||
? 'error'
|
||||
: isValidParse && naturalInput.trim()
|
||||
? 'success'
|
||||
: undefined
|
||||
"
|
||||
@input="parseNaturalInput"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
<div v-if="naturalInput.trim()" class="absolute right-3 top-2.5">
|
||||
<Icon
|
||||
v-if="isValidParse"
|
||||
name="heroicons:check-circle"
|
||||
class="w-5 h-5 text-green-500"
|
||||
/>
|
||||
<Icon
|
||||
v-else-if="hasError"
|
||||
name="heroicons:exclamation-circle"
|
||||
class="w-5 h-5 text-red-500"
|
||||
/>
|
||||
</div>
|
||||
>
|
||||
<template #trailing>
|
||||
<Icon
|
||||
v-if="isValidParse && naturalInput.trim()"
|
||||
name="heroicons:check-circle"
|
||||
class="w-5 h-5 text-green-500"
|
||||
/>
|
||||
<Icon
|
||||
v-else-if="hasError && naturalInput.trim()"
|
||||
name="heroicons:exclamation-circle"
|
||||
class="w-5 h-5 text-red-500"
|
||||
/>
|
||||
</template>
|
||||
</UInput>
|
||||
</div>
|
||||
|
||||
<div v-if="parsedDate && isValidParse" class="text-sm text-green-700 bg-green-50 px-3 py-2 rounded-lg border border-green-200">
|
||||
|
||||
<div
|
||||
v-if="parsedDate && isValidParse"
|
||||
class="text-sm text-green-700 bg-green-50 px-3 py-2 rounded-lg border border-green-200"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="heroicons:calendar" class="w-4 h-4" />
|
||||
<span>{{ formatParsedDate(parsedDate) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasError && naturalInput.trim()" class="text-sm text-red-700 bg-red-50 px-3 py-2 rounded-lg border border-red-200">
|
||||
|
||||
<div
|
||||
v-if="hasError && naturalInput.trim()"
|
||||
class="text-sm text-red-700 bg-red-50 px-3 py-2 rounded-lg border border-red-200"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="heroicons:exclamation-triangle" class="w-4 h-4" />
|
||||
<span>{{ errorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Fallback datetime-local input -->
|
||||
<details class="text-sm">
|
||||
<summary class="cursor-pointer text-gray-600 hover:text-gray-900">
|
||||
<summary class="cursor-pointer text-muted hover:text-default">
|
||||
Use traditional date picker
|
||||
</summary>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
v-model="datetimeValue"
|
||||
<UInput
|
||||
v-model="datetimeValue"
|
||||
type="datetime-local"
|
||||
:class="[
|
||||
'w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent',
|
||||
inputClass
|
||||
]"
|
||||
@change="onDatetimeChange"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -65,174 +66,179 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import * as chrono from 'chrono-node'
|
||||
import * as chrono from "chrono-node";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: "",
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'e.g., "tomorrow at 3pm", "next Friday at 9am", "in 2 hours"'
|
||||
default: 'e.g., "tomorrow at 3pm", "next Friday at 9am", "in 2 hours"',
|
||||
},
|
||||
inputClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: "",
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const naturalInput = ref('')
|
||||
const parsedDate = ref(null)
|
||||
const isValidParse = ref(false)
|
||||
const hasError = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const datetimeValue = ref('')
|
||||
const naturalInput = ref("");
|
||||
const parsedDate = ref(null);
|
||||
const isValidParse = ref(false);
|
||||
const hasError = ref(false);
|
||||
const errorMessage = ref("");
|
||||
const datetimeValue = ref("");
|
||||
|
||||
// Initialize with current value
|
||||
onMounted(() => {
|
||||
if (props.modelValue) {
|
||||
const date = new Date(props.modelValue)
|
||||
const date = new Date(props.modelValue);
|
||||
if (!isNaN(date.getTime())) {
|
||||
parsedDate.value = date
|
||||
datetimeValue.value = formatForDatetimeLocal(date)
|
||||
isValidParse.value = true
|
||||
parsedDate.value = date;
|
||||
datetimeValue.value = formatForDatetimeLocal(date);
|
||||
isValidParse.value = true;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Watch for external changes to modelValue
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (newValue && newValue !== formatForDatetimeLocal(parsedDate.value)) {
|
||||
const date = new Date(newValue)
|
||||
if (!isNaN(date.getTime())) {
|
||||
parsedDate.value = date
|
||||
datetimeValue.value = formatForDatetimeLocal(date)
|
||||
isValidParse.value = true
|
||||
naturalInput.value = '' // Clear natural input when set externally
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (newValue && newValue !== formatForDatetimeLocal(parsedDate.value)) {
|
||||
const date = new Date(newValue);
|
||||
if (!isNaN(date.getTime())) {
|
||||
parsedDate.value = date;
|
||||
datetimeValue.value = formatForDatetimeLocal(date);
|
||||
isValidParse.value = true;
|
||||
naturalInput.value = ""; // Clear natural input when set externally
|
||||
}
|
||||
} else if (!newValue) {
|
||||
reset();
|
||||
}
|
||||
} else if (!newValue) {
|
||||
reset()
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
const parseNaturalInput = () => {
|
||||
const input = naturalInput.value.trim()
|
||||
|
||||
const input = naturalInput.value.trim();
|
||||
|
||||
if (!input) {
|
||||
reset()
|
||||
return
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Parse with chrono-node
|
||||
const results = chrono.parse(input)
|
||||
|
||||
const results = chrono.parse(input);
|
||||
|
||||
if (results.length > 0) {
|
||||
const result = results[0]
|
||||
const date = result.date()
|
||||
|
||||
const result = results[0];
|
||||
const date = result.date();
|
||||
|
||||
// Validate the parsed date
|
||||
if (date && !isNaN(date.getTime())) {
|
||||
parsedDate.value = date
|
||||
isValidParse.value = true
|
||||
hasError.value = false
|
||||
datetimeValue.value = formatForDatetimeLocal(date)
|
||||
emit('update:modelValue', formatForDatetimeLocal(date))
|
||||
parsedDate.value = date;
|
||||
isValidParse.value = true;
|
||||
hasError.value = false;
|
||||
datetimeValue.value = formatForDatetimeLocal(date);
|
||||
emit("update:modelValue", formatForDatetimeLocal(date));
|
||||
} else {
|
||||
setError('Could not parse this date format')
|
||||
setError("Could not parse this date format");
|
||||
}
|
||||
} else {
|
||||
setError('Could not understand this date format. Try something like "tomorrow at 3pm" or "next Friday"')
|
||||
setError(
|
||||
'Could not understand this date format. Try something like "tomorrow at 3pm" or "next Friday"',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
setError('Error parsing date')
|
||||
setError("Error parsing date");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
// If we have a valid parse but the input changed, try to parse again
|
||||
if (naturalInput.value.trim() && !isValidParse.value) {
|
||||
parseNaturalInput()
|
||||
parseNaturalInput();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onDatetimeChange = () => {
|
||||
if (datetimeValue.value) {
|
||||
const date = new Date(datetimeValue.value)
|
||||
const date = new Date(datetimeValue.value);
|
||||
if (!isNaN(date.getTime())) {
|
||||
parsedDate.value = date
|
||||
isValidParse.value = true
|
||||
hasError.value = false
|
||||
naturalInput.value = '' // Clear natural input when using traditional picker
|
||||
emit('update:modelValue', datetimeValue.value)
|
||||
parsedDate.value = date;
|
||||
isValidParse.value = true;
|
||||
hasError.value = false;
|
||||
naturalInput.value = ""; // Clear natural input when using traditional picker
|
||||
emit("update:modelValue", datetimeValue.value);
|
||||
}
|
||||
} else {
|
||||
reset()
|
||||
reset();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
parsedDate.value = null
|
||||
isValidParse.value = false
|
||||
hasError.value = false
|
||||
errorMessage.value = ''
|
||||
emit('update:modelValue', '')
|
||||
}
|
||||
parsedDate.value = null;
|
||||
isValidParse.value = false;
|
||||
hasError.value = false;
|
||||
errorMessage.value = "";
|
||||
emit("update:modelValue", "");
|
||||
};
|
||||
|
||||
const setError = (message) => {
|
||||
isValidParse.value = false
|
||||
hasError.value = true
|
||||
errorMessage.value = message
|
||||
parsedDate.value = null
|
||||
}
|
||||
isValidParse.value = false;
|
||||
hasError.value = true;
|
||||
errorMessage.value = message;
|
||||
parsedDate.value = null;
|
||||
};
|
||||
|
||||
const formatForDatetimeLocal = (date) => {
|
||||
if (!date) return ''
|
||||
if (!date) return "";
|
||||
// Format as YYYY-MM-DDTHH:MM for datetime-local input
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`
|
||||
}
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
const formatParsedDate = (date) => {
|
||||
if (!date) return ''
|
||||
|
||||
const now = new Date()
|
||||
const isToday = date.toDateString() === now.toDateString()
|
||||
const tomorrow = new Date(now)
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
const isTomorrow = date.toDateString() === tomorrow.toDateString()
|
||||
|
||||
const timeStr = date.toLocaleString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
})
|
||||
|
||||
if (!date) return "";
|
||||
|
||||
const now = new Date();
|
||||
const isToday = date.toDateString() === now.toDateString();
|
||||
const tomorrow = new Date(now);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const isTomorrow = date.toDateString() === tomorrow.toDateString();
|
||||
|
||||
const timeStr = date.toLocaleString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
if (isToday) {
|
||||
return `Today at ${timeStr}`
|
||||
return `Today at ${timeStr}`;
|
||||
} else if (isTomorrow) {
|
||||
return `Tomorrow at ${timeStr}`
|
||||
return `Tomorrow at ${timeStr}`;
|
||||
} else {
|
||||
return date.toLocaleString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
})
|
||||
return date.toLocaleString("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue