authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-22 21:33:00-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-22 21:33:00-07:00
log29931dd3a9a4996a5282f59564a64e6be496d5c6
tree119f238532a8068f809f3e278bf0983e95acc58e
parentaca3c87cdbfb59ea5ed52fe2df82df508d57eb31
signature Signed by PGP key B5690EEEBB952194

hatch/variant functions (#13191)

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 @@
1import { useEffect, useId, useState } from "react";1import { useEffect, useId, useState } from "react";
2import { Tabs } from "@base-ui/react/tabs";
3import { create } from "zustand";2import { create } from "zustand";
4import {3import {
5 localSnoozeDate,4 localSnoozeDate,
...@@ -14,7 +13,7 @@ import { weekStartsOn } from "../timestampFormat";...@@ -14,7 +13,7 @@ import { weekStartsOn } from "../timestampFormat";
14import { Popover, PopoverTrigger, PopoverPopup } from "./ui/popover";13import { Popover, PopoverTrigger, PopoverPopup } from "./ui/popover";
15import { Input } from "./ui/input";14import { Input } from "./ui/input";
16import { Label } from "./ui/label";15import { Label } from "./ui/label";
17import { toggleVariants } from "./ui/toggle";16import { Toggle, ToggleGroup } from "./ui/toggle-group";
18import { Select, SelectTrigger, SelectValue, SelectPopup, SelectItem } from "./ui/select";17import { Select, SelectTrigger, SelectValue, SelectPopup, SelectItem } from "./ui/select";
19import {18import {
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.Root97 <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.Tab109 <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";
49import { RadioGroup } from "~/components/ui/radio-group";49import { RadioGroup } from "~/components/ui/radio-group";
50import { Spinner } from "~/components/ui/spinner";50import { Spinner } from "~/components/ui/spinner";
51import { toggleVariants } from "~/components/ui/toggle";51import { Toggle, ToggleGroup } from "~/components/ui/toggle-group";
52import { cn } from "~/lib/utils";52import { cn } from "~/lib/utils";
53import {53import {
54 buildGitActionProgressStages,54 buildGitActionProgressStages,
...@@ -815,10 +815,10 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {...@@ -815,10 +815,10 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
815 >815 >
816 Protocol816 Protocol
817 </span>817 </span>
818 <RadioGroup818 <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.Root830 <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";
4import type { ComponentProps } from "react";4import type { ComponentProps } from "react";
55
6import { cn } from "~/lib/utils";6import { cn } from "~/lib/utils";
7import { Button, buttonVariants } from "../ui/button";7import { Button } from "../ui/button";
8import { ScrollArea } from "../ui/scroll-area";8import { ScrollArea } from "../ui/scroll-area";
99
10export type ComposerBannerVariant = "default" | "error" | "info" | "success" | "warning";10export 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}
325325
326function ToggleIcon({ expanded, className }: { expanded: boolean; className?: string }) {326// Decorative: the row itself is the control, so this only matches Dismiss's box.
327function ToggleIcon({ expanded }: { expanded: boolean }) {
327 return (328 return (
328 <span329 <Button
329 aria-hidden330 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}
340340
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";
7import { memo, useEffect, useMemo, useState } from "react";7import { memo, useEffect, useMemo, useState } from "react";
8import type { VariantProps } from "class-variance-authority";
9import { Badge } from "../ui/badge";8import { Badge } from "../ui/badge";
10import { buttonVariants } from "../ui/button";9import type { ButtonVariant } from "../ui/button";
11import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";10import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
12import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";11import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
13import { cn } from "~/lib/utils";12import { 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";
18import { memo, useCallback } from "react";18import { memo, useCallback } from "react";
19import type { VariantProps } from "class-variance-authority";
20import { GaugeIcon, ZapIcon } from "lucide-react";19import { GaugeIcon, ZapIcon } from "lucide-react";
21import { buttonVariants } from "../ui/button";20import type { ButtonVariant } from "../ui/button";
22import {21import {
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";
16import type { VariantProps } from "class-variance-authority";
17import type { ReactNode } from "react";16import type { ReactNode } from "react";
1817
19import type { buttonVariants } from "../ui/button";18import type { ButtonVariant } from "../ui/button";
20import type { DraftId } from "../../composerDraftStore";19import type { DraftId } from "../../composerDraftStore";
21import { getProviderModelCapabilities } from "../../providerModels";20import { getProviderModelCapabilities } from "../../providerModels";
22import type { ComposerControlSize } from "./ComposerControl";21import 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 @@
1import { LegendList, type LegendListRef } from "@legendapp/list/react";1import { LegendList, type LegendListRef } from "@legendapp/list/react";
2import { CheckIcon, ChevronDownIcon } from "lucide-react";2import { CheckIcon } from "lucide-react";
3import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";3import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4import { isMonospaceFamily, queryInstalledFontFamilies } from "../../appearanceFonts";4import { isMonospaceFamily, queryInstalledFontFamilies } from "../../appearanceFonts";
5import {5import {
...@@ -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";
14import { selectTriggerVariants } from "../ui/select";14import { SelectButton } from "../ui/select";
1515
16const DEFAULT_FONT_VALUE = "__default__";16const DEFAULT_FONT_VALUE = "__default__";
1717
...@@ -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 <ComboboxSearchInput211 <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";
10import { ChevronDownIcon, PlayIcon } from "lucide-react";10import { PlayIcon } from "lucide-react";
11import { useCallback, useEffect, useRef, useState } from "react";11import { useCallback, useEffect, useRef, useState } from "react";
1212
13import { cn } from "~/lib/utils";
14
15import { useClientSettings, useUpdateClientSettings } from "../../hooks/useSettings";13import { useClientSettings, useUpdateClientSettings } from "../../hooks/useSettings";
16import { getDesktopSnapShotBridge } from "../../lib/desktopSnapShot";14import { getDesktopSnapShotBridge } from "../../lib/desktopSnapShot";
17import {15import {
...@@ -44,7 +42,7 @@ import {...@@ -44,7 +42,7 @@ import {
44import { searchableSetting } from "./settingsSearch";42import { searchableSetting } from "./settingsSearch";
45import { Button } from "../ui/button";43import { Button } from "../ui/button";
46import { Menu, MenuItem, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "../ui/menu";44import { Menu, MenuItem, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "../ui/menu";
47import { selectTriggerVariants } from "../ui/select";45import { SelectButton } from "../ui/select";
48import { Switch } from "../ui/switch";46import { Switch } from "../ui/switch";
49import { toastManager } from "../ui/toast";47import { toastManager } from "../ui/toast";
50import { SnapShotSetupDialog } from "./SnapShotSetupDialog";48import { SnapShotSetupDialog } from "./SnapShotSetupDialog";
...@@ -497,21 +495,19 @@ export function SnapShotSettings() {...@@ -497,21 +495,19 @@ export function SnapShotSettings() {
497 <Menu>495 <Menu>
498 <MenuTrigger496 <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 <MenuRadioGroup513 <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}
6262
63// The *Variants function is for components/ui modules only.
63export { Badge, badgeVariants };64export { 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);
7373
74type ButtonVariant = NonNullable<VariantProps<typeof buttonVariants>["variant"]>;
75type ButtonSize = NonNullable<VariantProps<typeof buttonVariants>["size"]>;
76
74interface ButtonProps extends useRender.ComponentProps<"button"> {77interface 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}
7881
79function Button({ className, variant, size, render, ...props }: ButtonProps) {82function 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}
97100
98export { Button, buttonVariants };101// buttonVariants is exported for other components/ui modules only; app code
102// renders a Button (with `render` for other elements) instead.
103export { Button, buttonVariants, type ButtonSize, type ButtonVariant };
99104
100const inlineButtonVariants = cva(105const 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";
2import { mergeProps } from "@base-ui/react/merge-props";
2import { Select as SelectPrimitive } from "@base-ui/react/select";3import { Select as SelectPrimitive } from "@base-ui/react/select";
4import { useRender } from "@base-ui/react/use-render";
3import { cva, type VariantProps } from "class-variance-authority";5import { cva, type VariantProps } from "class-variance-authority";
4import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";6import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
5import type * as React from "react";7import type * as React from "react";
...@@ -57,6 +59,37 @@ function SelectTrigger({...@@ -57,6 +59,37 @@ function SelectTrigger({
57 );59 );
58}60}
5961
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 */
66function 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
60function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {93function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
61 return (94 return (
62 <SelectPrimitive.Value95 <SelectPrimitive.Value
...@@ -182,7 +215,7 @@ function SelectGroupLabel(props: SelectPrimitive.GroupLabel.Props) {...@@ -182,7 +215,7 @@ function SelectGroupLabel(props: SelectPrimitive.GroupLabel.Props) {
182export {215export {
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}
5454
55// The *Variants function is for components/ui modules only.
55export { Toggle, toggleVariants };56export { 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 the8// 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 you9// 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.
11export const RESTYLE_CEILING = 627;11export const RESTYLE_CEILING = 623;
1212
13const RULE = "shadcn(no-restyle)";13const RULE = "shadcn(no-restyle)";
1414
vite.config.ts+27
...@@ -16,6 +16,20 @@ const RESTRICTED_IMPORT_PATHS = [...@@ -16,6 +16,20 @@ const RESTRICTED_IMPORT_PATHS = [
16 },16 },
17];17];
1818
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 */
24const 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. */
20const RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS = {34const 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 picks191 // 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.