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:
Jennie Robinson Faber 2025-10-13 15:05:29 +01:00
parent 9b45652b83
commit 3fea484585
13 changed files with 788 additions and 785 deletions

View file

@ -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>