Skip to content

Commit 75e6977

Browse files
Sch/fix/styling (#18)
* feat(baukasten): add className prop support for custom style merging Add className prop to multiple components allowing users to extend or override styles using clsx for proper class merging: - Alert: merge custom className with variant styles - Badge: merge custom className with variant/size/outline styles - Button: merge custom className with all button variants - Hero: merge custom className with container styles - Input: merge custom className with input styles - StatusBar: merge custom className with item styles - Table: merge custom className with all table sub-components Also add Storybook examples demonstrating className merging capabilities. * feat(baukasten): wrap generated CSS in @layer for easier style overriding Add @layer baukasten wrapper around all component library CSS to ensure lower specificity than consumer styles. This makes overriding styles easier without requiring !important or complex selectors. Changes: - Add wrapInLayer Vite plugin to wrap vanilla-extract CSS output - Wrap global styles in @layer in generate-css.ts and css-variables.ts - CSS variables remain outside the layer (no cascade protection needed) - Add documentation comments explaining the layer strategy
1 parent 2547338 commit 75e6977

File tree

11 files changed

+132
-23
lines changed

11 files changed

+132
-23
lines changed

packages/baukasten/scripts/generate-css.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@ const distDir = join(rootDir, 'dist');
3636

3737
/**
3838
* Combine all tokens into a full CSS string
39+
* CSS variables remain outside the layer (they don't need cascade protection)
40+
* Global styles are wrapped in @layer baukasten for easy overriding
3941
*/
4042
function buildFullCSS(): string {
4143
return `/**
4244
* Baukasten UI - CSS Variables and Global Styles
4345
* Generated automatically - do not edit manually
44-
*
46+
*
4547
* This file contains all design tokens and global styles for the Baukasten UI library.
4648
*/
4749
@@ -53,7 +55,9 @@ ${typographyTokens}
5355
5456
${effectsTokens}
5557
58+
@layer baukasten {
5659
${globalStylesContent}
60+
}
5761
`;
5862
}
5963

@@ -104,8 +108,11 @@ function addHeader(css: string, variant: 'vscode' | 'theia' | 'web'): string {
104108
return `/**
105109
* Baukasten UI - ${variant.toUpperCase()} Variant
106110
* ${descriptions[variant]}
107-
*
111+
*
108112
* Generated automatically - do not edit manually
113+
*
114+
* Global styles are wrapped in @layer baukasten for easy overriding.
115+
* CSS variables remain outside the layer (they don't need cascade protection).
109116
*/
110117
111118
${css}`;
@@ -121,7 +128,7 @@ function main() {
121128
mkdirSync(distDir, { recursive: true });
122129
}
123130

124-
// Build the base CSS
131+
// Build the base CSS (layer wrapping is done at source level in css-variables.ts)
125132
const baseCss = buildFullCSS();
126133

127134
// Generate VSCode variant (uses --vscode-* variables)

packages/baukasten/src/components/Alert/Alert.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useState } from 'react';
2+
import clsx from 'clsx';
23
import { Icon } from '../Icon';
34
import type { CodiconName } from '../Icon/codicon-names';
45
import {
@@ -113,6 +114,7 @@ export const Alert: React.FC<AlertProps> = ({
113114
icon,
114115
closable = false,
115116
onClose,
117+
className,
116118
...props
117119
}) => {
118120
const [isVisible, setIsVisible] = useState(true);
@@ -129,7 +131,7 @@ export const Alert: React.FC<AlertProps> = ({
129131
}
130132

131133
return (
132-
<div className={alert({ variant })} role="alert" {...props}>
134+
<div className={clsx(alert({ variant }), className)} role="alert" {...props}>
133135
{displayIcon && (
134136
<div className={alertIcon({ variant })}>
135137
{displayIcon}

packages/baukasten/src/components/Badge/Badge.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import clsx from 'clsx';
23
import { type Size } from '../../styles';
34
import { badge } from './Badge.css';
45

@@ -81,11 +82,12 @@ export const Badge: React.FC<BadgeProps> = ({
8182
variant = 'default',
8283
size = 'md',
8384
outline = false,
85+
className,
8486
...props
8587
}) => {
8688
return (
8789
<span
88-
className={badge({ variant, size, outline })}
90+
className={clsx(badge({ variant, size, outline }), className)}
8991
{...props}
9092
>
9193
{children}

packages/baukasten/src/components/Button/Button.stories.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,3 +445,60 @@ export const Showcase: Story = {
445445
},
446446
},
447447
};
448+
449+
/**
450+
* Demonstrating that custom classNames are properly merged with built-in styles.
451+
*/
452+
export const WithCustomClassName: Story = {
453+
render: () => (
454+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--bk-spacing-4)' }}>
455+
<style>
456+
{`
457+
.custom-rotate {
458+
transform: rotate(-2deg);
459+
}
460+
.custom-shadow {
461+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
462+
}
463+
.custom-gradient {
464+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
465+
}
466+
.custom-border {
467+
border: 2px dashed yellow !important;
468+
}
469+
`}
470+
</style>
471+
<div>
472+
<h4 style={{ marginBottom: 'var(--bk-spacing-2)', fontSize: 'var(--bk-font-size-sm)', fontWeight: 'var(--bk-font-weight-medium)' }}>
473+
Custom className merging
474+
</h4>
475+
<p style={{ marginBottom: 'var(--bk-spacing-3)', fontSize: 'var(--bk-font-size-sm)', color: 'var(--bk-color-text-secondary)' }}>
476+
Custom classes are merged with built-in styles, allowing you to extend the button appearance.
477+
</p>
478+
<div style={{ display: 'flex', gap: 'var(--bk-gap-sm)', flexWrap: 'wrap', alignItems: 'center' }}>
479+
<Button variant="primary">Normal Button</Button>
480+
<Button variant="primary" className="custom-rotate">With Rotation</Button>
481+
<Button variant="primary" className="custom-shadow">With Shadow</Button>
482+
<Button variant="secondary" className="custom-gradient">With Gradient</Button>
483+
<Button variant="ghost" className="custom-border">With Dashed Border</Button>
484+
</div>
485+
</div>
486+
<div>
487+
<h4 style={{ marginBottom: 'var(--bk-spacing-2)', fontSize: 'var(--bk-font-size-sm)', fontWeight: 'var(--bk-font-weight-medium)' }}>
488+
Multiple custom classes
489+
</h4>
490+
<div style={{ display: 'flex', gap: 'var(--bk-gap-sm)', flexWrap: 'wrap', alignItems: 'center' }}>
491+
<Button variant="primary" className="custom-rotate custom-shadow">Rotate + Shadow</Button>
492+
<Button variant="secondary" className="custom-gradient custom-shadow">Gradient + Shadow</Button>
493+
</div>
494+
</div>
495+
</div>
496+
),
497+
parameters: {
498+
docs: {
499+
description: {
500+
story: 'Custom classNames are properly merged with the built-in button styles using `clsx`. This allows you to extend or override specific styles without losing the base button functionality.',
501+
},
502+
},
503+
},
504+
};

packages/baukasten/src/components/Button/Button.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import clsx from 'clsx';
23
import { type Size } from '../../styles';
34
import { button } from './Button.css';
45

@@ -111,11 +112,12 @@ export const Button: React.FC<ButtonProps> = ({
111112
outline = false,
112113
circular = false,
113114
children,
115+
className,
114116
...props
115117
}) => {
116118
return (
117119
<button
118-
className={button({ variant, size, width, outline, circular })}
120+
className={clsx(button({ variant, size, width, outline, circular }), className)}
119121
{...props}
120122
>
121123
{children}

packages/baukasten/src/components/Hero/Hero.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import clsx from 'clsx';
23
import { heroContainer, heroTitle, heroDescription } from './Hero.css';
34

45
/**
@@ -117,11 +118,12 @@ export const Hero: React.FC<HeroProps> = ({
117118
size = 'md',
118119
background = 'default',
119120
children,
121+
className,
120122
...props
121123
}) => {
122124
return (
123125
<div
124-
className={heroContainer({ align, size, background })}
126+
className={clsx(heroContainer({ align, size, background }), className)}
125127
{...props}
126128
>
127129
<h1 className={heroTitle}>{title}</h1>

packages/baukasten/src/components/Input/Input.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import clsx from 'clsx';
23
import { inputWrapper, input, errorText } from './Input.css';
34
import { type Size } from '../../styles';
45

@@ -66,6 +67,7 @@ export const Input: React.FC<InputProps> = ({
6667
size = 'md',
6768
error,
6869
fullWidth = false,
70+
className,
6971
...props
7072
}) => {
7173
// Only show error text if error is a string (not just boolean)
@@ -74,7 +76,7 @@ export const Input: React.FC<InputProps> = ({
7476
return (
7577
<div className={inputWrapper({ fullWidth })}>
7678
<input
77-
className={input({ size, hasError: !!error })}
79+
className={clsx(input({ size, hasError: !!error }), className)}
7880
{...props}
7981
/>
8082
{errorMessage && <span className={errorText}>{errorMessage}</span>}

packages/baukasten/src/components/StatusBar/StatusBar.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,16 @@ export const StatusBarItem: React.FC<StatusBarItemProps> = ({
161161
onClick,
162162
active = false,
163163
tooltip,
164+
className,
164165
...props
165166
}) => {
166167
return (
167168
<div
168-
className={styles.statusBarItem({
169+
className={clsx(styles.statusBarItem({
169170
variant,
170171
clickable: !!onClick,
171172
active,
172-
})}
173+
}), className)}
173174
onClick={onClick}
174175
title={tooltip}
175176
role={onClick ? 'button' : undefined}

packages/baukasten/src/components/Table/Table.tsx

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { createContext, useContext } from 'react';
2+
import clsx from 'clsx';
23
import { type Size } from '../../styles';
34
import * as styles from './Table.css';
45

@@ -243,6 +244,7 @@ export const Table: React.FC<TableProps> & {
243244
caption,
244245
captionSide = 'top',
245246
children,
247+
className,
246248
...props
247249
}) => {
248250
const contextValue: TableContextValue = {
@@ -253,7 +255,7 @@ export const Table: React.FC<TableProps> & {
253255

254256
const table = (
255257
<table
256-
className={styles.table({ fullWidth, bordered })}
258+
className={clsx(styles.table({ fullWidth, bordered }), className)}
257259
{...props}
258260
>
259261
{caption && <caption className={styles.caption({ captionSide })}>{caption}</caption>}
@@ -289,12 +291,13 @@ export const Table: React.FC<TableProps> & {
289291
const TableHead: React.FC<TableHeadProps> = ({
290292
sticky = false,
291293
children,
294+
className,
292295
...props
293296
}) => {
294297
const { bordered } = useContext(TableContext);
295298

296299
return (
297-
<thead className={styles.tableHead({ sticky, bordered })} {...props}>
300+
<thead className={clsx(styles.tableHead({ sticky, bordered }), className)} {...props}>
298301
{children}
299302
</thead>
300303
);
@@ -315,14 +318,15 @@ const TableBody: React.FC<TableBodyProps> = ({
315318
emptyComponent,
316319
colSpan = DEFAULT_COLSPAN,
317320
children,
321+
className,
318322
...props
319323
}) => {
320324
const { variant } = useContext(TableContext);
321325

322326
// Show loading state
323327
if (loading) {
324328
return (
325-
<tbody className={styles.tableBody} {...props}>
329+
<tbody className={clsx(styles.tableBody, className)} {...props}>
326330
<tr className={styles.tableRow({ variant, selected: false, hoverable: false })}>
327331
<td
328332
className={styles.tableCell({ align: 'center', bordered: false, size: 'md' })}
@@ -339,7 +343,7 @@ const TableBody: React.FC<TableBodyProps> = ({
339343
// Show empty state
340344
if (empty || (!children || (React.Children.count(children) === 0))) {
341345
return (
342-
<tbody className={styles.tableBody} {...props}>
346+
<tbody className={clsx(styles.tableBody, className)} {...props}>
343347
<tr className={styles.tableRow({ variant, selected: false, hoverable: false })}>
344348
<td
345349
className={styles.tableCell({ align: 'center', bordered: false, size: 'md' })}
@@ -354,7 +358,7 @@ const TableBody: React.FC<TableBodyProps> = ({
354358
}
355359

356360
return (
357-
<tbody className={styles.tableBody} {...props}>
361+
<tbody className={clsx(styles.tableBody, className)} {...props}>
358362
{children}
359363
</tbody>
360364
);
@@ -365,12 +369,13 @@ const TableBody: React.FC<TableBodyProps> = ({
365369
*/
366370
const TableFooter: React.FC<React.HTMLAttributes<HTMLTableSectionElement>> = ({
367371
children,
372+
className,
368373
...props
369374
}) => {
370375
const { bordered } = useContext(TableContext);
371376

372377
return (
373-
<tfoot className={styles.tableFooter({ bordered })} {...props}>
378+
<tfoot className={clsx(styles.tableFooter({ bordered }), className)} {...props}>
374379
{children}
375380
</tfoot>
376381
);
@@ -383,13 +388,14 @@ const TableRow: React.FC<TableRowProps> = ({
383388
selected = false,
384389
hoverable = true,
385390
children,
391+
className,
386392
...props
387393
}) => {
388394
const { variant } = useContext(TableContext);
389395

390396
const rowClassName = variant === 'zebra'
391-
? `${styles.tableRow({ variant, selected, hoverable })} ${styles.zebraRow}`
392-
: styles.tableRow({ variant, selected, hoverable });
397+
? clsx(styles.tableRow({ variant, selected, hoverable }), styles.zebraRow, className)
398+
: clsx(styles.tableRow({ variant, selected, hoverable }), className);
393399

394400
return (
395401
<tr
@@ -407,17 +413,18 @@ const TableRow: React.FC<TableRowProps> = ({
407413
const TableCell: React.FC<TableCellProps> = ({
408414
align = 'left',
409415
children,
416+
className,
410417
...props
411418
}) => {
412419
const { bordered, size } = useContext(TableContext);
413420

414421
return (
415422
<td
416-
className={styles.tableCell({
423+
className={clsx(styles.tableCell({
417424
align,
418425
bordered,
419426
size,
420-
})}
427+
}), className)}
421428
{...props}
422429
>
423430
{children}
@@ -435,6 +442,7 @@ const TableHeaderCell: React.FC<TableHeaderCellProps> = ({
435442
onSort,
436443
children,
437444
scope = 'col',
445+
className,
438446
...props
439447
}) => {
440448
const { bordered, size } = useContext(TableContext);
@@ -449,12 +457,12 @@ const TableHeaderCell: React.FC<TableHeaderCellProps> = ({
449457
return (
450458
<th
451459
scope={scope}
452-
className={styles.tableHeaderCell({
460+
className={clsx(styles.tableHeaderCell({
453461
align,
454462
sortable,
455463
bordered,
456464
size,
457-
})}
465+
}), className)}
458466
onClick={sortable ? onSort : undefined}
459467
onKeyDown={sortable ? handleKeyDown : undefined}
460468
tabIndex={sortable ? 0 : undefined}

packages/baukasten/src/styles/css-variables.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,16 @@ export const cssVariables = `
3939
/**
4040
* Complete CSS including variables and global styles
4141
* This includes scrollbar styling and utility classes
42+
*
43+
* Global styles are wrapped in @layer baukasten for easy overriding
44+
* CSS variables remain outside the layer (they don't need cascade protection)
4245
*/
4346
export const cssVariablesWithGlobalStyles = `
4447
${cssVariables}
4548
49+
@layer baukasten {
4650
${globalStylesContent}
51+
}
4752
`;
4853

4954
/**

0 commit comments

Comments
 (0)