Improve UI
This commit is contained in:
parent
1197a8cb06
commit
23840319c2
11 changed files with 338 additions and 396 deletions
|
|
@ -1,16 +1,8 @@
|
|||
import { Button } from "@ui/components/ui/button";
|
||||
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@ui/components/ui/card";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/components/ui/select";
|
||||
import { Switch } from "@ui/components/ui/switch";
|
||||
import { useTimePicker } from "@ui/ui-library/time-picker";
|
||||
import { TimeInput } from "@ui/components/ui/time-input";
|
||||
import { Copy as CopyIcon, Minus as MinusIcon, Plus as PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface TimeRange {
|
||||
start: string;
|
||||
|
|
@ -59,8 +51,6 @@ export function AvailabilityCard({
|
|||
}: AvailabilityCardProps) {
|
||||
const dayDisplay = DAYS_OF_WEEK_DISPLAY[day];
|
||||
|
||||
const [selectedRangeIndex, setSelectedRangeIndex] = useState(0);
|
||||
|
||||
const handleAddRange = () => {
|
||||
// Find a free slot for the new range
|
||||
const sortedRanges = [...timeRanges].sort(
|
||||
|
|
@ -100,17 +90,13 @@ export function AvailabilityCard({
|
|||
|
||||
const newRanges = [...timeRanges, { start: newStart, end: newEnd }];
|
||||
onTimeRangesChange(newRanges);
|
||||
setSelectedRangeIndex(newRanges.length - 1);
|
||||
};
|
||||
|
||||
const handleDeleteRange = (index: number) => {
|
||||
const newRanges = timeRanges.filter((_, i) => i !== index);
|
||||
onTimeRangesChange(newRanges);
|
||||
setSelectedRangeIndex(Math.min(selectedRangeIndex, newRanges.length - 1));
|
||||
};
|
||||
|
||||
const timeOptions = useTimePicker({ intervalInMinute: 30 });
|
||||
|
||||
const validateTimeRange = (ranges: TimeRange[], index: number): boolean => {
|
||||
const currentRange = ranges[index];
|
||||
const currentStart = timeToMinutes(currentRange.start);
|
||||
|
|
@ -171,93 +157,54 @@ export function AvailabilityCard({
|
|||
</div>
|
||||
|
||||
{/* Time Ranges */}
|
||||
<div className="flex gap-1 flex-wrap items-center">
|
||||
<div className="space-y-2">
|
||||
{timeRanges.map((range, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => setSelectedRangeIndex(index)}
|
||||
className={`flex items-center gap-1 rounded-md px-1.5 py-1 cursor-pointer transition-all duration-200 ${
|
||||
selectedRangeIndex === index ? "bg-primary/10" : "bg-muted/80 hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center text-xs w-fit">
|
||||
{selectedRangeIndex === index ? (
|
||||
<>
|
||||
<Select
|
||||
value={range.start}
|
||||
disabled={!enabled}
|
||||
onValueChange={(value) => {
|
||||
const newRanges = [...timeRanges];
|
||||
newRanges[index] = {
|
||||
...range,
|
||||
start: value,
|
||||
};
|
||||
if (validateTimeRange(newRanges, index)) {
|
||||
onTimeRangesChange(newRanges);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label="Heure de début"
|
||||
className="min-h-0 h-6 px-1 py-0 text-xs bg-transparent hover:bg-accent focus:bg-accent shadow-none outline-none ring-0 focus:ring-0 focus:outline-none min-w-[3rem] w-auto rounded-sm border-0"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-[300px]">
|
||||
{timeOptions.map((item) => (
|
||||
<SelectItem key={item.id} value={item.value} className="text-xs py-0.5">
|
||||
{item.value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-muted-foreground text-[10px] mx-2">-</span>
|
||||
<Select
|
||||
value={range.end}
|
||||
disabled={!enabled}
|
||||
onValueChange={(value) => {
|
||||
const newRanges = [...timeRanges];
|
||||
newRanges[index] = {
|
||||
...range,
|
||||
end: value,
|
||||
};
|
||||
if (validateTimeRange(newRanges, index)) {
|
||||
onTimeRangesChange(newRanges);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label="Heure de fin"
|
||||
className="min-h-0 h-6 px-1 py-0 text-xs bg-transparent hover:bg-accent focus:bg-accent shadow-none outline-none ring-0 focus:ring-0 focus:outline-none min-w-[3rem] w-auto rounded-sm border-0"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-[300px]">
|
||||
{timeOptions.map((item) => (
|
||||
<SelectItem key={item.id} value={item.value} className="text-xs py-0.5">
|
||||
{item.value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="font-medium text-xs px-1">{range.start}</span>
|
||||
<span className="text-muted-foreground text-[10px]">→</span>
|
||||
<span className="font-medium text-xs px-1">{range.end}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<TimeInput
|
||||
value={range.start}
|
||||
onChange={(value) => {
|
||||
const newRanges = [...timeRanges];
|
||||
newRanges[index] = {
|
||||
...range,
|
||||
start: value,
|
||||
};
|
||||
if (validateTimeRange(newRanges, index)) {
|
||||
onTimeRangesChange(newRanges);
|
||||
}
|
||||
}}
|
||||
isDisabled={!enabled}
|
||||
className="h-8 text-sm max-w-32"
|
||||
id={`start-${day}-${index}`}
|
||||
/>
|
||||
<span className="text-muted-foreground text-sm">-</span>
|
||||
<TimeInput
|
||||
value={range.end}
|
||||
onChange={(value) => {
|
||||
const newRanges = [...timeRanges];
|
||||
newRanges[index] = {
|
||||
...range,
|
||||
end: value,
|
||||
};
|
||||
if (validateTimeRange(newRanges, index)) {
|
||||
onTimeRangesChange(newRanges);
|
||||
}
|
||||
}}
|
||||
isDisabled={!enabled}
|
||||
className="h-8 text-sm max-w-32"
|
||||
id={`end-${day}-${index}`}
|
||||
/>
|
||||
{timeRanges.length > 1 && (
|
||||
<Button
|
||||
onClick={() => handleDeleteRange(index)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteRange(index);
|
||||
}}
|
||||
disabled={!enabled}
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="h-4 w-4 p-0 border-0"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="hover:text-red-500 hover:bg-red-50/10 dark:hover:bg-red-950/10"
|
||||
>
|
||||
<MinusIcon className="size-2" />
|
||||
<MinusIcon />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -268,9 +215,10 @@ export function AvailabilityCard({
|
|||
disabled={!enabled}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-5 px-1.5 flex items-center text-xs border-0 bg-muted/50 hover:bg-muted"
|
||||
className="h-8 px-3 flex items-center text-sm"
|
||||
>
|
||||
<PlusIcon className="size-2.5" />
|
||||
<PlusIcon className="size-4 mr-1" />
|
||||
Ajouter une plage horaire
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
import { getLocalTimeZone, parseDate, today } from "@internationalized/date";
|
||||
import { Button } from "@ui/components/ui/button";
|
||||
import { DatePicker } from "@ui/components/ui/date-picker";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@ui/components/ui/dialog";
|
||||
import { Input } from "@ui/components/ui/input";
|
||||
import { Label } from "@ui/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
@ -7,13 +18,13 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/components/ui/select";
|
||||
import { Textarea } from "@ui/components/ui/textarea";
|
||||
import { TimeInput } from "@ui/components/ui/time-input";
|
||||
import { useCreateEvents, useEvent, useUpdateEvent } from "@ui/hooks/events";
|
||||
import { useTablosList } from "@ui/hooks/tablos";
|
||||
import { useUser } from "@ui/providers/UserStoreProvider";
|
||||
import { Event, EventInsert } from "@ui/types/events.types";
|
||||
import { useTimePicker } from "@ui/ui-library/time-picker";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Group } from "react-aria-components";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
|
||||
export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
|
||||
|
|
@ -29,7 +40,6 @@ export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
|
|||
const { data: tablos, isLoading: tablosLoading } = useTablosList();
|
||||
const createEvents = useCreateEvents();
|
||||
const updateEvent = useUpdateEvent();
|
||||
const timeOptions = useTimePicker({ intervalInMinute: 15 });
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onClose = () => {
|
||||
|
|
@ -44,31 +54,20 @@ export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
|
|||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
// Find the nearest time option to the selected date
|
||||
const getNearestTimeOption = (date: Date, type: "start" | "end") => {
|
||||
const dateMinutes = date.getHours() * 60 + date.getMinutes();
|
||||
|
||||
let nearestOption = timeOptions[0];
|
||||
let smallestDiff = Infinity;
|
||||
|
||||
for (const option of timeOptions) {
|
||||
const optionMinutes = option.hour * 60 + option.minute;
|
||||
const diff =
|
||||
type === "start" ? Math.abs(dateMinutes - optionMinutes) : dateMinutes + 30 - optionMinutes;
|
||||
|
||||
if (0 <= diff && diff < smallestDiff) {
|
||||
smallestDiff = diff;
|
||||
nearestOption = option;
|
||||
}
|
||||
}
|
||||
|
||||
return nearestOption?.id || "";
|
||||
// Format time from Date to HH:MM string
|
||||
const formatTimeFromDate = (date: Date, addMinutes: number = 0): string => {
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes() + addMinutes;
|
||||
const totalMinutes = hours * 60 + minutes;
|
||||
const finalHours = Math.floor(totalMinutes / 60) % 24;
|
||||
const finalMinutes = totalMinutes % 60;
|
||||
return `${finalHours.toString().padStart(2, "0")}:${finalMinutes.toString().padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
const [formEvent, setFormEvent] = useState<EventInsert>({
|
||||
start_date: date ? getLocalDateString(date) : "",
|
||||
start_time: date ? getNearestTimeOption(date, "start") : "",
|
||||
end_time: date ? getNearestTimeOption(date, "end") : "",
|
||||
start_time: date ? formatTimeFromDate(date) : "",
|
||||
end_time: date ? formatTimeFromDate(date, 30) : "",
|
||||
tablo_id: tablo_id || "",
|
||||
title: "",
|
||||
created_by: user.id,
|
||||
|
|
@ -90,30 +89,11 @@ export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
|
|||
}, [mode, event]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-2xl w-full max-w-lg mx-4 overflow-hidden">
|
||||
{/* Header with colored accent */}
|
||||
<div className="bg-gradient-to-r from-blue-500 to-blue-600 p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-medium">
|
||||
{mode === "edit" ? "Modifier l'événement" : "Nouvel événement"}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-white hover:text-gray-200 transition-colors"
|
||||
aria-label="Fermer le modal"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 text-blue-100 text-sm">
|
||||
<Dialog open={true} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{mode === "edit" ? "Modifier l'événement" : "Nouvel événement"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{mode === "edit" && event
|
||||
? new Date(event.start_date).toLocaleDateString("fr-FR", {
|
||||
weekday: "long",
|
||||
|
|
@ -127,14 +107,15 @@ export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
|
|||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Form Content */}
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="space-y-4">
|
||||
{/* Title Input */}
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
<Label htmlFor="event-title">Titre *</Label>
|
||||
<Input
|
||||
id="event-title"
|
||||
type="text"
|
||||
value={formEvent?.title}
|
||||
onChange={(e) =>
|
||||
|
|
@ -143,19 +124,14 @@ export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
|
|||
title: e.target.value,
|
||||
} as Event)
|
||||
}
|
||||
className="w-full text-lg font-medium border-none outline-none bg-transparent text-gray-900 dark:text-white placeholder-gray-400 focus:ring-0 px-0"
|
||||
placeholder="Ajouter un titre"
|
||||
aria-label="Titre de l'événement"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="border-b border-gray-200 dark:border-gray-700"></div>
|
||||
</div>
|
||||
|
||||
{/* Tablo Selection */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Tablo *
|
||||
</label>
|
||||
<Label htmlFor="event-tablo">Tablo *</Label>
|
||||
<Select
|
||||
value={formEvent?.tablo_id}
|
||||
onValueChange={(value) =>
|
||||
|
|
@ -166,7 +142,7 @@ export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
|
|||
}
|
||||
disabled={tablosLoading}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectTrigger id="event-tablo" className="w-full">
|
||||
<SelectValue placeholder="Sélectionner un tablo" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -179,9 +155,9 @@ export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
|
|||
</Select>
|
||||
</div>
|
||||
|
||||
<Group className="flex flex-row gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-600 dark:text-gray-400">Date</label>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="event-date">Date *</Label>
|
||||
<DatePicker
|
||||
aria-label="Date de l'événement"
|
||||
value={formEvent?.start_date ? parseDate(formEvent?.start_date) : undefined}
|
||||
|
|
@ -198,65 +174,46 @@ export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
|
|||
});
|
||||
}
|
||||
}}
|
||||
buttonClassName="h-[36px]"
|
||||
buttonClassName="h-10 w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-600 dark:text-gray-400">Début</label>
|
||||
<Select
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="event-start-time">Début *</Label>
|
||||
<TimeInput
|
||||
value={formEvent?.start_time || undefined}
|
||||
onValueChange={(value) => {
|
||||
onChange={(value) => {
|
||||
setFormEvent({
|
||||
...formEvent,
|
||||
start_time: value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="min-w-[110px]" aria-label="Heure de début">
|
||||
<SelectValue placeholder="--:--" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-[300px]">
|
||||
{timeOptions.map((item) => (
|
||||
<SelectItem key={item.id} value={item.id}>
|
||||
{item.value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
className="w-full"
|
||||
id="event-start-time"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-600 dark:text-gray-400">Fin</label>
|
||||
<Select
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="event-end-time">Fin</Label>
|
||||
<TimeInput
|
||||
value={formEvent?.end_time || undefined}
|
||||
onValueChange={(value) => {
|
||||
onChange={(value) => {
|
||||
setFormEvent({
|
||||
...formEvent,
|
||||
end_time: value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="min-w-[110px]" aria-label="Heure de fin">
|
||||
<SelectValue placeholder="--:--" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-[300px]">
|
||||
{timeOptions.map((item) => (
|
||||
<SelectItem key={item.id} value={item.id}>
|
||||
{item.value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
className="w-full"
|
||||
id="event-end-time"
|
||||
/>
|
||||
</div>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
<Label htmlFor="event-description">Description</Label>
|
||||
<Textarea
|
||||
id="event-description"
|
||||
value={formEvent?.description ?? ""}
|
||||
onChange={(e) =>
|
||||
setFormEvent({
|
||||
|
|
@ -265,28 +222,16 @@ export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
|
|||
} as Event)
|
||||
}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2.5 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800 dark:text-white resize-none transition-all"
|
||||
placeholder="Ajouter une description (optionnel)"
|
||||
aria-label="Description de l'événement"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="bg-gray-50 dark:bg-gray-800 px-6 py-4 flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
onClick={onClose}
|
||||
aria-label={
|
||||
mode === "edit" ? "Annuler la modification" : "Annuler la création d'événement"
|
||||
}
|
||||
>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="px-6 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-sm hover:shadow-md"
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const eventName = formEvent?.title.trim() || "(Sans titre)";
|
||||
if (mode === "edit" && event) {
|
||||
|
|
@ -299,12 +244,11 @@ export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
|
|||
}
|
||||
}}
|
||||
disabled={!formEvent?.tablo_id}
|
||||
aria-label={mode === "edit" ? "Modifier l'événement" : "Enregistrer l'événement"}
|
||||
>
|
||||
{mode === "edit" ? "Modifier" : "Enregistrer"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
CardTitle,
|
||||
} from "@ui/components/ui/card";
|
||||
import { CopyButton } from "@ui/components/ui/clipboard";
|
||||
import { Text } from "@ui/components/ui/typography";
|
||||
import { EventType, EventTypeConfig, useEventTypes } from "@ui/hooks/event-types";
|
||||
import { CheckIcon, EditIcon, ExternalLinkIcon, TrashIcon, XIcon } from "lucide-react";
|
||||
import { useUser } from "src/providers/UserStoreProvider";
|
||||
|
|
@ -38,7 +37,7 @@ export function EventTypeCard({
|
|||
};
|
||||
return (
|
||||
<Card key={eventType.id} className={eventType.isActive ? "opacity-100" : "opacity-60"}>
|
||||
<CardHeader>
|
||||
<CardHeader className="min-h-[80px]">
|
||||
<CardTitle className="text-lg">{eventType.name}</CardTitle>
|
||||
<CardAction>
|
||||
<div className="flex gap-2">
|
||||
|
|
@ -66,6 +65,7 @@ export function EventTypeCard({
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => deleteEventType({ id: eventType.id })}
|
||||
className="hover:text-red-500 hover:bg-red-50/10 dark:hover:bg-red-950/10"
|
||||
>
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
|
|
@ -74,7 +74,7 @@ export function EventTypeCard({
|
|||
</CardHeader>
|
||||
|
||||
<CardContent className="min-h-[200px]">
|
||||
<Text className="text-muted-foreground">{eventType.description}</Text>
|
||||
{/* <Text className="text-muted-foreground">{eventType.description}</Text> */}
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
|
|
|
|||
|
|
@ -71,9 +71,7 @@ export function EventTypeModal({
|
|||
|
||||
{/* Timing Configuration Section */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||
Configuration des horaires
|
||||
</h4>
|
||||
<Label>Configuration des horaires</Label>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Button } from "@ui/components/ui/button";
|
||||
import { ButtonGroup } from "@ui/components/ui/button-group";
|
||||
import { Text } from "@ui/components/ui/typography";
|
||||
import { useTheme } from "@ui/contexts/ThemeContext";
|
||||
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
|
||||
|
||||
|
|
@ -10,38 +9,70 @@ const translation = {
|
|||
system: "Système",
|
||||
};
|
||||
|
||||
export function ThemeSwitcher() {
|
||||
export function ThemeSwitcher({ isCollapsed = false }: { isCollapsed?: boolean }) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const getThemeIcon = () => {
|
||||
switch (theme) {
|
||||
case "light":
|
||||
return <SunIcon className="w-5 h-5" />;
|
||||
case "dark":
|
||||
return <MoonIcon className="w-5 h-5" />;
|
||||
case "system":
|
||||
return <MonitorIcon className="w-5 h-5" />;
|
||||
}
|
||||
};
|
||||
|
||||
const cycleTheme = () => {
|
||||
const themes: Array<"light" | "dark" | "system"> = ["light", "system", "dark"];
|
||||
const currentIndex = themes.indexOf(theme);
|
||||
const nextIndex = (currentIndex + 1) % themes.length;
|
||||
setTheme(themes[nextIndex]);
|
||||
};
|
||||
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={cycleTheme}
|
||||
aria-label={`Thème actuel: ${translation[theme]}`}
|
||||
className="hover:bg-navbar-darker w-full h-auto px-2 py-1.5 text-gray-300/90"
|
||||
>
|
||||
{getThemeIcon()}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Text className="text-gray-300/90">Thème: {translation[theme]}</Text>
|
||||
<ButtonGroup orientation="horizontal">
|
||||
<Button
|
||||
variant={theme === "light" ? "default" : "outline"}
|
||||
size="icon"
|
||||
onClick={() => setTheme("light")}
|
||||
aria-label="Mode clair"
|
||||
>
|
||||
<SunIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === "system" ? "default" : "outline"}
|
||||
size="icon"
|
||||
onClick={() => setTheme("system")}
|
||||
aria-label="Mode système"
|
||||
>
|
||||
<MonitorIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === "dark" ? "default" : "outline"}
|
||||
size="icon"
|
||||
onClick={() => setTheme("dark")}
|
||||
aria-label="Mode sombre"
|
||||
>
|
||||
<MoonIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<ButtonGroup orientation="horizontal" className="w-fit">
|
||||
<Button
|
||||
variant={theme === "light" ? "default" : "outline"}
|
||||
size="icon-sm"
|
||||
onClick={() => setTheme("light")}
|
||||
aria-label="Mode clair"
|
||||
className="flex-1"
|
||||
>
|
||||
<SunIcon className="w-4 h-4 color-foreground" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === "system" ? "default" : "outline"}
|
||||
size="icon-sm"
|
||||
onClick={() => setTheme("system")}
|
||||
aria-label="Mode système"
|
||||
className="flex-1"
|
||||
>
|
||||
<MonitorIcon className="w-4 h-4 color-foreground" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === "dark" ? "default" : "outline"}
|
||||
size="icon-sm"
|
||||
onClick={() => setTheme("dark")}
|
||||
aria-label="Mode sombre"
|
||||
className="flex-1"
|
||||
>
|
||||
<MoonIcon className="w-4 h-4 color-foreground" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ const buttonVariants = cva(
|
|||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
outline:
|
||||
"border bg-background text-foreground shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ const DialogTitle = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
className={cn("text-lg font-semibold leading-none tracking-tight text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ interface TimeInputProps {
|
|||
defaultValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
className?: string;
|
||||
isDisabled?: boolean;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ export const TimeInputWithLabel = ({
|
|||
defaultValue,
|
||||
onChange,
|
||||
className,
|
||||
isDisabled,
|
||||
id = "time-picker",
|
||||
}: TimeInputWithLabelProps) => {
|
||||
return (
|
||||
|
|
@ -32,6 +34,7 @@ export const TimeInputWithLabel = ({
|
|||
defaultValue={defaultValue || "08:30:00"}
|
||||
onChange={onChange}
|
||||
className={className}
|
||||
isDisabled={isDisabled}
|
||||
id={id}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -43,6 +46,7 @@ export const TimeInput = ({
|
|||
defaultValue,
|
||||
onChange,
|
||||
className,
|
||||
isDisabled,
|
||||
id = "time-picker",
|
||||
}: TimeInputProps) => {
|
||||
return (
|
||||
|
|
@ -52,6 +56,7 @@ export const TimeInput = ({
|
|||
step="60"
|
||||
value={value}
|
||||
defaultValue={defaultValue}
|
||||
disabled={isDisabled}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
className={cn(
|
||||
"bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none",
|
||||
|
|
|
|||
|
|
@ -1,22 +1,34 @@
|
|||
import { createContext, ReactNode, useContext, useEffect, useState } from "react";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
|
||||
interface ThemeContextType {
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
}
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
setTheme: () => null,
|
||||
};
|
||||
|
||||
const THEME_STORAGE_KEY = "xtablo-theme";
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
// Load theme from localStorage on initial render
|
||||
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY) as Theme;
|
||||
return savedTheme || "system";
|
||||
});
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "vite-ui-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
|
|
@ -26,22 +38,32 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
root.classList.add(systemTheme);
|
||||
} else {
|
||||
root.classList.add(theme);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save theme to localStorage whenever it changes
|
||||
localStorage.setItem(THEME_STORAGE_KEY, theme);
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
|
||||
return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>;
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider");
|
||||
|
||||
return context;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
194
ui/src/main.css
194
ui/src/main.css
|
|
@ -1,17 +1,80 @@
|
|||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@plugin 'tailwindcss-animate';
|
||||
@plugin '@tailwindcss/container-queries';
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/*
|
||||
---break---
|
||||
*/
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.145 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.145 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.396 0.141 25.723);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.269 0 0);
|
||||
--input: oklch(0.269 0 0);
|
||||
--ring: oklch(0.439 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.269 0 0);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
|
|
@ -27,6 +90,7 @@
|
|||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
|
|
@ -35,6 +99,10 @@
|
|||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
|
|
@ -43,103 +111,16 @@
|
|||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
--color-navbar-background: #292e39;
|
||||
--color-navbar-darker: #171920;
|
||||
|
||||
/* --color-logo-staging-background: #4a3522; */
|
||||
}
|
||||
|
||||
/*
|
||||
---break---
|
||||
*/
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.015 286.067);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||
}
|
||||
|
||||
/*
|
||||
---break---
|
||||
*/
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.92 0.004 286.32);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||
}
|
||||
|
||||
/*
|
||||
---break---
|
||||
*/
|
||||
|
||||
@layer base {
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
@apply border-[hsl(var(--border))];
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-[hsl(var(--background))] text-[hsl(var(--foreground))];
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -320,7 +301,8 @@
|
|||
transform: translate(-50%, -50%) rotate(0deg) translateX(150px) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) rotate(360deg) translateX(150px) rotate(-360deg);
|
||||
transform: translate(-50%, -50%) rotate(360deg) translateX(150px)
|
||||
rotate(-360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -329,7 +311,8 @@
|
|||
transform: translate(-50%, -50%) rotate(0deg) translateX(200px) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) rotate(-360deg) translateX(200px) rotate(360deg);
|
||||
transform: translate(-50%, -50%) rotate(-360deg) translateX(200px)
|
||||
rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -338,7 +321,8 @@
|
|||
transform: translate(-50%, -50%) rotate(0deg) translateX(100px) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) rotate(360deg) translateX(100px) rotate(-360deg);
|
||||
transform: translate(-50%, -50%) rotate(360deg) translateX(100px)
|
||||
rotate(-360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -516,7 +500,8 @@
|
|||
transform: translate(-50%, -50%) rotate(0deg) translateX(250px) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) rotate(360deg) translateX(250px) rotate(-360deg);
|
||||
transform: translate(-50%, -50%) rotate(360deg) translateX(250px)
|
||||
rotate(-360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -525,7 +510,8 @@
|
|||
transform: translate(-50%, -50%) rotate(0deg) translateX(120px) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) rotate(-360deg) translateX(120px) rotate(360deg);
|
||||
transform: translate(-50%, -50%) rotate(-360deg) translateX(120px)
|
||||
rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { TabloInsert, TabloUpdate, UserTablo } from "@ui/types/tablos.types";
|
|||
import {
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Eye,
|
||||
HelpCircle,
|
||||
LayoutGrid,
|
||||
List,
|
||||
|
|
@ -549,8 +550,8 @@ export const TabloPage = () => {
|
|||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{/* Quick action buttons */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="p-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -558,11 +559,11 @@ export const TabloPage = () => {
|
|||
}}
|
||||
title="Conversations"
|
||||
>
|
||||
<Users className="w-5 h-5" />
|
||||
<Users className="w-5 h-5 color-foreground" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="p-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -574,8 +575,8 @@ export const TabloPage = () => {
|
|||
</Button>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="p-2 text-destructive hover:text-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -587,9 +588,15 @@ export const TabloPage = () => {
|
|||
</Button>
|
||||
)}
|
||||
{!isAdmin && (
|
||||
<div className="p-2 text-muted-foreground" title="Lecture seule">
|
||||
<Shield className="w-5 h-5" />
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled
|
||||
className="p-2 text-muted-foreground hover:text-muted-foreground"
|
||||
title="Lecture seule"
|
||||
>
|
||||
<Eye className="w-5 h-5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue