feat(ui): drive mobile layouts from useIsMobile
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
} from "@/components/ui/popover";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { extractAllImagesFromClipboard } from "@/lib/clipboard-image";
|
||||
import {
|
||||
detectOs,
|
||||
@@ -114,6 +115,7 @@ export const AIToolbar = ({
|
||||
summaryUpdated,
|
||||
events,
|
||||
}: AIToolbarProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
const examplePrompts = [
|
||||
"Lunch with Maya next Thursday at 12:30pm at Toma, remind me 30 minutes before.",
|
||||
"Project sync tomorrow from 9am to 10am on Google Meet with a weekly repeat.",
|
||||
@@ -281,7 +283,13 @@ export const AIToolbar = ({
|
||||
</div>
|
||||
</div>
|
||||
) : isAuthenticated ? (
|
||||
<div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
||||
<div
|
||||
className={
|
||||
isMobile
|
||||
? "grid gap-3"
|
||||
: "grid gap-3 grid-cols-[minmax(0,1fr)_minmax(0,1fr)]"
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-[10px] bg-card shadow focus-within:ring-[3px] focus-within:ring-ring/20">
|
||||
<Textarea
|
||||
@@ -347,42 +355,47 @@ export const AIToolbar = ({
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<HoverCard
|
||||
openDelay={300}
|
||||
closeDelay={100}
|
||||
open={isPopoverOpen ? false : undefined}
|
||||
>
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||
<HoverCardTrigger asChild>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="hidden h-8 w-8 text-muted-foreground/70 hover:text-foreground md:inline-flex"
|
||||
aria-label="Keyboard shortcuts"
|
||||
>
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</HoverCardTrigger>
|
||||
<PopoverContent
|
||||
{!isMobile ? (
|
||||
<HoverCard
|
||||
openDelay={300}
|
||||
closeDelay={100}
|
||||
open={isPopoverOpen ? false : undefined}
|
||||
>
|
||||
<Popover
|
||||
open={isPopoverOpen}
|
||||
onOpenChange={setIsPopoverOpen}
|
||||
>
|
||||
<HoverCardTrigger asChild>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-muted-foreground/70 hover:text-foreground"
|
||||
aria-label="Keyboard shortcuts"
|
||||
>
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</HoverCardTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
side="top"
|
||||
sideOffset={6}
|
||||
className="w-52 p-3"
|
||||
>
|
||||
<ShortcutsList os={os} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<HoverCardContent
|
||||
align="start"
|
||||
side="top"
|
||||
sideOffset={6}
|
||||
className="w-52 p-3"
|
||||
>
|
||||
<ShortcutsList os={os} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<HoverCardContent
|
||||
align="start"
|
||||
side="top"
|
||||
sideOffset={6}
|
||||
className="w-52 p-3"
|
||||
>
|
||||
<ShortcutsList os={os} />
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
) : null}
|
||||
|
||||
{events.length > 0 && (
|
||||
<Button
|
||||
@@ -453,7 +466,9 @@ export const AIToolbar = ({
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 4 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="mt-3 grid gap-2 sm:grid-cols-2"
|
||||
className={
|
||||
isMobile ? "mt-3 grid gap-2" : "mt-3 grid gap-2 grid-cols-2"
|
||||
}
|
||||
>
|
||||
{imagePreviews.map((preview, index) => (
|
||||
<motion.div
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import {
|
||||
type EventFormValues,
|
||||
getDefaultEventFormValues,
|
||||
@@ -45,6 +46,7 @@ export const EventDialog = ({
|
||||
onSave,
|
||||
onReset,
|
||||
}: EventDialogProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
const isAiDraft = dialogSource === "ai" && !editingId;
|
||||
const titleText = editingId
|
||||
? "Edit Event"
|
||||
@@ -192,7 +194,11 @@ export const EventDialog = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div
|
||||
className={
|
||||
isMobile ? "grid grid-cols-1 gap-3" : "grid grid-cols-2 gap-3"
|
||||
}
|
||||
>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="event-location">Location</Label>
|
||||
<Controller
|
||||
@@ -329,7 +335,7 @@ export const EventDialog = ({
|
||||
)}
|
||||
</section>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<DialogFooter className={isMobile ? "gap-2" : "gap-0"}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { Sparkles, Zap } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import type { UserSettings } from "@/lib/user-settings";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -22,6 +25,7 @@ export function SettingsPanel({
|
||||
onSettingsChange,
|
||||
settings,
|
||||
}: SettingsPanelProps) {
|
||||
const isMobile = useIsMobile();
|
||||
const valuePrefix = hasLoadedSettings
|
||||
? "Current preference"
|
||||
: "Default value";
|
||||
@@ -35,7 +39,12 @@ export function SettingsPanel({
|
||||
|
||||
return (
|
||||
<section className={className}>
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div
|
||||
className={cn(
|
||||
"gap-4",
|
||||
isMobile ? "flex flex-col" : "flex items-start justify-between",
|
||||
)}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-primary">
|
||||
Settings
|
||||
@@ -51,7 +60,14 @@ export function SettingsPanel({
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-4 lg:grid-cols-[minmax(0,1.7fr)_minmax(18rem,1fr)]">
|
||||
<div
|
||||
className={cn(
|
||||
"mt-5 grid gap-4",
|
||||
isMobile
|
||||
? "grid-cols-1"
|
||||
: "grid-cols-[minmax(0,1.7fr)_minmax(18rem,1fr)]",
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-4">
|
||||
<div className={settingRowClasses}>
|
||||
<div className="flex items-start gap-3">
|
||||
@@ -149,7 +165,12 @@ export function SettingsPanel({
|
||||
{summaryDescription}
|
||||
</p>
|
||||
</div>
|
||||
<dl className="grid gap-3 sm:grid-cols-2 lg:grid-cols-1">
|
||||
<dl
|
||||
className={cn(
|
||||
"grid gap-3",
|
||||
isMobile ? "grid-cols-1" : "grid-cols-2",
|
||||
)}
|
||||
>
|
||||
<div className="rounded-[8px] bg-secondary p-3 shadow-[inset_0_0_0_1px_var(--color-border)]">
|
||||
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
||||
Direct create preference
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getDefaultClassNames,
|
||||
} from "react-day-picker";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Calendar({
|
||||
@@ -26,6 +27,7 @@ function Calendar({
|
||||
}: React.ComponentProps<typeof DayPicker> & {
|
||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"];
|
||||
}) {
|
||||
const isMobile = useIsMobile();
|
||||
const defaultClassNames = getDefaultClassNames();
|
||||
|
||||
return (
|
||||
@@ -46,7 +48,9 @@ function Calendar({
|
||||
classNames={{
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn(
|
||||
"flex gap-4 flex-col md:flex-row relative",
|
||||
isMobile
|
||||
? "relative flex flex-col gap-4"
|
||||
: "relative flex flex-row gap-4",
|
||||
defaultClassNames.months,
|
||||
),
|
||||
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
|
||||
interface DropdownItem {
|
||||
disabled?: boolean;
|
||||
@@ -28,6 +29,7 @@ interface DropdownItem {
|
||||
}
|
||||
|
||||
function CalendarDropdown(props: DropdownProps) {
|
||||
const isMobile = useIsMobile();
|
||||
const { options, value, onChange, "aria-label": ariaLabel } = props;
|
||||
|
||||
const items: DropdownItem[] =
|
||||
@@ -50,35 +52,35 @@ function CalendarDropdown(props: DropdownProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex items-center sm:hidden">
|
||||
<select
|
||||
aria-label={ariaLabel}
|
||||
className="absolute inset-0 z-10 w-full cursor-pointer opacity-0"
|
||||
value={value?.toString() ?? ""}
|
||||
onChange={onChange}
|
||||
>
|
||||
{options?.map((option) => (
|
||||
<option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="pointer-events-none h-8 w-full justify-between gap-2 px-2 font-medium"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{selectedItem?.label}
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:block">
|
||||
{isMobile ? (
|
||||
<div className="relative flex items-center">
|
||||
<select
|
||||
aria-label={ariaLabel}
|
||||
className="absolute inset-0 z-10 w-full cursor-pointer opacity-0"
|
||||
value={value?.toString() ?? ""}
|
||||
onChange={onChange}
|
||||
>
|
||||
{options?.map((option) => (
|
||||
<option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="pointer-events-none h-8 w-full justify-between gap-2 px-2 font-medium"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{selectedItem?.label}
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Combobox
|
||||
aria-label={ariaLabel}
|
||||
autoHighlight
|
||||
@@ -105,7 +107,7 @@ function CalendarDropdown(props: DropdownProps) {
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -139,6 +141,7 @@ export function DatePicker({
|
||||
month: controlledMonth,
|
||||
onMonthChange: controlledOnMonthChange,
|
||||
}: DatePickerProps) {
|
||||
const isMobile = useIsMobile();
|
||||
const fallbackId = React.useId();
|
||||
const id = providedId ?? fallbackId;
|
||||
|
||||
@@ -239,9 +242,21 @@ export function DatePicker({
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
{triggerContent}
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<div className="flex max-sm:flex-col">
|
||||
<div className="relative py-1 ps-1 max-sm:order-1 max-sm:border-t">
|
||||
<div className="flex h-full flex-col sm:border-e sm:pe-3">
|
||||
<div className={isMobile ? "flex flex-col" : "flex"}>
|
||||
<div
|
||||
className={
|
||||
isMobile
|
||||
? "relative order-1 border-t py-1 ps-1"
|
||||
: "relative py-1 ps-1"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
isMobile
|
||||
? "flex h-full flex-col"
|
||||
: "flex h-full flex-col border-e pe-3"
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className="w-full justify-start"
|
||||
onClick={() => {
|
||||
@@ -292,7 +307,7 @@ export function DatePicker({
|
||||
</div>
|
||||
</div>
|
||||
<Calendar
|
||||
className="max-sm:pb-3 sm:ps-2"
|
||||
className={isMobile ? "pb-3" : "ps-2"}
|
||||
mode="single"
|
||||
captionLayout="dropdown"
|
||||
components={{ Dropdown: CalendarDropdown }}
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Dialog({
|
||||
@@ -54,13 +55,16 @@ function DialogContent({
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-5 rounded-[10px] p-6 shadow-xl duration-200 sm:max-w-lg",
|
||||
"bg-card text-card-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-5 rounded-[10px] p-6 shadow-xl duration-200",
|
||||
isMobile ? undefined : "max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -91,11 +95,15 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
isMobile
|
||||
? "flex flex-col-reverse gap-2"
|
||||
: "flex flex-row justify-end gap-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -4,37 +4,53 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const inputGroupAddonVariants = cva(
|
||||
"flex h-auto cursor-text select-none items-center justify-center gap-2 leading-none [&>kbd]:rounded-[calc(var(--radius)-5px)] in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 [&_svg]:-mx-0.5 not-has-[button]:**:[svg:not([class*='opacity-'])]:opacity-80",
|
||||
{
|
||||
defaultVariants: {
|
||||
align: "inline-start",
|
||||
},
|
||||
variants: {
|
||||
align: {
|
||||
"block-end":
|
||||
"order-last w-full justify-start px-[calc(--spacing(3)-1px)] pb-[calc(--spacing(3)-1px)] [.border-t]:pt-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
|
||||
"block-start":
|
||||
"order-first w-full justify-start px-[calc(--spacing(3)-1px)] pt-[calc(--spacing(3)-1px)] [.border-b]:pb-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
|
||||
"inline-end":
|
||||
"order-last pe-[calc(--spacing(3)-1px)] has-[>:last-child[data-slot=badge]]:-me-1.5 has-[>button]:-me-2 has-[>kbd:last-child]:me-[-0.35rem] [[data-size=sm]+&]:pe-[calc(--spacing(2.5)-1px)]",
|
||||
"inline-start":
|
||||
"order-first ps-[calc(--spacing(3)-1px)] has-[>:last-child[data-slot=badge]]:-ms-1.5 has-[>button]:-ms-2 has-[>kbd:last-child]:ms-[-0.35rem] [[data-size=sm]+&]:ps-[calc(--spacing(2.5)-1px)]",
|
||||
},
|
||||
const inputGroupAddonBaseClasses =
|
||||
"flex h-auto cursor-text select-none items-center justify-center gap-2 leading-none [&>kbd]:rounded-[calc(var(--radius)-5px)] [&_svg]:-mx-0.5 not-has-[button]:**:[svg:not([class*='opacity-'])]:opacity-80";
|
||||
|
||||
const inputGroupRootBaseClasses =
|
||||
"relative inline-flex w-full min-w-0 items-center rounded-lg border border-input bg-background not-dark:bg-clip-padding text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_1px_--theme(--color-black/4%)] has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/64 has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/16 has-[textarea]:h-auto has-data-[align=block-end]:h-auto has-data-[align=block-start]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col has-[input:focus-visible,textarea:focus-visible]:border-ring has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/36 has-autofill:bg-foreground/4 has-[input:disabled,textarea:disabled]:opacity-64 has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,input[aria-invalid],textarea[aria-invalid]]:shadow-none has-[input:focus-visible,textarea:focus-visible]:ring-[3px] dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/24 dark:not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_-1px_--theme(--color-white/6%)] has-data-[align=inline-start]:**:[[data-size=sm]_input]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_input]:pe-1.5 *:[[data-slot=input-control],[data-slot=textarea-control]]:contents *:[[data-slot=input-control],[data-slot=textarea-control]]:before:hidden has-[[data-align=block-start],[data-align=block-end]]:**:[input]:h-auto has-data-[align=inline-start]:**:[[data-size=sm]_textarea]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_textarea]:pe-1.5 has-[[data-align=block-start],[data-align=block-end]]:**:[textarea]:h-auto";
|
||||
|
||||
const inputGroupTextBaseClasses =
|
||||
"line-clamp-1 flex items-center gap-2 whitespace-nowrap text-muted-foreground leading-none [&_svg]:pointer-events-none [&_svg]:-mx-0.5";
|
||||
|
||||
const inputGroupAddonVariants = cva(inputGroupAddonBaseClasses, {
|
||||
defaultVariants: {
|
||||
align: "inline-start",
|
||||
},
|
||||
variants: {
|
||||
align: {
|
||||
"block-end":
|
||||
"order-last w-full justify-start px-[calc(--spacing(3)-1px)] pb-[calc(--spacing(3)-1px)] [.border-t]:pt-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
|
||||
"block-start":
|
||||
"order-first w-full justify-start px-[calc(--spacing(3)-1px)] pt-[calc(--spacing(3)-1px)] [.border-b]:pb-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
|
||||
"inline-end":
|
||||
"order-last pe-[calc(--spacing(3)-1px)] has-[>:last-child[data-slot=badge]]:-me-1.5 has-[>button]:-me-2 has-[>kbd:last-child]:me-[-0.35rem] [[data-size=sm]+&]:pe-[calc(--spacing(2.5)-1px)]",
|
||||
"inline-start":
|
||||
"order-first ps-[calc(--spacing(3)-1px)] has-[>:last-child[data-slot=badge]]:-ms-1.5 has-[>button]:-ms-2 has-[>kbd:last-child]:ms-[-0.35rem] [[data-size=sm]+&]:ps-[calc(--spacing(2.5)-1px)]",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
function getAddonResponsiveClasses(isMobile: boolean) {
|
||||
return isMobile
|
||||
? "in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5"
|
||||
: "in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4";
|
||||
}
|
||||
|
||||
export function InputGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">): React.ReactElement {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative inline-flex w-full min-w-0 items-center rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_1px_--theme(--color-black/4%)] has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/64 has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/16 has-[textarea]:h-auto has-data-[align=block-end]:h-auto has-data-[align=block-start]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col has-[input:focus-visible,textarea:focus-visible]:border-ring has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/36 has-autofill:bg-foreground/4 has-[input:disabled,textarea:disabled]:opacity-64 has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,input[aria-invalid],textarea[aria-invalid]]:shadow-none has-[input:focus-visible,textarea:focus-visible]:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/24 dark:not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_-1px_--theme(--color-white/6%)] has-data-[align=inline-start]:**:[[data-size=sm]_input]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_input]:pe-1.5 *:[[data-slot=input-control],[data-slot=textarea-control]]:contents *:[[data-slot=input-control],[data-slot=textarea-control]]:before:hidden has-[[data-align=block-start],[data-align=block-end]]:**:[input]:h-auto has-data-[align=inline-start]:**:[input]:ps-2 has-data-[align=inline-end]:**:[input]:pe-2 has-data-[align=block-end]:**:[input]:pt-1.5 has-data-[align=block-start]:**:[input]:pb-1.5 **:[textarea]:min-h-20.5 **:[textarea]:resize-none **:[textarea]:py-[calc(--spacing(3)-1px)] **:[textarea]:max-sm:min-h-23.5 **:[textarea_button]:rounded-[calc(var(--radius-md)-1px)]",
|
||||
inputGroupRootBaseClasses,
|
||||
isMobile ? "text-base" : "text-sm",
|
||||
className,
|
||||
)}
|
||||
data-slot="input-group"
|
||||
@@ -49,9 +65,15 @@ export function InputGroupAddon({
|
||||
...props
|
||||
}: React.ComponentProps<"div"> &
|
||||
VariantProps<typeof inputGroupAddonVariants>): React.ReactElement {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||
className={cn(
|
||||
inputGroupAddonVariants({ align }),
|
||||
getAddonResponsiveClasses(isMobile),
|
||||
className,
|
||||
)}
|
||||
data-align={align}
|
||||
data-slot="input-group-addon"
|
||||
{...props}
|
||||
@@ -63,10 +85,13 @@ export function InputGroupText({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">): React.ReactElement {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"line-clamp-1 flex items-center gap-2 whitespace-nowrap text-muted-foreground leading-none in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-mx-0.5",
|
||||
inputGroupTextBaseClasses,
|
||||
getAddonResponsiveClasses(isMobile),
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import type * as React from "react";
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex field-sizing-content min-h-16 w-full rounded-[8px] bg-secondary px-3 py-2 text-base shadow-sm transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/25 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"placeholder:text-muted-foreground aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex field-sizing-content min-h-16 w-full rounded-[8px] bg-secondary px-3 py-2 shadow-sm transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/25 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
isMobile ? "text-base" : "text-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user