Shared UI Components
The foundational UI wrappers for the Busflow Workspace. These components act as a bridge between the Quasar layout engine and our Tailwind design system, enforcing the UX guidelines defined in
docs/ux-design-decisions.md.
Exported Components
You can import all components directly from the public API:
import { DataTable, DrawerForm, DetailPopup, TextField } from 'src/shared/ui';1. DataTable
A heavily styled wrapper around Quasar's QTable. It enforces our Tailwind aesthetics, handles pagination intelligently (no default crash-inducing sort columns), and implements a standardized empty state.
Usage
<template>
<DataTable
title="Fleet Management"
:columns="columns"
:rows="vehicles"
:loading="isLoading"
empty-headline="Let's build your fleet"
empty-subtext="Add your vehicles to start dispatching."
>
<!-- Add buttons to the top right -->
<template #top-actions>
<q-btn color="primary" label="+ Add Vehicle" @click="openDrawer" />
</template>
<!-- Custom column rendering (uses Quasar's body-cell-[name] syntax) -->
<template #body-cell-status="props">
<q-td :props="props">
<q-chip :color="props.row.status === 'ACTIVE' ? 'positive' : 'grey'">
{{ props.row.status }}
</q-chip>
</q-td>
</template>
</DataTable>
</template>2. DrawerForm
A standardized right-aligned slide-over QDrawer. It solves the "scrolling form" problem by hard-pinning the header and footer to the top and bottom of the viewport, wrapping the entire content in a native <form> element.
Usage
<template>
<DrawerForm
v-model="isOpen"
title="Add Vehicle"
submitLabel="Save Vehicle"
:loading="isSubmitting"
@submit="handleSave"
>
<!-- The body of the form goes here. It scrolls automatically. -->
<TextField name="license_plate" label="License Plate" />
<TextField name="capacity" label="Capacity" type="number" />
</DrawerForm>
</template>3. DetailPopup
A centered modal QDialog using a Tailwind card layout. Used for medium-complexity views (like clicking on a crew member to see their qualifications).
Usage
<template>
<DetailPopup v-model="isOpen" title="Vehicle Details">
<template #header-badge>
<q-chip color="positive" size="sm">ACTIVE</q-chip>
</template>
<!-- Body Content -->
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-sm text-muted-foreground">License Plate</p>
<p class="font-medium">{{ vehicle.license_plate }}</p>
</div>
</div>
</div>
</DetailPopup>
</template>4. Validation Fields (VeeValidate + Valibot)
TextField and SelectField are thin wrappers around Quasar's q-input and q-select. They automatically bind to VeeValidate form state based on the name prop, pulling in validation errors and displaying them natively inline.
Note: To use these, the parent component must be wrapped in VeeValidate's
<Form>or useuseForm()with a Valibot schema.
Usage
<script setup>
import { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/valibot';
import * as v from 'valibot';
import { TextField, SelectField, DrawerForm } from 'src/shared/ui';
// 1. Define Valibot Schema
const schema = v.object({
model: v.pipe(v.string(), v.minLength(1, "Model is required")),
class: v.pipe(v.string(), v.minLength(1, "Class is required"))
});
// 2. Setup VeeValidate
const { handleSubmit } = useForm({
validationSchema: toTypedSchema(schema)
});
const onSubmit = handleSubmit((values) => {
console.log(values);
});
</script>
<template>
<DrawerForm v-model="isOpen" title="Add Vehicle" @submit="onSubmit">
<!-- Notice we don't need v-model, we just bind the schema name! -->
<TextField name="model" label="Vehicle Model" placeholder="e.g. Mercedes" />
<SelectField
name="class"
label="Vehicle Class"
:options="['COACH', 'MINIBUS']"
/>
</DrawerForm>
</template>