diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 39f01d5..49bc4a3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,6 +14,8 @@ jobs:
- run: bun install --frozen-lockfile
- run: bun run theme:check
- run: bun run tokens:check
+ - name: cn keeps the type scale next to a text color
+ run: bun -e 'import { cn } from "./registry/winlab/lib/utils"; const out = cn("text-body text-title", "text-foreground"); if (out !== "text-title text-foreground") throw new Error(out)'
- run: bun run lint
- run: bun run typecheck
- run: bun run format:check
@@ -98,6 +100,8 @@ jobs:
grep -q 'layout === "wide"' components/app-shell.tsx
bunx shadcn@latest add http://localhost:8099/r/confirm-dialog.json -y -o
grep -q 'useTransition' components/confirm-dialog.tsx
+ grep -q 'createCn' lib/utils.ts
+ grep -q 'from "@/lib/utils"' components/ui/button.tsx
bunx shadcn@latest add http://localhost:8099/r/form-dialog.json -y -o
grep -q 'fieldset' components/form-dialog.tsx
bunx shadcn@latest add http://localhost:8099/r/empty-state.json -y -o
diff --git a/lib/utils.ts b/lib/utils.ts
index f815ded..ae4c7c5 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -1 +1,2 @@
-export { cn } from "cn"
+// next/font and other generated code import cn from "@/lib/utils".
+export { cn } from "@/registry/winlab/lib/utils"
diff --git a/registry.json b/registry.json
index 8cfb6e5..6229f91 100644
--- a/registry.json
+++ b/registry.json
@@ -187,7 +187,7 @@
"name": "utils",
"type": "registry:lib",
"title": "Utils",
- "description": "cn re-exported at @/lib/utils.",
+ "description": "cn at @/lib/utils, aware of the text-title / text-body sizes.",
"dependencies": [
"cn"
],
@@ -225,6 +225,9 @@
"path": "registry/winlab/ui/button.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -240,6 +243,9 @@
"path": "registry/winlab/ui/label.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -256,6 +262,9 @@
"path": "registry/winlab/ui/input.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -271,6 +280,9 @@
"path": "registry/winlab/ui/textarea.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -286,6 +298,9 @@
"path": "registry/winlab/ui/skeleton.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -303,6 +318,9 @@
"path": "registry/winlab/ui/badge.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -317,6 +335,7 @@
"lucide-react"
],
"registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json",
"https://ui.winlab.tw/r/button.json"
],
"files": [
@@ -336,6 +355,7 @@
"cn"
],
"registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json",
"https://ui.winlab.tw/r/button.json"
],
"files": [
@@ -362,6 +382,7 @@
}
],
"registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json",
"https://ui.winlab.tw/r/field.json"
]
},
@@ -378,6 +399,9 @@
"path": "registry/winlab/ui/table.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -394,6 +418,9 @@
"path": "registry/winlab/ui/popover.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -411,6 +438,9 @@
"path": "registry/winlab/ui/command.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -428,6 +458,9 @@
"path": "registry/winlab/ui/dropdown-menu.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -445,6 +478,9 @@
"path": "registry/winlab/ui/checkbox.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -461,6 +497,9 @@
"path": "registry/winlab/ui/switch.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -477,6 +516,9 @@
"path": "registry/winlab/ui/tabs.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -493,6 +535,9 @@
"path": "registry/winlab/ui/avatar.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -509,6 +554,9 @@
"path": "registry/winlab/ui/tooltip.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -525,6 +573,9 @@
"path": "registry/winlab/ui/separator.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -541,6 +592,9 @@
"path": "registry/winlab/ui/collapsible.tsx",
"type": "registry:ui"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
},
{
@@ -569,6 +623,7 @@
"lucide-react"
],
"registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json",
"https://ui.winlab.tw/r/field.json",
"https://ui.winlab.tw/r/popover.json",
"https://ui.winlab.tw/r/command.json"
@@ -591,6 +646,7 @@
"react-day-picker"
],
"registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json",
"https://ui.winlab.tw/r/button.json"
],
"files": [
@@ -609,6 +665,7 @@
"cn"
],
"registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json",
"https://ui.winlab.tw/r/avatar.json",
"https://ui.winlab.tw/r/tooltip.json"
],
@@ -756,6 +813,7 @@
"react-day-picker"
],
"registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json",
"https://ui.winlab.tw/r/field.json",
"https://ui.winlab.tw/r/calendar.json",
"https://ui.winlab.tw/r/popover.json"
@@ -780,6 +838,9 @@
"path": "registry/winlab/blocks/action-panel/action-panel.tsx",
"type": "registry:component"
}
+ ],
+ "registryDependencies": [
+ "https://ui.winlab.tw/r/utils.json"
]
}
]
diff --git a/registry/winlab/blocks/action-panel/action-panel.tsx b/registry/winlab/blocks/action-panel/action-panel.tsx
index bd84a5f..1ead3fa 100644
--- a/registry/winlab/blocks/action-panel/action-panel.tsx
+++ b/registry/winlab/blocks/action-panel/action-panel.tsx
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
type ActionPanelProps = {
/** The verb: "開門". */
diff --git a/registry/winlab/blocks/app-shell/app-shell.tsx b/registry/winlab/blocks/app-shell/app-shell.tsx
index b3e9fe6..3aff6f3 100644
--- a/registry/winlab/blocks/app-shell/app-shell.tsx
+++ b/registry/winlab/blocks/app-shell/app-shell.tsx
@@ -3,7 +3,7 @@
import * as React from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
import {
Avatar,
diff --git a/registry/winlab/blocks/date-picker/date-picker.tsx b/registry/winlab/blocks/date-picker/date-picker.tsx
index 7a3cf56..5ced735 100644
--- a/registry/winlab/blocks/date-picker/date-picker.tsx
+++ b/registry/winlab/blocks/date-picker/date-picker.tsx
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
import { CalendarIcon } from "lucide-react"
import type { Matcher } from "react-day-picker"
diff --git a/registry/winlab/lib/utils.ts b/registry/winlab/lib/utils.ts
index 57f20b5..f72cf76 100644
--- a/registry/winlab/lib/utils.ts
+++ b/registry/winlab/lib/utils.ts
@@ -1,3 +1,8 @@
-// next/font and other generated code import cn from "@/lib/utils"; WinLab
-// components import it from the cn package directly.
-export { cn } from "cn"
+import { createCn } from "cn/config"
+
+// cn that knows the WinLab type scale. Stock tailwind-merge rules read
+// text-title and text-body as colors, so cn("text-body", "text-foreground")
+// would drop the size; registering them as font sizes keeps both.
+export const cn = createCn({
+ extend: { classGroups: { "font-size": [{ text: ["title", "body"] }] } },
+})
diff --git a/registry/winlab/ui/alert-dialog.tsx b/registry/winlab/ui/alert-dialog.tsx
index 50104d5..4e1ecec 100644
--- a/registry/winlab/ui/alert-dialog.tsx
+++ b/registry/winlab/ui/alert-dialog.tsx
@@ -2,7 +2,7 @@
import * as React from "react"
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
import { Button } from "@/registry/winlab/ui/button"
diff --git a/registry/winlab/ui/avatar.tsx b/registry/winlab/ui/avatar.tsx
index 00aed4a..99a84d0 100644
--- a/registry/winlab/ui/avatar.tsx
+++ b/registry/winlab/ui/avatar.tsx
@@ -1,7 +1,7 @@
"use client"
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
// One size, 40px: the height of a control row.
function Avatar({ className, ...props }: AvatarPrimitive.Root.Props) {
diff --git a/registry/winlab/ui/badge.tsx b/registry/winlab/ui/badge.tsx
index 8fc1648..4305c3e 100644
--- a/registry/winlab/ui/badge.tsx
+++ b/registry/winlab/ui/badge.tsx
@@ -1,7 +1,7 @@
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
// Status variants share one shape: a tinted background with text in the same color.
const badgeVariants = cva(
diff --git a/registry/winlab/ui/button.tsx b/registry/winlab/ui/button.tsx
index 8a8df47..056154b 100644
--- a/registry/winlab/ui/button.tsx
+++ b/registry/winlab/ui/button.tsx
@@ -1,6 +1,6 @@
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
// One height for text and icon buttons; variants follow what WinLab apps use.
const buttonVariants = cva(
diff --git a/registry/winlab/ui/calendar.tsx b/registry/winlab/ui/calendar.tsx
index 06c33f5..bee6299 100644
--- a/registry/winlab/ui/calendar.tsx
+++ b/registry/winlab/ui/calendar.tsx
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
import { DayPicker, type DayButton } from "react-day-picker"
import { zhTW } from "react-day-picker/locale"
diff --git a/registry/winlab/ui/checkbox.tsx b/registry/winlab/ui/checkbox.tsx
index ce22ab5..44b6c66 100644
--- a/registry/winlab/ui/checkbox.tsx
+++ b/registry/winlab/ui/checkbox.tsx
@@ -1,7 +1,7 @@
"use client"
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
import { CheckIcon } from "lucide-react"
// Round, 20px, with a larger invisible hit area. A future radio shows a dot,
diff --git a/registry/winlab/ui/collapsible.tsx b/registry/winlab/ui/collapsible.tsx
index 51f03fd..0e94d07 100644
--- a/registry/winlab/ui/collapsible.tsx
+++ b/registry/winlab/ui/collapsible.tsx
@@ -1,7 +1,7 @@
"use client"
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
return
diff --git a/registry/winlab/ui/combobox.tsx b/registry/winlab/ui/combobox.tsx
index 669aa2c..c1160de 100644
--- a/registry/winlab/ui/combobox.tsx
+++ b/registry/winlab/ui/combobox.tsx
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
import { ChevronDownIcon } from "lucide-react"
import { fieldTriggerClassName } from "@/registry/winlab/lib/field"
diff --git a/registry/winlab/ui/command.tsx b/registry/winlab/ui/command.tsx
index cfc0025..815a4df 100644
--- a/registry/winlab/ui/command.tsx
+++ b/registry/winlab/ui/command.tsx
@@ -2,7 +2,7 @@
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
import { CheckIcon, SearchIcon } from "lucide-react"
// Searchable list for a popover (combobox). It has no surface of its own:
diff --git a/registry/winlab/ui/dialog.tsx b/registry/winlab/ui/dialog.tsx
index e7627d3..bd25301 100644
--- a/registry/winlab/ui/dialog.tsx
+++ b/registry/winlab/ui/dialog.tsx
@@ -3,7 +3,7 @@
import * as React from "react"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
import { XIcon } from "lucide-react"
import { Button } from "@/registry/winlab/ui/button"
diff --git a/registry/winlab/ui/dropdown-menu.tsx b/registry/winlab/ui/dropdown-menu.tsx
index 7cd59e7..0efd9ee 100644
--- a/registry/winlab/ui/dropdown-menu.tsx
+++ b/registry/winlab/ui/dropdown-menu.tsx
@@ -2,7 +2,7 @@
import * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
import { ChevronRightIcon } from "lucide-react"
const menuSurface =
diff --git a/registry/winlab/ui/input.tsx b/registry/winlab/ui/input.tsx
index c4e539a..c79b948 100644
--- a/registry/winlab/ui/input.tsx
+++ b/registry/winlab/ui/input.tsx
@@ -1,6 +1,6 @@
import * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
// Same height and radius as Button so they line up in a row.
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
diff --git a/registry/winlab/ui/label.tsx b/registry/winlab/ui/label.tsx
index f6fa313..193a2cf 100644
--- a/registry/winlab/ui/label.tsx
+++ b/registry/winlab/ui/label.tsx
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
function Label({ className, ...props }: React.ComponentProps<"label">) {
return (
diff --git a/registry/winlab/ui/popover.tsx b/registry/winlab/ui/popover.tsx
index c88ef00..c4d3bc7 100644
--- a/registry/winlab/ui/popover.tsx
+++ b/registry/winlab/ui/popover.tsx
@@ -2,7 +2,7 @@
import * as React from "react"
import { Popover as PopoverPrimitive } from "@base-ui/react/popover"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
function Popover({ ...props }: PopoverPrimitive.Root.Props) {
return
diff --git a/registry/winlab/ui/select.tsx b/registry/winlab/ui/select.tsx
index a91415f..954ece3 100644
--- a/registry/winlab/ui/select.tsx
+++ b/registry/winlab/ui/select.tsx
@@ -2,7 +2,7 @@
import * as React from "react"
import { Select as SelectPrimitive } from "@base-ui/react/select"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { fieldTriggerClassName } from "@/registry/winlab/lib/field"
diff --git a/registry/winlab/ui/separator.tsx b/registry/winlab/ui/separator.tsx
index d69c134..c8b3a62 100644
--- a/registry/winlab/ui/separator.tsx
+++ b/registry/winlab/ui/separator.tsx
@@ -1,7 +1,7 @@
"use client"
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
function Separator({
className,
diff --git a/registry/winlab/ui/skeleton.tsx b/registry/winlab/ui/skeleton.tsx
index c4e5f00..575f96b 100644
--- a/registry/winlab/ui/skeleton.tsx
+++ b/registry/winlab/ui/skeleton.tsx
@@ -1,4 +1,4 @@
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
diff --git a/registry/winlab/ui/switch.tsx b/registry/winlab/ui/switch.tsx
index 21f1851..687e48d 100644
--- a/registry/winlab/ui/switch.tsx
+++ b/registry/winlab/ui/switch.tsx
@@ -1,7 +1,7 @@
"use client"
import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
// One size: a 40 x 24 track with 2px padding, so the 20px thumb travels 16px.
function Switch({ className, ...props }: SwitchPrimitive.Root.Props) {
diff --git a/registry/winlab/ui/table.tsx b/registry/winlab/ui/table.tsx
index 128f273..8ba9939 100644
--- a/registry/winlab/ui/table.tsx
+++ b/registry/winlab/ui/table.tsx
@@ -1,5 +1,5 @@
import * as React from "react"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
// Header text is muted so rows read first; put tabular-nums on number columns.
// The outer columns have no side padding so text lines up with the page edge.
diff --git a/registry/winlab/ui/tabs.tsx b/registry/winlab/ui/tabs.tsx
index d428832..a7ff6a3 100644
--- a/registry/winlab/ui/tabs.tsx
+++ b/registry/winlab/ui/tabs.tsx
@@ -1,7 +1,7 @@
"use client"
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
function Tabs({ className, ...props }: TabsPrimitive.Root.Props) {
return (
diff --git a/registry/winlab/ui/textarea.tsx b/registry/winlab/ui/textarea.tsx
index 7c9d764..d634020 100644
--- a/registry/winlab/ui/textarea.tsx
+++ b/registry/winlab/ui/textarea.tsx
@@ -1,5 +1,5 @@
import * as React from "react"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
diff --git a/registry/winlab/ui/tooltip.tsx b/registry/winlab/ui/tooltip.tsx
index 9777bf7..500bd01 100644
--- a/registry/winlab/ui/tooltip.tsx
+++ b/registry/winlab/ui/tooltip.tsx
@@ -1,7 +1,7 @@
"use client"
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
-import { cn } from "cn"
+import { cn } from "@/registry/winlab/lib/utils"
function TooltipProvider({
delay = 0,