Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions apps/web/src/components/CustomSnoozeDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Calendar } from "./ui/calendar";
import { weekStartsOn } from "../timestampFormat";
import { useNowMinute } from "../hooks/useNowMinute";
import { Popover, PopoverTrigger, PopoverPopup } from "./ui/popover";
import { Input } from "./ui/input";
import { TimePicker } from "./ui/time-picker";
import { Label } from "./ui/label";
import { Toggle, ToggleGroup } from "./ui/toggle-group";
import { Select, SelectTrigger, SelectValue, SelectPopup, SelectItem } from "./ui/select";
Expand Down Expand Up @@ -160,21 +160,17 @@ function CustomSnoozeDialog() {
</PopoverPopup>
</Popover>
</div>
<Label className="flex min-w-0 flex-col items-stretch" htmlFor={`${id}-time`}>
Time
<Input
nativeInput
<div className="flex min-w-0 flex-col gap-1.5">
<Label htmlFor={`${id}-time`}>Time</Label>
<TimePicker
id={`${id}-time`}
className="h-9 sm:h-8"
type="time"
required
value={time}
onChange={(event) => {
setTime(event.target.value);
onValueChange={(value) => {
setTime(value);
setError(null);
}}
/>
</Label>
</div>
</div>
) : (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
Expand Down
22 changes: 8 additions & 14 deletions apps/web/src/components/settings/SnoozePresetsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { useNowMinute } from "../../hooks/useNowMinute";
import { reconcileSnoozeDraft, sortSnoozeRows } from "./SnoozePresetsSettings.logic";
import { formatShortTimestamp } from "../../timestampFormat";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { TimePicker } from "../ui/time-picker";
import {
NumberField,
NumberFieldDecrement,
Expand Down Expand Up @@ -375,19 +375,13 @@ function SnoozePresetFields(props: {
) : (
<>
<span className="text-xs text-muted-foreground">at</span>
<Input
nativeInput
size="sm"
type="time"
className="w-32"
aria-label="Time"
value={preset.time}
onChange={(event) => {
if (/^\d{2}:\d{2}$/.test(event.target.value)) {
onChange({ ...preset, time: event.target.value });
}
}}
/>
<div className="w-32">
<TimePicker
size="sm"
value={preset.time}
onValueChange={(time) => onChange({ ...preset, time })}
/>
</div>
</>
)}
</div>
Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/components/ui/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
function SelectPopup({
className,
children,
variant = "default",
side = "bottom",
sideOffset = 4,
align = "start",
Expand All @@ -107,6 +108,7 @@ function SelectPopup({
anchor,
...props
}: SelectPrimitive.Popup.Props & {
variant?: "default" | "compact";
side?: SelectPrimitive.Positioner.Props["side"];
sideOffset?: SelectPrimitive.Positioner.Props["sideOffset"];
align?: SelectPrimitive.Positioner.Props["align"];
Expand Down Expand Up @@ -145,7 +147,13 @@ function SelectPopup({
)}
>
<SelectPrimitive.List
className={cn("max-h-(--available-height) overflow-y-auto p-1", className)}
className={cn(
"overflow-y-auto p-1",
variant === "compact"
? "max-h-[min(15rem,var(--available-height))] scroll-py-12"
: "max-h-(--available-height)",
className,
)}
data-slot="select-list"
>
{children}
Expand Down
67 changes: 67 additions & 0 deletions apps/web/src/components/ui/time-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./select";

const HOURS = Array.from({ length: 24 }, (_, hour) => {
const value = String(hour).padStart(2, "0");
return { value, label: value };
});
const MINUTES = Array.from({ length: 60 }, (_, minute) => {
const value = String(minute).padStart(2, "0");
return { value, label: value };
});

export function TimePicker({
id,
value,
onValueChange,
size = "default",
}: {
id?: string;
value: string;
onValueChange: (value: string) => void;
size?: "default" | "sm";
}) {
const [hour, minute] = value.split(":");
return (
<div role="group" aria-label="Time" className="flex min-w-0 items-center gap-1.5">
<Select
value={hour}
items={HOURS}
onValueChange={(next) => {
if (next !== null) onValueChange(`${next}:${minute}`);
}}
>
<SelectTrigger id={id} size={size} className="min-w-0 flex-1" aria-label="Hours">
<SelectValue />
</SelectTrigger>
<SelectPopup alignItemWithTrigger={false} variant="compact">
{HOURS.map(({ value, label }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectPopup>
</Select>
<span aria-hidden className="text-muted-foreground">
:
</span>
<Select
value={minute}
items={MINUTES}
onValueChange={(next) => {
if (next !== null) onValueChange(`${hour}:${next}`);
}}
>
<SelectTrigger size={size} className="min-w-0 flex-1" aria-label="Minutes">
<SelectValue />
</SelectTrigger>
<SelectPopup alignItemWithTrigger={false} variant="compact">
{MINUTES.map(({ value, label }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectPopup>
</Select>
</div>
);
}
Loading