| 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 | 1 | import { useEffect, useId, useState } from "react"; |
| 2 | import { Tabs } from "@base-ui/react/tabs"; | |
| 3 | 2 | import { create } from "zustand"; |
| 4 | 3 | import { |
| 5 | 4 | localSnoozeDate, |
| ... | ... | @@ -14,7 +13,7 @@ import { weekStartsOn } from "../timestampFormat"; |
| 14 | 13 | import { Popover, PopoverTrigger, PopoverPopup } from "./ui/popover"; |
| 15 | 14 | import { Input } from "./ui/input"; |
| 16 | 15 | import { Label } from "./ui/label"; |
| 17 | import { toggleVariants } from "./ui/toggle"; | |
| 16 | import { Toggle, ToggleGroup } from "./ui/toggle-group"; | |
| 18 | 17 | import { Select, SelectTrigger, SelectValue, SelectPopup, SelectItem } from "./ui/select"; |
| 19 | 18 | import { |
| 20 | 19 | NumberField, |
| ... | ... | @@ -95,34 +94,21 @@ function CustomSnoozeDialog() { |
| 95 | 94 | <DialogDescription>Choose when snoozed threads return to your inbox.</DialogDescription> |
| 96 | 95 | </DialogHeader> |
| 97 | 96 | <DialogPanel className="text-base sm:text-sm"> |
| 98 | <Tabs.Root | |
| 99 | value={mode} | |
| 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 | |
| 97 | <div className="flex flex-col gap-4"> | |
| 98 | <ToggleGroup | |
| 107 | 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) => ( | |
| 111 | <Tabs.Tab | |
| 112 | key={value} | |
| 113 | value={value} | |
| 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"> | |
| 108 | <Toggle value="date">Date and time</Toggle> | |
| 109 | <Toggle value="duration">Duration</Toggle> | |
| 110 | </ToggleGroup> | |
| 111 | <div className="flex flex-col gap-4"> | |
| 126 | 112 | {mode === "date" ? ( |
| 127 | 113 | <div className="grid grid-cols-1 gap-3 sm:grid-cols-2"> |
| 128 | 114 | <div className="flex min-w-0 flex-col gap-1.5"> |
| ... | ... | @@ -226,8 +212,8 @@ function CustomSnoozeDialog() { |
| 226 | 212 | </Label> |
| 227 | 213 | </div> |
| 228 | 214 | )} |
| 229 | </Tabs.Panel> | |
| 230 | </Tabs.Root> | |
| 215 | </div> | |
| 216 | </div> | |
| 231 | 217 | {error && ( |
| 232 | 218 | <p role="alert" className="text-destructive"> |
| 233 | 219 | {error} |
apps/web/src/components/GitActionsControl.tsx+8-19| ... | ... | @@ -48,7 +48,7 @@ import { |
| 48 | 48 | } from "~/components/Icons"; |
| 49 | 49 | import { RadioGroup } from "~/components/ui/radio-group"; |
| 50 | 50 | import { Spinner } from "~/components/ui/spinner"; |
| 51 | import { toggleVariants } from "~/components/ui/toggle"; | |
| 51 | import { Toggle, ToggleGroup } from "~/components/ui/toggle-group"; | |
| 52 | 52 | import { cn } from "~/lib/utils"; |
| 53 | 53 | import { |
| 54 | 54 | buildGitActionProgressStages, |
| ... | ... | @@ -815,10 +815,10 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { |
| 815 | 815 | > |
| 816 | 816 | Protocol |
| 817 | 817 | </span> |
| 818 | <RadioGroup | |
| 819 | className="w-fit flex-row gap-0.5 rounded-lg bg-input/40 p-0.5" | |
| 820 | value={publishProtocol} | |
| 821 | onValueChange={(protocol) => { | |
| 818 | <ToggleGroup | |
| 819 | value={[publishProtocol]} | |
| 820 | onValueChange={(next) => { | |
| 821 | const protocol = next[0]; | |
| 822 | 822 | if (protocol === "ssh" || protocol === "https") { |
| 823 | 823 | setPublishProtocol(protocol); |
| 824 | 824 | } |
| ... | ... | @@ -826,20 +826,9 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { |
| 826 | 826 | aria-labelledby="publish-protocol-label" |
| 827 | 827 | disabled={publishRepositoryAction.isPending} |
| 828 | 828 | > |
| 829 | {(["ssh", "https"] as const).map((protocol) => ( | |
| 830 | <RadioPrimitive.Root | |
| 831 | key={protocol} | |
| 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> | |
| 829 | <Toggle value="ssh">SSH</Toggle> | |
| 830 | <Toggle value="https">HTTPS</Toggle> | |
| 831 | </ToggleGroup> | |
| 843 | 832 | </div> |
| 844 | 833 | </div> |
| 845 | 834 | ) : null} |
apps/web/src/components/chat/ComposerBanner.tsx+10-10| ... | ... | @@ -4,7 +4,7 @@ import { ChevronDownIcon, XIcon } from "lucide-react"; |
| 4 | 4 | import type { ComponentProps } from "react"; |
| 5 | 5 | |
| 6 | 6 | import { cn } from "~/lib/utils"; |
| 7 | import { Button, buttonVariants } from "../ui/button"; | |
| 7 | import { Button } from "../ui/button"; | |
| 8 | 8 | import { ScrollArea } from "../ui/scroll-area"; |
| 9 | 9 | |
| 10 | 10 | export type ComposerBannerVariant = "default" | "error" | "info" | "success" | "warning"; |
| ... | ... | @@ -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 | 328 | return ( |
| 328 | <span | |
| 329 | aria-hidden | |
| 330 | className={cn( | |
| 331 | buttonVariants({ size: "icon-xs", variant: "ghost" }), | |
| 332 | "pointer-events-none", | |
| 333 | className, | |
| 334 | )} | |
| 329 | <Button | |
| 330 | render={<span aria-hidden />} | |
| 331 | size="icon-xs" | |
| 332 | variant="ghost" | |
| 333 | tabIndex={-1} | |
| 334 | className="pointer-events-none" | |
| 335 | 335 | > |
| 336 | 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 | 5 | type ResolvedKeybindingsConfig, |
| 6 | 6 | } from "@t3tools/contracts"; |
| 7 | 7 | import { memo, useEffect, useMemo, useState } from "react"; |
| 8 | import type { VariantProps } from "class-variance-authority"; | |
| 9 | 8 | import { Badge } from "../ui/badge"; |
| 10 | import { buttonVariants } from "../ui/button"; | |
| 9 | import type { ButtonVariant } from "../ui/button"; | |
| 11 | 10 | import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; |
| 12 | 11 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 13 | 12 | import { cn } from "~/lib/utils"; |
| ... | ... | @@ -49,7 +48,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { |
| 49 | 48 | disabled?: boolean; |
| 50 | 49 | terminalOpen?: boolean; |
| 51 | 50 | open?: boolean; |
| 52 | triggerVariant?: VariantProps<typeof buttonVariants>["variant"]; | |
| 51 | triggerVariant?: ButtonVariant; | |
| 53 | 52 | triggerClassName?: string; |
| 54 | 53 | /** Aggregate settings can show a neutral value without claiming one provider is selected. */ |
| 55 | 54 | triggerLabel?: string; |
apps/web/src/components/chat/TraitsPicker.tsx+2-3| ... | ... | @@ -16,9 +16,8 @@ import { |
| 16 | 16 | normalizeModelSlug, |
| 17 | 17 | } from "@t3tools/shared/model"; |
| 18 | 18 | import { memo, useCallback } from "react"; |
| 19 | import type { VariantProps } from "class-variance-authority"; | |
| 20 | 19 | import { GaugeIcon, ZapIcon } from "lucide-react"; |
| 21 | import { buttonVariants } from "../ui/button"; | |
| 20 | import type { ButtonVariant } from "../ui/button"; | |
| 22 | 21 | import { |
| 23 | 22 | Menu, |
| 24 | 23 | MenuGroup, |
| ... | ... | @@ -277,7 +276,7 @@ export interface TraitsMenuContentProps { |
| 277 | 276 | modelOptions?: ProviderOptions | null | undefined; |
| 278 | 277 | allowPromptInjectedEffort?: boolean; |
| 279 | 278 | planModeEnabled: boolean; |
| 280 | triggerVariant?: VariantProps<typeof buttonVariants>["variant"]; | |
| 279 | triggerVariant?: ButtonVariant; | |
| 281 | 280 | triggerClassName?: string; |
| 282 | 281 | isComposerOwned?: boolean; |
| 283 | 282 | } |
apps/web/src/components/chat/composerProviderState.tsx+2-3| ... | ... | @@ -13,10 +13,9 @@ import { |
| 13 | 13 | isClaudeUltrathinkPrompt, |
| 14 | 14 | normalizeModelSlug, |
| 15 | 15 | } from "@t3tools/shared/model"; |
| 16 | import type { VariantProps } from "class-variance-authority"; | |
| 17 | 16 | import type { ReactNode } from "react"; |
| 18 | 17 | |
| 19 | import type { buttonVariants } from "../ui/button"; | |
| 18 | import type { ButtonVariant } from "../ui/button"; | |
| 20 | 19 | import type { DraftId } from "../../composerDraftStore"; |
| 21 | 20 | import { getProviderModelCapabilities } from "../../providerModels"; |
| 22 | 21 | import type { ComposerControlSize } from "./ComposerControl"; |
| ... | ... | @@ -55,7 +54,7 @@ type TraitsRenderInput = { |
| 55 | 54 | planModeEnabled: boolean; |
| 56 | 55 | size?: ComposerControlSize; |
| 57 | 56 | hidden?: boolean; |
| 58 | triggerVariant?: VariantProps<typeof buttonVariants>["variant"]; | |
| 57 | triggerVariant?: ButtonVariant; | |
| 59 | 58 | triggerClassName?: string; |
| 60 | 59 | isComposerOwned?: boolean; |
| 61 | 60 | }; |
apps/web/src/components/settings/FontFamilyPicker.tsx+4-7| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | import { LegendList, type LegendListRef } from "@legendapp/list/react"; |
| 2 | import { CheckIcon, ChevronDownIcon } from "lucide-react"; | |
| 2 | import { CheckIcon } from "lucide-react"; | |
| 3 | 3 | import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; |
| 4 | 4 | import { isMonospaceFamily, queryInstalledFontFamilies } from "../../appearanceFonts"; |
| 5 | 5 | import { |
| ... | ... | @@ -11,7 +11,7 @@ import { |
| 11 | 11 | ComboboxPopup, |
| 12 | 12 | ComboboxTrigger, |
| 13 | 13 | } from "../ui/combobox"; |
| 14 | import { selectTriggerVariants } from "../ui/select"; | |
| 14 | import { SelectButton } from "../ui/select"; | |
| 15 | 15 | |
| 16 | 16 | const DEFAULT_FONT_VALUE = "__default__"; |
| 17 | 17 | |
| ... | ... | @@ -204,11 +204,8 @@ export function FontFamilyPicker({ |
| 204 | 204 | void listRef.current?.scrollIndexIntoView?.({ index: eventDetails.index, animated: false }); |
| 205 | 205 | }} |
| 206 | 206 | > |
| 207 | <ComboboxTrigger aria-label={ariaLabel} className={selectTriggerVariants({ size: "sm" })}> | |
| 208 | <span className="min-w-0 truncate"> | |
| 209 | {selectedFamily.length === 0 ? defaultFamily : selectedFamily} | |
| 210 | </span> | |
| 211 | <ChevronDownIcon className="-me-1 size-3 opacity-50" /> | |
| 207 | <ComboboxTrigger aria-label={ariaLabel} render={<SelectButton size="sm" />}> | |
| 208 | {selectedFamily.length === 0 ? defaultFamily : selectedFamily} | |
| 212 | 209 | </ComboboxTrigger> |
| 213 | 210 | <ComboboxPopup align="end" className="flex w-72 flex-col"> |
| 214 | 211 | <ComboboxSearchInput |
apps/web/src/components/settings/SnapShotSettings.tsx+13-17| ... | ... | @@ -7,11 +7,9 @@ import { |
| 7 | 7 | type DesktopSnapShotSetupAction, |
| 8 | 8 | type SnapShotShortcut, |
| 9 | 9 | } from "@t3tools/contracts"; |
| 10 | import { ChevronDownIcon, PlayIcon } from "lucide-react"; | |
| 10 | import { PlayIcon } from "lucide-react"; | |
| 11 | 11 | import { useCallback, useEffect, useRef, useState } from "react"; |
| 12 | 12 | |
| 13 | import { cn } from "~/lib/utils"; | |
| 14 | ||
| 15 | 13 | import { useClientSettings, useUpdateClientSettings } from "../../hooks/useSettings"; |
| 16 | 14 | import { getDesktopSnapShotBridge } from "../../lib/desktopSnapShot"; |
| 17 | 15 | import { |
| ... | ... | @@ -44,7 +42,7 @@ import { |
| 44 | 42 | import { searchableSetting } from "./settingsSearch"; |
| 45 | 43 | import { Button } from "../ui/button"; |
| 46 | 44 | import { Menu, MenuItem, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "../ui/menu"; |
| 47 | import { selectTriggerVariants } from "../ui/select"; | |
| 45 | import { SelectButton } from "../ui/select"; | |
| 48 | 46 | import { Switch } from "../ui/switch"; |
| 49 | 47 | import { toastManager } from "../ui/toast"; |
| 50 | 48 | import { SnapShotSetupDialog } from "./SnapShotSetupDialog"; |
| ... | ... | @@ -497,21 +495,19 @@ export function SnapShotSettings() { |
| 497 | 495 | <Menu> |
| 498 | 496 | <MenuTrigger |
| 499 | 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 | 500 | disabled={!captureAvailable} |
| 502 | 501 | > |
| 503 | <span className="min-w-0 flex-1 truncate text-left"> | |
| 504 | {soundSelection === "off" ? ( | |
| 505 | "Off" | |
| 506 | ) : soundSelection === "soft-pop" ? ( | |
| 507 | <> | |
| 508 | Whoosh <span className="text-muted-foreground">(Default)</span> | |
| 509 | </> | |
| 510 | ) : ( | |
| 511 | "Click" | |
| 512 | )} | |
| 513 | </span> | |
| 514 | <ChevronDownIcon className="-me-1 size-3 shrink-0 opacity-50" /> | |
| 502 | {soundSelection === "off" ? ( | |
| 503 | "Off" | |
| 504 | ) : soundSelection === "soft-pop" ? ( | |
| 505 | <> | |
| 506 | Whoosh <span className="text-muted-foreground">(Default)</span> | |
| 507 | </> | |
| 508 | ) : ( | |
| 509 | "Click" | |
| 510 | )} | |
| 515 | 511 | </MenuTrigger> |
| 516 | 512 | <MenuPopup align="end"> |
| 517 | 513 | <MenuRadioGroup |
apps/web/src/components/ui/badge.tsx+1| ... | ... | @@ -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 | 64 | export { Badge, badgeVariants }; |
apps/web/src/components/ui/button.tsx+8-3| ... | ... | @@ -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 | 77 | interface ButtonProps extends useRender.ComponentProps<"button"> { |
| 75 | variant?: VariantProps<typeof buttonVariants>["variant"]; | |
| 76 | size?: VariantProps<typeof buttonVariants>["size"]; | |
| 78 | variant?: ButtonVariant; | |
| 79 | size?: ButtonSize; | |
| 77 | 80 | } |
| 78 | 81 | |
| 79 | 82 | 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 | 105 | const inlineButtonVariants = cva( |
| 101 | 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 | 1 | "use client"; |
| 2 | import { mergeProps } from "@base-ui/react/merge-props"; | |
| 2 | 3 | import { Select as SelectPrimitive } from "@base-ui/react/select"; |
| 4 | import { useRender } from "@base-ui/react/use-render"; | |
| 3 | 5 | import { cva, type VariantProps } from "class-variance-authority"; |
| 4 | 6 | import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; |
| 5 | 7 | import type * as React from "react"; |
| ... | ... | @@ -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 | 93 | function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { |
| 61 | 94 | return ( |
| 62 | 95 | <SelectPrimitive.Value |
| ... | ... | @@ -182,7 +215,7 @@ function SelectGroupLabel(props: SelectPrimitive.GroupLabel.Props) { |
| 182 | 215 | export { |
| 183 | 216 | Select, |
| 184 | 217 | SelectTrigger, |
| 185 | selectTriggerVariants, | |
| 218 | SelectButton, | |
| 186 | 219 | SelectValue, |
| 187 | 220 | SelectPopup, |
| 188 | 221 | SelectPopup as SelectContent, |
apps/web/src/components/ui/toggle.tsx+1| ... | ... | @@ -52,4 +52,5 @@ function Toggle({ |
| 52 | 52 | ); |
| 53 | 53 | } |
| 54 | 54 | |
| 55 | // The *Variants function is for components/ui modules only. | |
| 55 | 56 | export { Toggle, toggleVariants }; |
scripts/lint-restyle-ceiling.ts+1-1| ... | ... | @@ -8,7 +8,7 @@ import * as NodeURL from "node:url"; |
| 8 | 8 | // components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the |
| 9 | 9 | // count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you |
| 10 | 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 | 13 | const RULE = "shadcn(no-restyle)"; |
| 14 | 14 |
vite.config.ts+27| ... | ... | @@ -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 | 33 | /** Lucide's pull-request glyphs, which only `pullRequestIcons.tsx` may name. */ |
| 20 | 34 | const RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS = { |
| 21 | 35 | name: "lucide-react", |
| ... | ... | @@ -160,6 +174,19 @@ export default defineConfig({ |
| 160 | 174 | files: ["packages/shared/src/hostProcess.ts"], |
| 161 | 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 | 191 | // The one module allowed to name lucide's pull-request glyphs; everything else picks |
| 165 | 192 | // from its vocabulary. The other import restrictions still apply here. |