From 5579b5074b71e301f0b0c92eb6fca70290212ca6 Mon Sep 17 00:00:00 2001 From: Shak Date: Fri, 31 Jul 2026 03:28:37 +0330 Subject: [PATCH 1/4] feat(ui): add cn() helper with tailwind-merge config for custom utilities Co-Authored-By: Claude Opus 5 --- src/common/utils/cn.ts | 78 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/common/utils/cn.ts diff --git a/src/common/utils/cn.ts b/src/common/utils/cn.ts new file mode 100644 index 00000000..68abd2c5 --- /dev/null +++ b/src/common/utils/cn.ts @@ -0,0 +1,78 @@ +import { type ClassValue, clsx } from 'clsx' +import { extendTailwindMerge } from 'tailwind-merge' + +/** + * Class merging for the whole app. + * + * Division of labour: class-variance-authority declares the *variants* a UI + * component has; `cn()` does all the *merging*. cva's own `cx` is never used + * for merging — it cannot resolve Tailwind conflicts. + * + * --------------------------------------------------------------------------- + * Why the config below is mandatory, not an optimisation + * --------------------------------------------------------------------------- + * tailwind-merge knows nothing about the `@utility` classes we define in + * styles/semantic.css and styles/z-index.css, and gets them wrong three ways: + * + * 1. ACTIVELY DESTRUCTIVE — `bg-glass`. + * It sets `background-image` (the theme scrim) and `backdrop-filter`, never + * a background COLOUR. But `bg-` + anything matches tailwind-merge's + * background-colour validator (the colour theme is `isAny`), so it would be + * classified as one. 23 of its 27 usages are written `bg-content bg-glass`. + * Unconfigured, `bg-glass` (last) wins and `bg-content` is DELETED — every + * widget loses its surface fill on all six themes. Hence its own class + * group: a literal registration is matched before the validator fallback, + * which removes it from `bg-color` for good. + * + * 2. SILENTLY IGNORED — `rounded-widget`, `rounded-card`, `elevation*`, + * `transition-ui`, `focus-ring`, and the `z-*` layer names are unknown, so + * they never override the Tailwind class they replace. Both survive and CSS + * emission order decides instead of the caller — and custom `@utility` rules + * are emitted AFTER core utilities, so the primitive silently beats the call + * site that tried to override it. + * + * 3. ACCIDENTALLY CORRECT — `text-muted`, `bg-content`, `border-content` and + * friends resolve today only because the colour validator accepts anything. + * They are pinned below so a future tailwind-merge release cannot quietly + * un-resolve them. + * + * RULE: every `@utility` we define must be registered here. Adding one without + * a matching entry is a silent styling bug, not a compile error. + */ +const twMerge = extendTailwindMerge<'wg-backdrop'>({ + extend: { + classGroups: { + // bg-glass is a backdrop-filter, NOT a background colour. See (1). + 'wg-backdrop': ['bg-glass'], + + // Pinned — see (3). + 'bg-color': ['bg-widget', 'bg-content', 'bg-raised'], + 'text-color': ['text-strong', 'text-content', 'text-muted', 'text-subtle'], + 'border-color': ['border-content', 'border-strong'], + + // Genuinely unknown without this — see (2). + rounded: ['rounded-widget', 'rounded-card'], + shadow: ['elevation-sm', 'elevation', 'elevation-lg'], + transition: ['transition-ui'], + 'outline-style': ['focus-ring'], + z: [ + 'z-base', + 'z-raised', + 'z-sticky', + 'z-drag', + 'z-nav', + 'z-backdrop', + 'z-sheet', + 'z-modal', + 'z-popover', + 'z-tooltip', + 'z-toast', + 'z-pet', + ], + }, + }, +}) + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} From 3e05d33e9030e161099a2a358e73edf475ef9ad0 Mon Sep 17 00:00:00 2001 From: Shak Date: Thu, 13 Aug 2026 20:37:34 +0330 Subject: [PATCH 2/4] chore: ignore local .claude directory Local Claude Code session config shouldn't be tracked. --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f9b65d12..201c15df 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,5 @@ dev local_output diff.txt -docs/* \ No newline at end of file +docs/* +.claude \ No newline at end of file From 8f509c8f3e615c8076d86633f20865fe3cb7ef70 Mon Sep 17 00:00:00 2001 From: Shak Date: Thu, 13 Aug 2026 20:41:35 +0330 Subject: [PATCH 3/4] feat(ui): add cn() helper with tailwind-merge config for custom utilities --- src/common/utils/cn.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/common/utils/cn.ts diff --git a/src/common/utils/cn.ts b/src/common/utils/cn.ts new file mode 100644 index 00000000..d555533d --- /dev/null +++ b/src/common/utils/cn.ts @@ -0,0 +1,37 @@ +import { type ClassValue, clsx } from 'clsx' +import { extendTailwindMerge } from 'tailwind-merge' + +const twMerge = extendTailwindMerge<'wg-backdrop'>({ + extend: { + classGroups: { + 'wg-backdrop': ['bg-glass'], + + 'bg-color': ['bg-widget', 'bg-content', 'bg-raised'], + 'text-color': ['text-strong', 'text-content', 'text-muted', 'text-subtle'], + 'border-color': ['border-content', 'border-strong'], + + rounded: ['rounded-widget', 'rounded-card'], + shadow: ['elevation-sm', 'elevation', 'elevation-lg'], + transition: ['transition-ui'], + 'outline-style': ['focus-ring'], + z: [ + 'z-base', + 'z-raised', + 'z-sticky', + 'z-drag', + 'z-nav', + 'z-backdrop', + 'z-sheet', + 'z-modal', + 'z-popover', + 'z-tooltip', + 'z-toast', + 'z-pet', + ], + }, + }, +}) + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} From 462f1e55ca379bc4519f818cdb4cdaa02b384593 Mon Sep 17 00:00:00 2001 From: Shak Date: Thu, 13 Aug 2026 20:41:40 +0330 Subject: [PATCH 4/4] chore: ignore local .claude directory Local Claude Code session config shouldn't be tracked. --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f9b65d12..201c15df 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,5 @@ dev local_output diff.txt -docs/* \ No newline at end of file +docs/* +.claude \ No newline at end of file