@latte-macchiat-io/latte-vanilla-components 0.0.339 → 0.0.341

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -295,6 +295,76 @@ const customButton = style({
295
295
  });
296
296
  ```
297
297
 
298
+ ### Overriding Component Styles
299
+
300
+ All components accept a `className` prop for customization. However, due to CSS cascade order in bundled applications (Next.js, Webpack, etc.), you need to use proper specificity to ensure your styles override library defaults.
301
+
302
+ #### Recommended: Use `styleOverride` Utility
303
+
304
+ ```tsx
305
+ import { Button, styleOverride } from '@latte-macchiat-io/latte-vanilla-components';
306
+
307
+ const customButton = styleOverride({
308
+ textTransform: 'uppercase',
309
+ letterSpacing: '0.1em',
310
+ backgroundColor: '#FF0000',
311
+ padding: '20px 40px',
312
+ });
313
+
314
+ <Button variant="primary" className={customButton}>
315
+ Custom Styled Button
316
+ </Button>
317
+ ```
318
+
319
+ **Why use `styleOverride`?** In production builds, CSS files are split into multiple chunks (`layout.css`, `page.css`, etc.). This can cause library styles to override your customizations even when your className appears last in the HTML. The `styleOverride` utility uses CSS attribute selectors to increase specificity from `(0,1,0)` to `(0,2,0)`, ensuring your styles always win.
320
+
321
+ #### Manual Override (Advanced)
322
+
323
+ If you prefer manual control:
324
+
325
+ ```tsx
326
+ import { style } from '@vanilla-extract/css';
327
+
328
+ const customButton = style({
329
+ selectors: {
330
+ '&[class]': {
331
+ // Your styles here - guaranteed to override library styles
332
+ backgroundColor: '#FF0000',
333
+ border: '2px solid #000',
334
+ },
335
+ },
336
+ });
337
+ ```
338
+
339
+ #### Combining Class Names
340
+
341
+ Use the exported `cn` utility for conditional class names:
342
+
343
+ ```tsx
344
+ import { cn, styleOverride } from '@latte-macchiat-io/latte-vanilla-components';
345
+
346
+ const baseButton = styleOverride({ padding: '20px' });
347
+ const activeButton = styleOverride({ backgroundColor: 'red' });
348
+
349
+ <Button className={cn(baseButton, isActive && activeButton)}>
350
+ Conditional Styles
351
+ </Button>
352
+ ```
353
+
354
+ #### Troubleshooting Style Overrides
355
+
356
+ **Problem:** Custom styles aren't applying
357
+
358
+ **Solutions:**
359
+ 1. Use `styleOverride` utility instead of plain `style()`
360
+ 2. Check browser DevTools to see which CSS rule is winning
361
+ 3. Verify your className appears in the rendered HTML
362
+ 4. Clear build cache: `rm -rf .next && npm run dev`
363
+
364
+ **Problem:** Styles work in dev but not production
365
+
366
+ **Solution:** This is a CSS chunking issue. Using `styleOverride` ensures proper specificity regardless of load order.
367
+
298
368
  ## 🎯 Performance
299
369
 
300
370
  - **Zero Runtime CSS-in-JS** - All styles compiled at build time
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latte-macchiat-io/latte-vanilla-components",
3
- "version": "0.0.339",
3
+ "version": "0.0.341",
4
4
  "description": "Beautiful components for amazing projects, with a touch of Vanilla 🥤",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -9,6 +9,7 @@
9
9
  "exports": {
10
10
  ".": "./src/index.ts",
11
11
  "./*.css.ts": "./src/*.css.ts",
12
+ "./utils/*": "./src/utils/*.ts",
12
13
  "./src/*": "./src/*"
13
14
  },
14
15
  "files": [
@@ -44,7 +45,6 @@
44
45
  "@vanilla-extract/recipes": "^0.5.7",
45
46
  "@vanilla-extract/sprinkles": "^1.6.5",
46
47
  "@vanilla-extract/vite-plugin": "^5.1.1",
47
- "clsx": "^2.1.1",
48
48
  "eslint": "^9.15.0",
49
49
  "eslint-config-prettier": "^10.0.1",
50
50
  "eslint-import-resolver-typescript": "^4.4.4",
@@ -1,6 +1,6 @@
1
- import { clsx } from 'clsx';
2
-
3
1
  import { actionsRecipe, type ActionsVariants } from './styles.css';
2
+ import { cn } from '../../utils/styleOverride';
3
+
4
4
 
5
5
  export type ActionsProps = React.HTMLAttributes<HTMLDivElement> &
6
6
  ActionsVariants & {
@@ -8,5 +8,5 @@ export type ActionsProps = React.HTMLAttributes<HTMLDivElement> &
8
8
  };
9
9
 
10
10
  export const Actions = ({ align, direction, className, children }: ActionsProps) => (
11
- <div className={clsx(actionsRecipe({ align, direction }), className)}>{children}</div>
11
+ <div className={cn(actionsRecipe({ align, direction }), className)}>{children}</div>
12
12
  );
@@ -1,7 +1,7 @@
1
- import { clsx } from 'clsx';
2
1
 
3
2
  import { buttonRecipe, type ButtonVariants } from './styles.css';
4
3
  import { AllowedButton } from './types';
4
+ import { cn } from '../../utils/styleOverride';
5
5
 
6
6
  export type ButtonProps = React.HTMLAttributes<HTMLButtonElement | HTMLAnchorElement> &
7
7
  ButtonVariants & {
@@ -30,14 +30,14 @@ export const Button = ({
30
30
  }: ButtonProps) => {
31
31
  if (as === 'a') {
32
32
  return (
33
- <a href={href} className={clsx(buttonRecipe({ variant, style, size, fullWidth }), className)}>
33
+ <a href={href} className={cn(buttonRecipe({ variant, style, size, fullWidth }), className)}>
34
34
  {children}
35
35
  </a>
36
36
  );
37
37
  }
38
38
 
39
39
  return (
40
- <button onClick={onClick} disabled={isDisabled || isPending} className={clsx(buttonRecipe({ variant, style, size, fullWidth }), className)}>
40
+ <button onClick={onClick} disabled={isDisabled || isPending} className={cn(buttonRecipe({ variant, style, size, fullWidth }), className)}>
41
41
  {isPending ? translation.isPendingLabel : children}
42
42
  </button>
43
43
  );
@@ -1,6 +1,5 @@
1
1
  'use client';
2
2
 
3
- import { clsx } from 'clsx';
4
3
  import { ReactNode, useEffect, useRef, useState } from 'react';
5
4
 
6
5
  import {
@@ -20,6 +19,7 @@ import {
20
19
 
21
20
  import { breakpoints } from '../../styles/mediaqueries';
22
21
  import { getResponsiveValue, parseResponsiveNumber } from '../../utils/getResponsiveValue';
22
+ import { cn } from '../../utils/styleOverride';
23
23
  import { useWindowSize } from '../../utils/useWindowSize';
24
24
 
25
25
  import { Icon } from '../Icon';
@@ -172,7 +172,7 @@ export const Carousel = ({
172
172
  const maxIndex = Math.max(0, data.length - visibleItems);
173
173
 
174
174
  return (
175
- <div className={clsx(carouselRecipe({ isFullWidth }), className)}>
175
+ <div className={cn(carouselRecipe({ isFullWidth }), className)}>
176
176
  <div ref={carouselRef} className={carouselContent({ overflow })}>
177
177
  <div
178
178
  ref={slideRef}
@@ -208,7 +208,7 @@ export const Carousel = ({
208
208
  type="button"
209
209
  aria-label={`Go to slide ${index + 1}`}
210
210
  onClick={() => handleBulletClick(index)}
211
- className={clsx(carouselBullet, index === currentIndex && carouselBulletActive)}
211
+ className={cn(carouselBullet, index === currentIndex && carouselBulletActive)}
212
212
  />
213
213
  ))}
214
214
  </div>
@@ -1,6 +1,6 @@
1
- import { clsx } from 'clsx';
2
-
3
1
  import { columnsRecipe, ColumnsVariants } from './styles.css';
2
+ import { cn } from '../../utils/styleOverride';
3
+
4
4
 
5
5
  export type ColumnsProps = React.HTMLAttributes<HTMLDivElement> &
6
6
  ColumnsVariants & {
@@ -12,7 +12,7 @@ export const Columns = ({ align, vAlign, columns, children, className }: Columns
12
12
  const columnsValue = columns.map((c) => `${c}fr`).join(' ');
13
13
 
14
14
  return (
15
- <div className={clsx(columnsRecipe({ align, vAlign }), className)} style={{ ['--columns' as never]: columnsValue }}>
15
+ <div className={cn(columnsRecipe({ align, vAlign }), className)} style={{ ['--columns' as never]: columnsValue }}>
16
16
  {children}
17
17
  </div>
18
18
  );
@@ -1,10 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { clsx } from 'clsx';
4
3
  import { useEffect, useState } from 'react';
5
4
 
6
5
  import { getCookie, setCookie } from './cookie';
7
6
  import { consentActions, consentContent, type ConsentCookieVariants, consentRecipe } from './styles.css';
7
+ import { cn } from '../../utils/styleOverride';
8
8
 
9
9
  import { Button } from '../Button';
10
10
 
@@ -89,7 +89,7 @@ export const ConsentCookie = ({
89
89
  if (!showConsent) return null;
90
90
 
91
91
  return (
92
- <div className={clsx(consentRecipe({ variant }), className)} role="dialog" aria-modal="true" aria-labelledby="consent-title">
92
+ <div className={cn(consentRecipe({ variant }), className)} role="dialog" aria-modal="true" aria-labelledby="consent-title">
93
93
  <div className={consentContent}>
94
94
  {children}
95
95
  <div className={consentActions}>
@@ -1,9 +1,9 @@
1
- import { clsx } from 'clsx';
2
-
3
1
  import { footerRecipe } from './styles.css';
2
+ import { cn } from '../../utils/styleOverride';
3
+
4
4
 
5
5
  export type FooterProps = React.HTMLAttributes<HTMLDivElement> & {
6
6
  children: React.ReactNode;
7
7
  };
8
8
 
9
- export const Footer = ({ children, className }: FooterProps) => <footer className={clsx(footerRecipe, className)}>{children}</footer>;
9
+ export const Footer = ({ children, className }: FooterProps) => <footer className={cn(footerRecipe, className)}>{children}</footer>;
@@ -1,13 +1,13 @@
1
- import { clsx } from 'clsx';
2
1
  import { forwardRef } from 'react';
3
2
 
4
3
  import { rowRecipe, type RowVariants } from './styles.css';
4
+ import { cn } from '../../utils/styleOverride';
5
5
 
6
6
  export type RowProps = React.HTMLAttributes<HTMLDivElement> & RowVariants;
7
7
 
8
8
  export const Row = forwardRef<HTMLDivElement, RowProps>(({ children, align, className }, ref) => {
9
9
  return (
10
- <div ref={ref} className={clsx(rowRecipe({ align }), className)}>
10
+ <div ref={ref} className={cn(rowRecipe({ align }), className)}>
11
11
  {children}
12
12
  </div>
13
13
  );
@@ -1,12 +1,12 @@
1
- import { clsx } from 'clsx';
2
1
  import { inputRecipe } from './styles.css';
3
2
 
4
3
  import { InputType } from './types';
4
+ import { cn } from '../../../utils/styleOverride';
5
5
 
6
6
  export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
7
7
  type?: InputType;
8
8
  };
9
9
 
10
10
  export const Input = ({ name, type = 'text', value, required, placeholder, className }: InputProps) => (
11
- <input id={name} name={name} type={type} value={value} required={required} placeholder={placeholder} className={clsx(inputRecipe(), className)} />
11
+ <input id={name} name={name} type={type} value={value} required={required} placeholder={placeholder} className={cn(inputRecipe(), className)} />
12
12
  );
@@ -1,6 +1,6 @@
1
- import { clsx } from 'clsx';
2
-
3
1
  import { labelRecipe, type LabelVariants } from './styles.css';
2
+ import { cn } from '../../../utils/styleOverride';
3
+
4
4
 
5
5
  export type LabelProps = React.LabelHTMLAttributes<HTMLLabelElement> &
6
6
  LabelVariants & {
@@ -10,7 +10,7 @@ export type LabelProps = React.LabelHTMLAttributes<HTMLLabelElement> &
10
10
  };
11
11
 
12
12
  export const Label = ({ label, name, required, className }: LabelProps) => (
13
- <label htmlFor={name} className={clsx(labelRecipe(), className)}>
13
+ <label htmlFor={name} className={cn(labelRecipe(), className)}>
14
14
  {label} {required && '*'}
15
15
  </label>
16
16
  );
@@ -1,11 +1,11 @@
1
- import { clsx } from 'clsx';
2
-
3
1
  import { textareaRecipe } from './styles.css';
2
+ import { cn } from '../../../utils/styleOverride';
3
+
4
4
 
5
5
  export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> & {
6
6
  name: string;
7
7
  };
8
8
 
9
9
  export const Textarea = ({ name, rows, className }: TextareaProps) => (
10
- <textarea id={name} rows={rows} name={name} className={clsx(textareaRecipe(), className)} />
10
+ <textarea id={name} rows={rows} name={name} className={cn(textareaRecipe(), className)} />
11
11
  );
@@ -1,6 +1,5 @@
1
1
  'use client';
2
2
 
3
- import { clsx } from 'clsx';
4
3
  import { useMemo } from 'react';
5
4
 
6
5
  import { Input } from './Input';
@@ -8,6 +7,7 @@ import { InputType } from './Input/types';
8
7
 
9
8
  import { errorMessage, messageContainer, textFieldRecipe } from './styles.css';
10
9
  import { Textarea } from './Textarea';
10
+ import { cn } from '../../utils/styleOverride';
11
11
 
12
12
  type CommonProps = {
13
13
  name: string;
@@ -31,7 +31,7 @@ export const TextField = (props: TextFieldProps) => {
31
31
  const isTextarea = type === 'textarea';
32
32
 
33
33
  return (
34
- <div className={clsx(textFieldRecipe(), className)}>
34
+ <div className={cn(textFieldRecipe(), className)}>
35
35
  {label && <label htmlFor={name}>{label}</label>}
36
36
 
37
37
  {isTextarea ? (
@@ -1,5 +1,5 @@
1
- import { clsx } from 'clsx';
2
1
  import { formRecipe } from './styles.css';
2
+ import { cn } from '../../utils/styleOverride';
3
3
 
4
4
  export type FormProps = React.HTMLAttributes<HTMLDivElement> & {
5
5
  as?: 'form' | 'div';
@@ -8,5 +8,5 @@ export type FormProps = React.HTMLAttributes<HTMLDivElement> & {
8
8
 
9
9
  export const Form = ({ as = 'form', children, className }: FormProps) => {
10
10
  const Component = as;
11
- return <Component className={clsx(formRecipe(), className)}>{children}</Component>;
11
+ return <Component className={cn(formRecipe(), className)}>{children}</Component>;
12
12
  };
@@ -1,7 +1,7 @@
1
- import { clsx } from 'clsx';
2
1
 
3
2
  import { ReactNode } from 'react';
4
3
  import { bar, toggleNavButton, toggleNavButtonMenu } from './styles.css';
4
+ import { cn } from '../../../utils/styleOverride';
5
5
 
6
6
  export type HeaderToggleNavProps = React.HTMLAttributes<HTMLDivElement> & {
7
7
  isOpen?: boolean;
@@ -11,12 +11,12 @@ export type HeaderToggleNavProps = React.HTMLAttributes<HTMLDivElement> & {
11
11
 
12
12
  export const HeaderToggleNav = ({ isOpen, onClick, children, className }: HeaderToggleNavProps) => {
13
13
  return (
14
- <button onClick={onClick} aria-label="Toggle navigation" className={clsx(toggleNavButton(), className)}>
14
+ <button onClick={onClick} aria-label="Toggle navigation" className={cn(toggleNavButton(), className)}>
15
15
  {children || (
16
16
  <span className={toggleNavButtonMenu()}>
17
- <span className={clsx(bar, isOpen && 'open')} />
18
- <span className={clsx(bar, isOpen && 'open')} />
19
- <span className={clsx(bar, isOpen && 'open')} />
17
+ <span className={cn(bar, isOpen && 'open')} />
18
+ <span className={cn(bar, isOpen && 'open')} />
19
+ <span className={cn(bar, isOpen && 'open')} />
20
20
  </span>
21
21
  )}
22
22
  </button>
@@ -1,10 +1,10 @@
1
1
  'use client';
2
2
 
3
- import clsx from 'clsx';
4
3
  import { useState } from 'react';
5
4
 
6
5
  import { HeaderToggleNav } from './HeaderToggleNav';
7
6
  import { headerOverlayRecipe, headerPlaceholder, headerRecipe, headerToggleNav, HeaderToggleNavVariants } from './styles.css';
7
+ import { cn } from '../../utils/styleOverride';
8
8
 
9
9
  export type HeaderProps = React.HTMLAttributes<HTMLDivElement> &
10
10
  HeaderToggleNavVariants & {
@@ -30,7 +30,7 @@ export const Header = ({ children, childrenOverlay, childrenToggleNav, isFixed =
30
30
 
31
31
  return (
32
32
  <>
33
- <header className={clsx(headerRecipe({ isFixed }), className)}>
33
+ <header className={cn(headerRecipe({ isFixed }), className)}>
34
34
  {childrenOverlay && <div className={headerOverlayRecipe({ isOpen: isNavOpen })}>{childrenOverlay}</div>}
35
35
 
36
36
  {children}
@@ -1,8 +1,8 @@
1
- import { clsx } from 'clsx';
2
1
  import parse from 'html-react-parser';
3
2
 
4
3
  import { headingRecipe, HeadingVariants } from './styles.css';
5
4
  import { AllowedHeading } from './types';
5
+ import { cn } from '../../utils/styleOverride';
6
6
 
7
7
  export type HeadingProps = React.HTMLAttributes<HTMLHeadingElement> &
8
8
  HeadingVariants & {
@@ -24,7 +24,7 @@ export const Heading = ({ as, size, className, children }: HeadingProps) => {
24
24
  const resolvedSize = size ?? levelToSize[as];
25
25
 
26
26
  return (
27
- <Component className={clsx(headingRecipe({ size: resolvedSize, as }), className)}>
27
+ <Component className={cn(headingRecipe({ size: resolvedSize, as }), className)}>
28
28
  {parse(typeof children === 'string' ? children : '')}
29
29
  </Component>
30
30
  );
@@ -1,9 +1,9 @@
1
- import clsx from 'clsx';
2
1
 
3
2
  import path from './path';
4
3
 
5
4
  import { iconRecipe } from './style.css';
6
5
  import { themeContract } from '../../theme/contract.css';
6
+ import { cn } from '../../utils/styleOverride';
7
7
 
8
8
  export type IconProps = React.HTMLAttributes<HTMLDivElement> & {
9
9
  icon?: string;
@@ -17,7 +17,7 @@ export const Icon = ({ icon = '', className, size = 24, iconPath = '', viewBox =
17
17
  const d = iconPath || path[icon as keyof typeof path];
18
18
 
19
19
  return (
20
- <svg className={clsx(iconRecipe, className)} viewBox={viewBox} width={`${size}px`} height={`${size}px`}>
20
+ <svg className={cn(iconRecipe, className)} viewBox={viewBox} width={`${size}px`} height={`${size}px`}>
21
21
  <path className={iconPath} d={d} fill={color} />
22
22
  </svg>
23
23
  );
@@ -1,12 +1,12 @@
1
1
  'use client';
2
2
 
3
- import { clsx } from 'clsx';
4
3
  import { forwardRef } from 'react';
5
4
  import CountUp from 'react-countup';
6
5
 
7
6
  import { useInView } from 'react-intersection-observer';
8
7
 
9
8
  import { keyNumberRecipe, keyNumberValueRecipe, KeyNumberValueVariants, type KeyNumberVariants } from './styles.css';
9
+ import { cn } from '../../utils/styleOverride';
10
10
 
11
11
  export type KeyNumberProps = React.HTMLAttributes<HTMLDivElement> &
12
12
  KeyNumberVariants &
@@ -39,7 +39,7 @@ export const KeyNumber = forwardRef<HTMLDivElement, KeyNumberProps>(
39
39
  }
40
40
  }
41
41
  }}
42
- className={clsx(keyNumberRecipe({ align, vAlign }), className)}>
42
+ className={cn(keyNumberRecipe({ align, vAlign }), className)}>
43
43
  {prefix}&nbsp;
44
44
  <CountUp end={inView ? value : 0} duration={duration} separator={separator} decimals={decimals} className={keyNumberValueRecipe({ size })} />
45
45
  &nbsp;{suffix}
@@ -1,5 +1,5 @@
1
- import { clsx } from 'clsx';
2
1
  import { languageSwitcherIcon, languageSwitcherRecipe, languageSwitcherSelect, type LanguageSwitcherVariants } from './styles.css';
2
+ import { cn } from '../../utils/styleOverride';
3
3
 
4
4
  import { Icon } from '../Icon';
5
5
 
@@ -31,7 +31,7 @@ export const LanguageSwitcher = ({
31
31
  const handleSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => onChange(event.target.value);
32
32
 
33
33
  return (
34
- <div className={clsx(languageSwitcherRecipe({ variant, size }), className)}>
34
+ <div className={cn(languageSwitcherRecipe({ variant, size }), className)}>
35
35
  <select className={languageSwitcherSelect} value={currentLocale} onChange={handleSelectChange} disabled={disabled} aria-label="Switch language">
36
36
  {placeholder && (
37
37
  <option value="" disabled>
@@ -1,10 +1,10 @@
1
- import { clsx } from 'clsx';
2
1
  import { ReactNode } from 'react';
3
2
 
4
3
  import { logo } from './styles.css';
4
+ import { cn } from '../../utils/styleOverride';
5
5
 
6
6
  export type LogoProps = React.HTMLAttributes<HTMLDivElement> & {
7
7
  children?: ReactNode;
8
8
  };
9
9
 
10
- export const Logo = ({ children, className }: LogoProps) => <div className={clsx(logo, className)}>{children}</div>;
10
+ export const Logo = ({ children, className }: LogoProps) => <div className={cn(logo, className)}>{children}</div>;
@@ -1,9 +1,9 @@
1
- import { clsx } from 'clsx';
2
-
3
1
  import { main } from './styles.css';
2
+ import { cn } from '../../utils/styleOverride';
3
+
4
4
 
5
5
  export type MainProps = React.HTMLAttributes<HTMLDivElement> & {
6
6
  children: React.ReactNode;
7
7
  };
8
8
 
9
- export const Main = ({ className, children }: MainProps) => <main className={clsx(main, className)}>{children}</main>;
9
+ export const Main = ({ className, children }: MainProps) => <main className={cn(main, className)}>{children}</main>;
@@ -1,11 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { clsx } from 'clsx';
4
3
  import { ReactNode, useState } from 'react';
5
4
 
6
5
  import { modal, modalContentCloseButton, modalContentRecipe, type ModalVariants } from './styles.css';
7
6
 
8
7
  import { themeContract } from '../../theme/contract.css';
8
+ import { cn } from '../../utils/styleOverride';
9
9
  import { Actions } from '../Actions';
10
10
  import { Button } from '../Button';
11
11
  import { Icon } from '../Icon';
@@ -39,7 +39,7 @@ export const Modal = ({ triggerLabel, className, children, align, withCloseButto
39
39
 
40
40
  {isOpen && (
41
41
  <div className={modal} role="dialog" aria-modal="true">
42
- <div className={clsx(modalContentRecipe({ align }), className)}>
42
+ <div className={cn(modalContentRecipe({ align }), className)}>
43
43
  {withCloseButton && (
44
44
  <button type="button" className={modalContentCloseButton} onClick={closeModal} aria-label="Close modal">
45
45
  <Icon icon="close" color={themeContract.modal.closeButtonColor} />
@@ -1,10 +1,10 @@
1
- import { clsx } from 'clsx';
2
-
3
1
  import { navRecipe, NavVariants } from './styles.css';
2
+ import { cn } from '../../utils/styleOverride';
3
+
4
4
 
5
5
  export type NavProps = React.HTMLAttributes<HTMLDivElement> &
6
6
  NavVariants & {
7
7
  children: React.ReactNode;
8
8
  };
9
9
 
10
- export const Nav = ({ children, className, direction }: NavProps) => <div className={clsx(navRecipe({ direction }), className)}>{children}</div>;
10
+ export const Nav = ({ children, className, direction }: NavProps) => <div className={cn(navRecipe({ direction }), className)}>{children}</div>;
@@ -1,9 +1,9 @@
1
- import { clsx } from 'clsx';
2
-
3
1
  import { navLegalRecipe } from './styles.css';
2
+ import { cn } from '../../utils/styleOverride';
3
+
4
4
 
5
5
  export type NavLegalProps = React.HTMLAttributes<HTMLDivElement> & {
6
6
  children: React.ReactNode;
7
7
  };
8
8
 
9
- export const NavLegal = ({ className, children }: NavLegalProps) => <div className={clsx(navLegalRecipe(), className)}>{children}</div>;
9
+ export const NavLegal = ({ className, children }: NavLegalProps) => <div className={cn(navLegalRecipe(), className)}>{children}</div>;
@@ -1,7 +1,7 @@
1
- import { clsx } from 'clsx';
2
1
 
3
2
  import { navSocialLink, navSocialRecipe } from './styles.css';
4
3
  import { Social } from './types';
4
+ import { cn } from '../../utils/styleOverride';
5
5
  import { Icon } from '../Icon';
6
6
  import icons from '../Icon/path';
7
7
 
@@ -11,7 +11,7 @@ export type NavSocialProps = React.HTMLAttributes<HTMLDivElement> & {
11
11
  };
12
12
 
13
13
  export const NavSocial = ({ size = 24, navSocial, className }: NavSocialProps) => (
14
- <div className={clsx(navSocialRecipe(), className)}>
14
+ <div className={cn(navSocialRecipe(), className)}>
15
15
  {navSocial?.map(({ name, link }, index) => {
16
16
  const capitalized = name[0].toUpperCase() + name.slice(1);
17
17
 
@@ -1,7 +1,7 @@
1
- import { clsx } from 'clsx';
2
1
  import parse from 'html-react-parser';
3
2
 
4
3
  import { paragraphRecipe, type ParagraphVariants } from './styles.css';
4
+ import { cn } from '../../utils/styleOverride';
5
5
 
6
6
  export type ParagraphProps = React.HTMLAttributes<HTMLDivElement> &
7
7
  ParagraphVariants & {
@@ -9,5 +9,5 @@ export type ParagraphProps = React.HTMLAttributes<HTMLDivElement> &
9
9
  };
10
10
 
11
11
  export const Paragraph = ({ align, className, children }: ParagraphProps) => (
12
- <p className={clsx(paragraphRecipe({ align }), className)}>{parse(typeof children === 'string' ? children : '')}</p>
12
+ <p className={cn(paragraphRecipe({ align }), className)}>{parse(typeof children === 'string' ? children : '')}</p>
13
13
  );
@@ -1,6 +1,6 @@
1
- import { clsx } from 'clsx';
2
-
3
1
  import { sectionContent, sectionRecipe, type SectionVariants } from './styles.css';
2
+ import { cn } from '../../utils/styleOverride';
3
+
4
4
 
5
5
  export type SectionProps = React.HTMLAttributes<HTMLDivElement> &
6
6
  SectionVariants & {
@@ -21,7 +21,7 @@ export const Section = ({
21
21
  }: SectionProps) => (
22
22
  <section
23
23
  style={style}
24
- className={clsx(sectionRecipe({ align, isDark, variant, isFullHeight, withPaddingTop, withPaddingBottom, isHeaderFixed }), className)}>
24
+ className={cn(sectionRecipe({ align, isDark, variant, isFullHeight, withPaddingTop, withPaddingBottom, isHeaderFixed }), className)}>
25
25
  <div className={sectionContent}>{children}</div>
26
26
  </section>
27
27
  );
@@ -1,6 +1,5 @@
1
1
  'use client';
2
2
 
3
- import { clsx } from 'clsx';
4
3
  import { useEffect, useRef, useState } from 'react';
5
4
 
6
5
  import {
@@ -15,6 +14,7 @@ import {
15
14
  type VideoVariants,
16
15
  } from './styles.css';
17
16
  import { themeContract } from '../../theme/contract.css';
17
+ import { cn } from '../../utils/styleOverride';
18
18
  import { Icon } from '../Icon';
19
19
 
20
20
  export type VideoProps = React.HTMLAttributes<HTMLDivElement> &
@@ -82,7 +82,7 @@ export const Video = ({
82
82
  }, [isMuted]);
83
83
 
84
84
  return (
85
- <div className={clsx(videoRecipe({ size }), className)}>
85
+ <div className={cn(videoRecipe({ size }), className)}>
86
86
  <video ref={videoRef} className={videoElement} playsInline muted={isMuted} autoPlay={isAutoPlay} onEnded={handleVideoEnded}>
87
87
  <source src={video} type="video/mp4" />
88
88
  </video>
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export { createLightTheme, createDarkTheme, type ThemeOverrides } from './theme/
6
6
  // Styles utilities (sprinkles, media queries, etc.)
7
7
  export { sprinkles, type Sprinkles } from './styles/sprinkles.css';
8
8
  export { breakpoints, queries } from './styles/mediaqueries';
9
+ export { styleOverride, cn } from './utils/styleOverride';
9
10
 
10
11
  // Components
11
12
 
@@ -0,0 +1,43 @@
1
+ import { style, StyleRule } from '@vanilla-extract/css';
2
+
3
+ /**
4
+ * Creates a style override with higher specificity to ensure consumer styles
5
+ * always win over library component styles, regardless of CSS load order.
6
+ *
7
+ * Uses the [class] attribute selector to increase specificity from (0,1,0) to (0,2,0).
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * import { styleOverride } from '@latte-macchiat-io/latte-vanilla-components/utils/styleOverride';
12
+ *
13
+ * const customButton = styleOverride({
14
+ * textTransform: 'uppercase',
15
+ * letterSpacing: '0.1em',
16
+ * backgroundColor: 'red',
17
+ * });
18
+ *
19
+ * <Button className={customButton}>My Button</Button>
20
+ * ```
21
+ */
22
+ export function styleOverride(styleRule: StyleRule) {
23
+ return style({
24
+ selectors: {
25
+ '&[class]': styleRule,
26
+ },
27
+ });
28
+ }
29
+
30
+ /**
31
+ * Alternative utility that merges class names properly.
32
+ * This is a lightweight replacement for clsx with better type safety.
33
+ *
34
+ * @example
35
+ * ```tsx
36
+ * import { cn } from '@latte-macchiat-io/latte-vanilla-components/utils/styleOverride';
37
+ *
38
+ * <Button className={cn(baseStyle, customStyle)} />
39
+ * ```
40
+ */
41
+ export function cn(...classes: (string | undefined | null | false)[]) {
42
+ return classes.filter(Boolean).join(' ');
43
+ }