| author | |
| committer | |
| log | 29931dd3a9a4996a5282f59564a64e6be496d5c6 |
| tree | 119f238532a8068f809f3e278bf0983e95acc58e |
| parent | aca3c87cdbfb59ea5ed52fe2df82df508d57eb31 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>14 files changed, 129 insertions(+), 97 deletions(-)
apps/web/src/components/CustomSnoozeDialog.tsx+16-30| ... | @@ -1,5 +1,4 @@ | ... | @@ -1,5 +1,4 @@ |
| 1 | import { useEffect, useId, useState } from "react"; | 1 | import { useEffect, useId, useState } from "react"; |
| 2 | import { Tabs } from "@base-ui/react/tabs"; | ||
| 3 | import { create } from "zustand"; | 2 | import { create } from "zustand"; |
| 4 | import { | 3 | import { |
| 5 | localSnoozeDate, | 4 | localSnoozeDate, |
| ... | @@ -14,7 +13,7 @@ import { weekStartsOn } from "../timestampFormat"; | ... | @@ -14,7 +13,7 @@ import { weekStartsOn } from "../timestampFormat"; |
| 14 | import { Popover, PopoverTrigger, PopoverPopup } from "./ui/popover"; | 13 | import { Popover, PopoverTrigger, PopoverPopup } from "./ui/popover"; |
| 15 | import { Input } from "./ui/input"; | 14 | import { Input } from "./ui/input"; |
| 16 | import { Label } from "./ui/label"; | 15 | import { Label } from "./ui/label"; |
| 17 | import { toggleVariants } from "./ui/toggle"; | 16 | import { Toggle, ToggleGroup } from "./ui/toggle-group"; |
| 18 | import { Select, SelectTrigger, SelectValue, SelectPopup, SelectItem } from "./ui/select"; | 17 | import { Select, SelectTrigger, SelectValue, SelectPopup, SelectItem } from "./ui/select"; |
| 19 | import { | 18 | import { |
| 20 | NumberField, | 19 | NumberField, |
| ... | @@ -95,34 +94,21 @@ function CustomSnoozeDialog() { | ... | @@ -95,34 +94,21 @@ function CustomSnoozeDialog() { |
| 95 | <DialogDescription>Choose when snoozed threads return to your inbox.</DialogDescription> | 94 | <DialogDescription>Choose when snoozed threads return to your inbox.</DialogDescription> |
| 96 | </DialogHeader> | 95 | </DialogHeader> |
| 97 | <DialogPanel className="text-base sm:text-sm"> | 96 | <DialogPanel className="text-base sm:text-sm"> |
| 98 | <Tabs.Root | 97 | <div className="flex flex-col gap-4"> |
| 99 | value={mode} | 98 | <ToggleGroup |
| 100 | onValueChange={(value) => { | ||
| 101 | if (value === "date" || value === "duration") setMode(value); | ||
| 102 | setError(null); | ||
| 103 | }} | ||
| 104 | className="flex flex-col gap-4" | ||
| 105 | > | ||
| 106 | <Tabs.List | ||
| 107 | aria-label="Schedule type" | 99 | aria-label="Schedule type" |
| 108 | className="flex gap-0.5 rounded-lg bg-input/40 p-0.5" | 100 | className="w-full *:flex-1" |
| 101 | value={[mode]} | ||
| 102 | onValueChange={(next) => { | ||
| 103 | const value = next[0]; | ||
| 104 | if (value === "date" || value === "duration") setMode(value); | ||
| 105 | setError(null); | ||
| 106 | }} | ||
| 109 | > | 107 | > |
| 110 | {(["date", "duration"] as const).map((value) => ( | 108 | <Toggle value="date">Date and time</Toggle> |
| 111 | <Tabs.Tab | 109 | <Toggle value="duration">Duration</Toggle> |
| 112 | key={value} | 110 | </ToggleGroup> |
| 113 | value={value} | 111 | <div className="flex flex-col gap-4"> |
| 114 | data-pressed={mode === value ? "" : undefined} | ||
| 115 | className={toggleVariants({ | ||
| 116 | variant: "segmented", | ||
| 117 | size: "sm", | ||
| 118 | className: "flex-1", | ||
| 119 | })} | ||
| 120 | > | ||
| 121 | {value === "date" ? "Date and time" : "Duration"} | ||
| 122 | </Tabs.Tab> | ||
| 123 | ))} | ||
| 124 | </Tabs.List> | ||
| 125 | <Tabs.Panel value={mode} className="flex flex-col gap-4"> | ||
| 126 | {mode === "date" ? ( | 112 | {mode === "date" ? ( |
| 127 | <div className="grid grid-cols-1 gap-3 sm:grid-cols-2"> | 113 | <div className="grid grid-cols-1 gap-3 sm:grid-cols-2"> |
| 128 | <div className="flex min-w-0 flex-col gap-1.5"> | 114 | <div className="flex min-w-0 flex-col gap-1.5"> |
| ... | @@ -226,8 +212,8 @@ function CustomSnoozeDialog() { | ... | @@ -226,8 +212,8 @@ function CustomSnoozeDialog() { |
| 226 | </Label> | 212 | </Label> |
| 227 | </div> | 213 | </div> |
| 228 | )} | 214 | )} |
| 229 | </Tabs.Panel> | 215 | </div> |
| 230 | </Tabs.Root> | 216 | </div> |
| 231 | {error && ( | 217 | {error && ( |
| 232 | <p role="alert" className="text-destructive"> | 218 | <p role="alert" className="text-destructive"> |
| 233 | {error} | 219 | {error} |
apps/web/src/components/GitActionsControl.tsx+8-19| ... | @@ -48,7 +48,7 @@ import { | ... | @@ -48,7 +48,7 @@ import { |
| 48 | } from "~/components/Icons"; | 48 | } from "~/components/Icons"; |
| 49 | import { RadioGroup } from "~/components/ui/radio-group"; | 49 | import { RadioGroup } from "~/components/ui/radio-group"; |
| 50 | import { Spinner } from "~/components/ui/spinner"; | 50 | import { Spinner } from "~/components/ui/spinner"; |
| 51 | import { toggleVariants } from "~/components/ui/toggle"; | 51 | import { Toggle, ToggleGroup } from "~/components/ui/toggle-group"; |
| 52 | import { cn } from "~/lib/utils"; | 52 | import { cn } from "~/lib/utils"; |
| 53 | import { | 53 | import { |
| 54 | buildGitActionProgressStages, | 54 | buildGitActionProgressStages, |
| ... | @@ -815,10 +815,10 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { | ... | @@ -815,10 +815,10 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { |
| 815 | > | 815 | > |
| 816 | Protocol | 816 | Protocol |
| 817 | </span> | 817 | </span> |
| 818 | <RadioGroup | 818 | <ToggleGroup |
| 819 | className="w-fit flex-row gap-0.5 rounded-lg bg-input/40 p-0.5" | 819 | value={[publishProtocol]} |
| 820 | value={publishProtocol} | 820 | onValueChange={(next) => { |
| 821 | onValueChange={(protocol) => { | 821 | const protocol = next[0]; |
| 822 | if (protocol === "ssh" || protocol === "https") { | 822 | if (protocol === "ssh" || protocol === "https") { |
| 823 | setPublishProtocol(protocol); | 823 | setPublishProtocol(protocol); |
| 824 | } | 824 | } |
| ... | @@ -826,20 +826,9 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { | ... | @@ -826,20 +826,9 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { |
| 826 | aria-labelledby="publish-protocol-label" | 826 | aria-labelledby="publish-protocol-label" |
| 827 | disabled={publishRepositoryAction.isPending} | 827 | disabled={publishRepositoryAction.isPending} |
| 828 | > | 828 | > |
| 829 | {(["ssh", "https"] as const).map((protocol) => ( | 829 | <Toggle value="ssh">SSH</Toggle> |
| 830 | <RadioPrimitive.Root | 830 | <Toggle value="https">HTTPS</Toggle> |
| 831 | key={protocol} | 831 | </ToggleGroup> |
| 832 | value={protocol} | ||
| 833 | data-pressed={publishProtocol === protocol ? "" : undefined} | ||
| 834 | className={toggleVariants({ | ||
| 835 | variant: "segmented", | ||
| 836 | size: "segmented", | ||
| 837 | })} | ||
| 838 | > | ||
| 839 | {protocol.toUpperCase()} | ||
| 840 | </RadioPrimitive.Root> | ||
| 841 | ))} | ||
| 842 | </RadioGroup> | ||
| 843 | </div> | 832 | </div> |
| 844 | </div> | 833 | </div> |
| 845 | ) : null} | 834 | ) : null} |
apps/web/src/components/chat/ComposerBanner.tsx+10-10| ... | @@ -4,7 +4,7 @@ import { ChevronDownIcon, XIcon } from "lucide-react"; | ... | @@ -4,7 +4,7 @@ import { ChevronDownIcon, XIcon } from "lucide-react"; |
| 4 | import type { ComponentProps } from "react"; | 4 | import type { ComponentProps } from "react"; |
| 5 | 5 | ||
| 6 | import { cn } from "~/lib/utils"; | 6 | import { cn } from "~/lib/utils"; |
| 7 | import { Button, buttonVariants } from "../ui/button"; | 7 | import { Button } from "../ui/button"; |
| 8 | import { ScrollArea } from "../ui/scroll-area"; | 8 | import { ScrollArea } from "../ui/scroll-area"; |
| 9 | 9 | ||
| 10 | export type ComposerBannerVariant = "default" | "error" | "info" | "success" | "warning"; | 10 | export type ComposerBannerVariant = "default" | "error" | "info" | "success" | "warning"; |
| ... | @@ -323,18 +323,18 @@ function Dot({ className, ...props }: ComponentProps<"span">) { | ... | @@ -323,18 +323,18 @@ function Dot({ className, ...props }: ComponentProps<"span">) { |
| 323 | ); | 323 | ); |
| 324 | } | 324 | } |
| 325 | 325 | ||
| 326 | function ToggleIcon({ expanded, className }: { expanded: boolean; className?: string }) { | 326 | // Decorative: the row itself is the control, so this only matches Dismiss's box. |
| 327 | function ToggleIcon({ expanded }: { expanded: boolean }) { | ||
| 327 | return ( | 328 | return ( |
| 328 | <span | 329 | <Button |
| 329 | aria-hidden | 330 | render={<span aria-hidden />} |
| 330 | className={cn( | 331 | size="icon-xs" |
| 331 | buttonVariants({ size: "icon-xs", variant: "ghost" }), | 332 | variant="ghost" |
| 332 | "pointer-events-none", | 333 | tabIndex={-1} |
| 333 | className, | 334 | className="pointer-events-none" |
| 334 | )} | ||
| 335 | > | 335 | > |
| 336 | <ChevronDownIcon className={cn("size-3.5", !expanded && "rotate-180")} /> | 336 | <ChevronDownIcon className={cn("size-3.5", !expanded && "rotate-180")} /> |
| 337 | </span> | 337 | </Button> |
| 338 | ); | 338 | ); |
| 339 | } | 339 | } |
| 340 | 340 |
apps/web/src/components/chat/ProviderModelPicker.tsx+2-3| ... | @@ -5,9 +5,8 @@ import { | ... | @@ -5,9 +5,8 @@ import { |
| 5 | type ResolvedKeybindingsConfig, | 5 | type ResolvedKeybindingsConfig, |
| 6 | } from "@t3tools/contracts"; | 6 | } from "@t3tools/contracts"; |
| 7 | import { memo, useEffect, useMemo, useState } from "react"; | 7 | import { memo, useEffect, useMemo, useState } from "react"; |
| 8 | import type { VariantProps } from "class-variance-authority"; | ||
| 9 | import { Badge } from "../ui/badge"; | 8 | import { Badge } from "../ui/badge"; |
| 10 | import { buttonVariants } from "../ui/button"; | 9 | import type { ButtonVariant } from "../ui/button"; |
| 11 | import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; | 10 | import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; |
| 12 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; | 11 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 13 | import { cn } from "~/lib/utils"; | 12 | import { cn } from "~/lib/utils"; |
| ... | @@ -49,7 +48,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { | ... | @@ -49,7 +48,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { |
| 49 | disabled?: boolean; | 48 | disabled?: boolean; |
| 50 | terminalOpen?: boolean; | 49 | terminalOpen?: boolean; |
| 51 | open?: boolean; | 50 | open?: boolean; |
| 52 | triggerVariant?: VariantProps<typeof buttonVariants>["variant"]; | 51 | triggerVariant?: ButtonVariant; |
| 53 | triggerClassName?: string; | 52 | triggerClassName?: string; |
| 54 | /** Aggregate settings can show a neutral value without claiming one provider is selected. */ | 53 | /** Aggregate settings can show a neutral value without claiming one provider is selected. */ |
| 55 | triggerLabel?: string; | 54 | triggerLabel?: string; |
apps/web/src/components/chat/TraitsPicker.tsx+2-3| ... | @@ -16,9 +16,8 @@ import { | ... | @@ -16,9 +16,8 @@ import { |
| 16 | normalizeModelSlug, | 16 | normalizeModelSlug, |
| 17 | } from "@t3tools/shared/model"; | 17 | } from "@t3tools/shared/model"; |
| 18 | import { memo, useCallback } from "react"; | 18 | import { memo, useCallback } from "react"; |
| 19 | import type { VariantProps } from "class-variance-authority"; | ||
| 20 | import { GaugeIcon, ZapIcon } from "lucide-react"; | 19 | import { GaugeIcon, ZapIcon } from "lucide-react"; |
| 21 | import { buttonVariants } from "../ui/button"; | 20 | import type { ButtonVariant } from "../ui/button"; |
| 22 | import { | 21 | import { |
| 23 | Menu, | 22 | Menu, |
| 24 | MenuGroup, | 23 | MenuGroup, |
| ... | @@ -277,7 +276,7 @@ export interface TraitsMenuContentProps { | ... | @@ -277,7 +276,7 @@ export interface TraitsMenuContentProps { |
| 277 | modelOptions?: ProviderOptions | null | undefined; | 276 | modelOptions?: ProviderOptions | null | undefined; |
| 278 | allowPromptInjectedEffort?: boolean; | 277 | allowPromptInjectedEffort?: boolean; |
| 279 | planModeEnabled: boolean; | 278 | planModeEnabled: boolean; |
| 280 | triggerVariant?: VariantProps<typeof buttonVariants>["variant"]; | 279 | triggerVariant?: ButtonVariant; |
| 281 | triggerClassName?: string; | 280 | triggerClassName?: string; |
| 282 | isComposerOwned?: boolean; | 281 | isComposerOwned?: boolean; |
| 283 | } | 282 | } |
apps/web/src/components/chat/composerProviderState.tsx+2-3| ... | @@ -13,10 +13,9 @@ import { | ... | @@ -13,10 +13,9 @@ import { |
| 13 | isClaudeUltrathinkPrompt, | 13 | isClaudeUltrathinkPrompt, |
| 14 | normalizeModelSlug, | 14 | normalizeModelSlug, |
| 15 | } from "@t3tools/shared/model"; | 15 | } from "@t3tools/shared/model"; |
| 16 | import type { VariantProps } from "class-variance-authority"; | ||
| 17 | import type { ReactNode } from "react"; | 16 | import type { ReactNode } from "react"; |
| 18 | 17 | ||
| 19 | import type { buttonVariants } from "../ui/button"; | 18 | import type { ButtonVariant } from "../ui/button"; |
| 20 | import type { DraftId } from "../../composerDraftStore"; | 19 | import type { DraftId } from "../../composerDraftStore"; |
| 21 | import { getProviderModelCapabilities } from "../../providerModels"; | 20 | import { getProviderModelCapabilities } from "../../providerModels"; |
| 22 | import type { ComposerControlSize } from "./ComposerControl"; | 21 | import type { ComposerControlSize } from "./ComposerControl"; |
| ... | @@ -55,7 +54,7 @@ type TraitsRenderInput = { | ... | @@ -55,7 +54,7 @@ type TraitsRenderInput = { |
| 55 | planModeEnabled: boolean; | 54 | planModeEnabled: boolean; |
| 56 | size?: ComposerControlSize; | 55 | size?: ComposerControlSize; |
| 57 | hidden?: boolean; | 56 | hidden?: boolean; |
| 58 | triggerVariant?: VariantProps<typeof buttonVariants>["variant"]; | 57 | triggerVariant?: ButtonVariant; |
| 59 | triggerClassName?: string; | 58 | triggerClassName?: string; |
| 60 | isComposerOwned?: boolean; | 59 | isComposerOwned?: boolean; |
| 61 | }; | 60 | }; |
apps/web/src/components/settings/FontFamilyPicker.tsx+4-7| ... | @@ -1,5 +1,5 @@ | ... | @@ -1,5 +1,5 @@ |
| 1 | import { LegendList, type LegendListRef } from "@legendapp/list/react"; | 1 | import { LegendList, type LegendListRef } from "@legendapp/list/react"; |
| 2 | import { CheckIcon, ChevronDownIcon } from "lucide-react"; | 2 | import { CheckIcon } from "lucide-react"; |
| 3 | import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; | 3 | import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; |
| 4 | import { isMonospaceFamily, queryInstalledFontFamilies } from "../../appearanceFonts"; | 4 | import { isMonospaceFamily, queryInstalledFontFamilies } from "../../appearanceFonts"; |
| 5 | import { | 5 | import { |
| ... | @@ -11,7 +11,7 @@ import { | ... | @@ -11,7 +11,7 @@ import { |
| 11 | ComboboxPopup, | 11 | ComboboxPopup, |
| 12 | ComboboxTrigger, | 12 | ComboboxTrigger, |
| 13 | } from "../ui/combobox"; | 13 | } from "../ui/combobox"; |
| 14 | import { selectTriggerVariants } from "../ui/select"; | 14 | import { SelectButton } from "../ui/select"; |
| 15 | 15 | ||
| 16 | const DEFAULT_FONT_VALUE = "__default__"; | 16 | const DEFAULT_FONT_VALUE = "__default__"; |
| 17 | 17 | ||
| ... | @@ -204,11 +204,8 @@ export function FontFamilyPicker({ | ... | @@ -204,11 +204,8 @@ export function FontFamilyPicker({ |
| 204 | void listRef.current?.scrollIndexIntoView?.({ index: eventDetails.index, animated: false }); | 204 | void listRef.current?.scrollIndexIntoView?.({ index: eventDetails.index, animated: false }); |
| 205 | }} | 205 | }} |
| 206 | > | 206 | > |
| 207 | <ComboboxTrigger aria-label={ariaLabel} className={selectTriggerVariants({ size: "sm" })}> | 207 | <ComboboxTrigger aria-label={ariaLabel} render={<SelectButton size="sm" />}> |
| 208 | <span className="min-w-0 truncate"> | 208 | {selectedFamily.length === 0 ? defaultFamily : selectedFamily} |
| 209 | {selectedFamily.length === 0 ? defaultFamily : selectedFamily} | ||
| 210 | </span> | ||
| 211 | <ChevronDownIcon className="-me-1 size-3 opacity-50" /> | ||
| 212 | </ComboboxTrigger> | 209 | </ComboboxTrigger> |
| 213 | <ComboboxPopup align="end" className="flex w-72 flex-col"> | 210 | <ComboboxPopup align="end" className="flex w-72 flex-col"> |
| 214 | <ComboboxSearchInput | 211 | <ComboboxSearchInput |
apps/web/src/components/settings/SnapShotSettings.tsx+13-17| ... | @@ -7,11 +7,9 @@ import { | ... | @@ -7,11 +7,9 @@ import { |
| 7 | type DesktopSnapShotSetupAction, | 7 | type DesktopSnapShotSetupAction, |
| 8 | type SnapShotShortcut, | 8 | type SnapShotShortcut, |
| 9 | } from "@t3tools/contracts"; | 9 | } from "@t3tools/contracts"; |
| 10 | import { ChevronDownIcon, PlayIcon } from "lucide-react"; | 10 | import { PlayIcon } from "lucide-react"; |
| 11 | import { useCallback, useEffect, useRef, useState } from "react"; | 11 | import { useCallback, useEffect, useRef, useState } from "react"; |
| 12 | 12 | ||
| 13 | import { cn } from "~/lib/utils"; | ||
| 14 | |||
| 15 | import { useClientSettings, useUpdateClientSettings } from "../../hooks/useSettings"; | 13 | import { useClientSettings, useUpdateClientSettings } from "../../hooks/useSettings"; |
| 16 | import { getDesktopSnapShotBridge } from "../../lib/desktopSnapShot"; | 14 | import { getDesktopSnapShotBridge } from "../../lib/desktopSnapShot"; |
| 17 | import { | 15 | import { |
| ... | @@ -44,7 +42,7 @@ import { | ... | @@ -44,7 +42,7 @@ import { |
| 44 | import { searchableSetting } from "./settingsSearch"; | 42 | import { searchableSetting } from "./settingsSearch"; |
| 45 | import { Button } from "../ui/button"; | 43 | import { Button } from "../ui/button"; |
| 46 | import { Menu, MenuItem, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "../ui/menu"; | 44 | import { Menu, MenuItem, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "../ui/menu"; |
| 47 | import { selectTriggerVariants } from "../ui/select"; | 45 | import { SelectButton } from "../ui/select"; |
| 48 | import { Switch } from "../ui/switch"; | 46 | import { Switch } from "../ui/switch"; |
| 49 | import { toastManager } from "../ui/toast"; | 47 | import { toastManager } from "../ui/toast"; |
| 50 | import { SnapShotSetupDialog } from "./SnapShotSetupDialog"; | 48 | import { SnapShotSetupDialog } from "./SnapShotSetupDialog"; |
| ... | @@ -497,21 +495,19 @@ export function SnapShotSettings() { | ... | @@ -497,21 +495,19 @@ export function SnapShotSettings() { |
| 497 | <Menu> | 495 | <Menu> |
| 498 | <MenuTrigger | 496 | <MenuTrigger |
| 499 | aria-label={"Snapshot sound: " + soundLabel} | 497 | aria-label={"Snapshot sound: " + soundLabel} |
| 500 | className={cn(selectTriggerVariants({ size: "sm" }), "w-auto min-w-0")} | 498 | render={<SelectButton size="sm" />} |
| 499 | className="w-auto min-w-0" | ||
| 501 | disabled={!captureAvailable} | 500 | disabled={!captureAvailable} |
| 502 | > | 501 | > |
| 503 | <span className="min-w-0 flex-1 truncate text-left"> | 502 | {soundSelection === "off" ? ( |
| 504 | {soundSelection === "off" ? ( | 503 | "Off" |
| 505 | "Off" | 504 | ) : soundSelection === "soft-pop" ? ( |
| 506 | ) : soundSelection === "soft-pop" ? ( | 505 | <> |
| 507 | <> | 506 | Whoosh <span className="text-muted-foreground">(Default)</span> |
| 508 | Whoosh <span className="text-muted-foreground">(Default)</span> | 507 | </> |
| 509 | </> | 508 | ) : ( |
| 510 | ) : ( | 509 | "Click" |
| 511 | "Click" | 510 | )} |
| 512 | )} | ||
| 513 | </span> | ||
| 514 | <ChevronDownIcon className="-me-1 size-3 shrink-0 opacity-50" /> | ||
| 515 | </MenuTrigger> | 511 | </MenuTrigger> |
| 516 | <MenuPopup align="end"> | 512 | <MenuPopup align="end"> |
| 517 | <MenuRadioGroup | 513 | <MenuRadioGroup |
apps/web/src/components/ui/badge.tsx+1| ... | @@ -60,4 +60,5 @@ function Badge({ className, variant, size, render, ...props }: BadgeProps) { | ... | @@ -60,4 +60,5 @@ function Badge({ className, variant, size, render, ...props }: BadgeProps) { |
| 60 | }); | 60 | }); |
| 61 | } | 61 | } |
| 62 | 62 | ||
| 63 | // The *Variants function is for components/ui modules only. | ||
| 63 | export { Badge, badgeVariants }; | 64 | export { Badge, badgeVariants }; |
apps/web/src/components/ui/button.tsx+8-3| ... | @@ -71,9 +71,12 @@ const buttonVariants = cva( | ... | @@ -71,9 +71,12 @@ const buttonVariants = cva( |
| 71 | }, | 71 | }, |
| 72 | ); | 72 | ); |
| 73 | 73 | ||
| 74 | type ButtonVariant = NonNullable<VariantProps<typeof buttonVariants>["variant"]>; | ||
| 75 | type ButtonSize = NonNullable<VariantProps<typeof buttonVariants>["size"]>; | ||
| 76 | |||
| 74 | interface ButtonProps extends useRender.ComponentProps<"button"> { | 77 | interface ButtonProps extends useRender.ComponentProps<"button"> { |
| 75 | variant?: VariantProps<typeof buttonVariants>["variant"]; | 78 | variant?: ButtonVariant; |
| 76 | size?: VariantProps<typeof buttonVariants>["size"]; | 79 | size?: ButtonSize; |
| 77 | } | 80 | } |
| 78 | 81 | ||
| 79 | function Button({ className, variant, size, render, ...props }: ButtonProps) { | 82 | function Button({ className, variant, size, render, ...props }: ButtonProps) { |
| ... | @@ -95,7 +98,9 @@ function Button({ className, variant, size, render, ...props }: ButtonProps) { | ... | @@ -95,7 +98,9 @@ function Button({ className, variant, size, render, ...props }: ButtonProps) { |
| 95 | }); | 98 | }); |
| 96 | } | 99 | } |
| 97 | 100 | ||
| 98 | export { Button, buttonVariants }; | 101 | // buttonVariants is exported for other components/ui modules only; app code |
| 102 | // renders a Button (with `render` for other elements) instead. | ||
| 103 | export { Button, buttonVariants, type ButtonSize, type ButtonVariant }; | ||
| 99 | 104 | ||
| 100 | const inlineButtonVariants = cva( | 105 | const inlineButtonVariants = cva( |
| 101 | "inline-flex shrink-0 cursor-pointer items-center gap-0.5 whitespace-nowrap font-medium underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-64", | 106 | "inline-flex shrink-0 cursor-pointer items-center gap-0.5 whitespace-nowrap font-medium underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-64", |
apps/web/src/components/ui/select.tsx+34-1| ... | @@ -1,5 +1,7 @@ | ... | @@ -1,5 +1,7 @@ |
| 1 | "use client"; | 1 | "use client"; |
| 2 | import { mergeProps } from "@base-ui/react/merge-props"; | ||
| 2 | import { Select as SelectPrimitive } from "@base-ui/react/select"; | 3 | import { Select as SelectPrimitive } from "@base-ui/react/select"; |
| 4 | import { useRender } from "@base-ui/react/use-render"; | ||
| 3 | import { cva, type VariantProps } from "class-variance-authority"; | 5 | import { cva, type VariantProps } from "class-variance-authority"; |
| 4 | import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; | 6 | import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; |
| 5 | import type * as React from "react"; | 7 | import type * as React from "react"; |
| ... | @@ -57,6 +59,37 @@ function SelectTrigger({ | ... | @@ -57,6 +59,37 @@ function SelectTrigger({ |
| 57 | ); | 59 | ); |
| 58 | } | 60 | } |
| 59 | 61 | ||
| 62 | /** | ||
| 63 | * The select-field look for a picker that is not a Select, such as a Menu or | ||
| 64 | * Combobox trigger. Render it as that trigger: `<MenuTrigger render={<SelectButton />}>`. | ||
| 65 | */ | ||
| 66 | function SelectButton({ | ||
| 67 | className, | ||
| 68 | size = "default", | ||
| 69 | children, | ||
| 70 | render, | ||
| 71 | ...props | ||
| 72 | }: useRender.ComponentProps<"button"> & Pick<VariantProps<typeof selectTriggerVariants>, "size">) { | ||
| 73 | const defaultProps = { | ||
| 74 | className: cn(selectTriggerVariants({ size }), className), | ||
| 75 | "data-slot": "select-trigger", | ||
| 76 | type: render ? undefined : ("button" as const), | ||
| 77 | }; | ||
| 78 | return useRender({ | ||
| 79 | defaultTagName: "button", | ||
| 80 | props: { | ||
| 81 | ...mergeProps<"button">(defaultProps, props), | ||
| 82 | children: ( | ||
| 83 | <> | ||
| 84 | <span className="min-w-0 flex-1 truncate text-left">{children}</span> | ||
| 85 | <ChevronDownIcon aria-hidden className="-me-1 size-3 shrink-0 opacity-50" /> | ||
| 86 | </> | ||
| 87 | ), | ||
| 88 | }, | ||
| 89 | render, | ||
| 90 | }); | ||
| 91 | } | ||
| 92 | |||
| 60 | function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { | 93 | function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { |
| 61 | return ( | 94 | return ( |
| 62 | <SelectPrimitive.Value | 95 | <SelectPrimitive.Value |
| ... | @@ -182,7 +215,7 @@ function SelectGroupLabel(props: SelectPrimitive.GroupLabel.Props) { | ... | @@ -182,7 +215,7 @@ function SelectGroupLabel(props: SelectPrimitive.GroupLabel.Props) { |
| 182 | export { | 215 | export { |
| 183 | Select, | 216 | Select, |
| 184 | SelectTrigger, | 217 | SelectTrigger, |
| 185 | selectTriggerVariants, | 218 | SelectButton, |
| 186 | SelectValue, | 219 | SelectValue, |
| 187 | SelectPopup, | 220 | SelectPopup, |
| 188 | SelectPopup as SelectContent, | 221 | SelectPopup as SelectContent, |
apps/web/src/components/ui/toggle.tsx+1| ... | @@ -52,4 +52,5 @@ function Toggle({ | ... | @@ -52,4 +52,5 @@ function Toggle({ |
| 52 | ); | 52 | ); |
| 53 | } | 53 | } |
| 54 | 54 | ||
| 55 | // The *Variants function is for components/ui modules only. | ||
| 55 | export { Toggle, toggleVariants }; | 56 | export { Toggle, toggleVariants }; |
scripts/lint-restyle-ceiling.ts+1-1| ... | @@ -8,7 +8,7 @@ import * as NodeURL from "node:url"; | ... | @@ -8,7 +8,7 @@ import * as NodeURL from "node:url"; |
| 8 | // components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the | 8 | // components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the |
| 9 | // count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you | 9 | // count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you |
| 10 | // migrate a file, and delete this script when the rule becomes an error. | 10 | // migrate a file, and delete this script when the rule becomes an error. |
| 11 | export const RESTYLE_CEILING = 627; | 11 | export const RESTYLE_CEILING = 623; |
| 12 | 12 | ||
| 13 | const RULE = "shadcn(no-restyle)"; | 13 | const RULE = "shadcn(no-restyle)"; |
| 14 | 14 |
vite.config.ts+27| ... | @@ -16,6 +16,20 @@ const RESTRICTED_IMPORT_PATHS = [ | ... | @@ -16,6 +16,20 @@ const RESTRICTED_IMPORT_PATHS = [ |
| 16 | }, | 16 | }, |
| 17 | ]; | 17 | ]; |
| 18 | 18 | ||
| 19 | /** | ||
| 20 | * The cva functions behind components/ui exports. They style a foreign element to look | ||
| 21 | * like a Button or Toggle, which bypasses the component's variants; render the component | ||
| 22 | * instead (`render={<Button …/>}`, or `SelectButton` for a picker trigger). | ||
| 23 | */ | ||
| 24 | const RESTRICTED_UI_VARIANT_PATTERNS = [ | ||
| 25 | { | ||
| 26 | group: ["**/components/ui/*", "**/ui/*", "./ui/*"], | ||
| 27 | importNames: ["buttonVariants", "toggleVariants", "badgeVariants", "selectTriggerVariants"], | ||
| 28 | message: | ||
| 29 | "Render the components/ui export instead of borrowing its class recipe (render={<Button …/>}, SelectButton, ToggleGroup).", | ||
| 30 | }, | ||
| 31 | ]; | ||
| 32 | |||
| 19 | /** Lucide's pull-request glyphs, which only `pullRequestIcons.tsx` may name. */ | 33 | /** Lucide's pull-request glyphs, which only `pullRequestIcons.tsx` may name. */ |
| 20 | const RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS = { | 34 | const RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS = { |
| 21 | name: "lucide-react", | 35 | name: "lucide-react", |
| ... | @@ -160,6 +174,19 @@ export default defineConfig({ | ... | @@ -160,6 +174,19 @@ export default defineConfig({ |
| 160 | files: ["packages/shared/src/hostProcess.ts"], | 174 | files: ["packages/shared/src/hostProcess.ts"], |
| 161 | rules: { "t3code/no-global-process-runtime": "off" }, | 175 | rules: { "t3code/no-global-process-runtime": "off" }, |
| 162 | }, | 176 | }, |
| 177 | { | ||
| 178 | files: ["apps/web/src/**"], | ||
| 179 | excludeFiles: ["apps/web/src/components/ui/**"], | ||
| 180 | rules: { | ||
| 181 | "eslint/no-restricted-imports": [ | ||
| 182 | "error", | ||
| 183 | { | ||
| 184 | paths: [...RESTRICTED_IMPORT_PATHS, RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS], | ||
| 185 | patterns: RESTRICTED_UI_VARIANT_PATTERNS, | ||
| 186 | }, | ||
| 187 | ], | ||
| 188 | }, | ||
| 189 | }, | ||
| 163 | { | 190 | { |
| 164 | // The one module allowed to name lucide's pull-request glyphs; everything else picks | 191 | // The one module allowed to name lucide's pull-request glyphs; everything else picks |
| 165 | // from its vocabulary. The other import restrictions still apply here. | 192 | // from its vocabulary. The other import restrictions still apply here. |