Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
b9a6e65
chore: πŸ€– update required packages
punkbit Jan 19, 2026
5283ab3
chore: πŸ€– provide styled components types
punkbit Jan 19, 2026
3143f07
fix: πŸ› linter should ignore d.ts files
punkbit Jan 19, 2026
6238526
fix: πŸ› use double quotes
punkbit Jan 19, 2026
4b09413
fix: πŸ› vite > v2 separates vitest config
punkbit Jan 19, 2026
3f1333b
chore: πŸ€– reorder package
punkbit Jan 19, 2026
750d3ec
chore: πŸ€– remove side property (deprecated)
punkbit Jan 19, 2026
62c10ef
fix: πŸ› dropdown amends
punkbit Jan 19, 2026
4a20902
fix: πŸ› theme prop
punkbit Jan 19, 2026
2ed9c1c
fix: πŸ› type
punkbit Jan 19, 2026
37ec2c9
chore: πŸ€– TIAS build version supported on next v16 RSC
punkbit Jan 20, 2026
f72ad14
chore: πŸ€– WIP ongoing styled-component v6.1.11 (non experimental) support
punkbit Jan 20, 2026
b6c48a4
chore: πŸ€– strict react version for dev
punkbit Jan 20, 2026
ea50f3e
fix: πŸ› missing ref on forwardRef, might have plenty of these
punkbit Jan 20, 2026
57c554d
chore: πŸ€– update lockfile
punkbit Jan 20, 2026
b22624d
chore: πŸ€– remove optional flag from ref (typo)
punkbit Jan 20, 2026
a769ad9
chore: πŸ€– add comment for future ref
punkbit Jan 20, 2026
694b240
refactor: πŸ’‘ banner
punkbit Jan 20, 2026
b373fd2
chore: πŸ€– lint amends (double quotes)
punkbit Jan 20, 2026
1ce4ff9
refactor: πŸ’‘ removes style prop as typed prop
punkbit Jan 20, 2026
f9cc7f4
chore: πŸ€– remove ajv
punkbit Jan 20, 2026
0c406c0
chore: πŸ€– format
punkbit Jan 20, 2026
c9bf9f4
test: πŸ’ add aria pressed to ButtonGroup
punkbit Jan 20, 2026
eb0d393
chore: πŸ€– format
punkbit Jan 20, 2026
8dea336
test: πŸ’ use local getByText
punkbit Jan 20, 2026
a231533
chore: πŸ€– update lockfile
punkbit Jan 21, 2026
91f4800
chore: πŸ€– add changeset
punkbit Jan 21, 2026
2206a99
chore: πŸ€– small text amend to trigger vercel deploy
punkbit Jan 21, 2026
4729912
chore: πŸ€– prevent running on CI
punkbit Jan 22, 2026
48523da
chore: πŸ€– add HUSKY to preven husky runnig pre-commit hook
punkbit Jan 22, 2026
82ff68b
Merge branch 'main' into fix/minimal-react-19-rsc-support
punkbit Jan 22, 2026
f3a0dcc
Merge branch 'chore/prevent-pre-commit-run-on-ci' into fix/minimal-re…
punkbit Jan 22, 2026
b1de479
fix: πŸ› conflict resolution
punkbit Jan 22, 2026
b23c72f
chore: πŸ€– bump rc number
punkbit Jan 22, 2026
adb82c3
docs: πŸ“ build esm, how to use
punkbit Jan 23, 2026
9e0cbc3
chore: πŸ€– ESM vite builder (wip)
punkbit Jan 23, 2026
738d97e
fix: πŸ› remove .tsx extension from import statements
punkbit Jan 23, 2026
1e99d39
fix: πŸ› remove .tsx extension from import statements
punkbit Jan 23, 2026
ed4f7b8
fix: πŸ› remove .tsx extension from import statements
punkbit Jan 23, 2026
a5e4ee9
Merge branch 'main' into fix/remove-tsx-extension-import-statements
punkbit Jan 23, 2026
ade0c8a
Merge branch 'fix/remove-tsx-extension-import-statements' into perf/d…
punkbit Jan 23, 2026
a7a4b9d
fix: πŸ› remove .ts extension from import statements
punkbit Jan 23, 2026
f8a39e8
fix: πŸ› remove .ts extension from import statements
punkbit Jan 23, 2026
3da59f1
chore: πŸ€– add eslint to assess import extensions not required
punkbit Jan 23, 2026
0b4d381
Merge branch 'chore/prevent-import-extensions' into perf/distribute-e…
punkbit Jan 23, 2026
1d0d42c
Merge branch 'fix/remove-tsx-extension-import-statements' into perf/d…
punkbit Jan 23, 2026
9df6254
chore: πŸ€– format
punkbit Jan 23, 2026
641ca6b
chore: πŸ€– temporary custom resolve tsconfig path
punkbit Jan 23, 2026
0f1082d
refactor: πŸ’‘ export from correct theme boundary
punkbit Jan 23, 2026
219b6ae
chore: πŸ€– node externals in vite, remove alias
punkbit Jan 23, 2026
ae52a23
chore: πŸ€– use relative paths
punkbit Jan 23, 2026
777ca23
chore: πŸ€– use externalize deps
punkbit Jan 23, 2026
9f48675
Merge branch 'main' into perf/distribute-esm-unbundled
punkbit Jan 23, 2026
652552b
chore: πŸ€– for ESM compatibility, tweak/handle CJS components
punkbit Jan 23, 2026
4487fcb
chore: revert ts alias rewrite to relative
punkbit Jan 23, 2026
95830e0
chore: lint do not allow barrel imports
punkbit Jan 23, 2026
b0bfea5
chore: remove excludes from tsconfig
punkbit Jan 23, 2026
e17a4ec
chore: set vite settings to preserve file struct in output
punkbit Jan 23, 2026
51a8c5b
fix: solve import cycles
punkbit Jan 23, 2026
3a50adf
fix: solve import cycles in stories
punkbit Jan 23, 2026
3c0f773
fix: build amends
punkbit Jan 23, 2026
677d314
fix: add .js extension
punkbit Jan 23, 2026
b80a63b
chore: analyze and visualise bundle
punkbit Jan 23, 2026
56099bb
chore: split ESM, CJS distribution
punkbit Jan 23, 2026
e76a47d
Merge branch 'main' into chore/distribution-unbundled
punkbit Jan 26, 2026
ac9c7c8
Merge branch 'main' into chore/distribution-unbundled
punkbit Jan 26, 2026
2f0b0f3
chore: format
punkbit Jan 26, 2026
3dd00a2
fix: πŸ› lint code block
punkbit Jan 26, 2026
a2ffec6
fix: πŸ› import Separator
punkbit Jan 26, 2026
d54269a
chore: format
punkbit Jan 26, 2026
ee433d9
fix: πŸ› import Separator
punkbit Jan 26, 2026
4edae27
chore: πŸ€– add changeset
punkbit Jan 26, 2026
8a820d8
chore: merge main
punkbit Jan 29, 2026
ecbbc8b
chore: πŸ€– use 0.0.251-rc.62
punkbit Jan 29, 2026
578471e
chore: πŸ€– resolve conflict resolution, deleted files which were remove…
punkbit Jan 29, 2026
32cfad1
chore: πŸ€– resolve conflict resolution, middle truncator
punkbit Jan 29, 2026
777aee5
chore: πŸ€– resolve conflict resolution, missing container changes
punkbit Jan 29, 2026
e163516
refactor: πŸ’‘ FileMultiUpload to follow FileUpload due to middle truncator
punkbit Jan 29, 2026
38a68b1
fix: πŸ› prevent icon success pushed right
punkbit Jan 29, 2026
447491c
Merge branch 'fix/middle-truncator-icon-success-upload-val-position' …
punkbit Jan 29, 2026
b0fafa5
fix: πŸ› remove file size from multiple file upload
punkbit Jan 29, 2026
c682e36
Merge branch 'main' into chore/distribution-unbundled
punkbit Feb 2, 2026
ad93bef
Merge branch 'main' into chore/distribution-unbundled
punkbit Feb 2, 2026
332649d
chore: πŸ€– merge conflict amend for ButtonGroup
punkbit Feb 2, 2026
c13bcd7
chore: πŸ€– remove comment
punkbit Feb 2, 2026
2edec36
Merge branch 'main' into chore/distribution-unbundled
punkbit Feb 2, 2026
cee7b6b
Merge branch 'main' into chore/distribution-unbundled
punkbit Feb 3, 2026
cb1ea1c
chore: πŸ€– add static copy vite plugin
punkbit Feb 4, 2026
a179e88
chore: πŸ€– process static css modules to dist
punkbit Feb 4, 2026
8e9dd4a
chore: πŸ€– update lockfile
punkbit Feb 4, 2026
3611167
chore: πŸ€– ignore css modules
punkbit Feb 4, 2026
fcf1eeb
chore: πŸ€– provide POC/Demo CSS Module
punkbit Feb 4, 2026
3228688
docs: πŸ“ add CSS Modules section describing the setup for consumer apps
punkbit Feb 4, 2026
c74f0ec
chore: πŸ€– update changeset
punkbit Feb 4, 2026
0c9e043
chore: πŸ€– update changeset (add note)
punkbit Feb 4, 2026
7f6477d
Merge branch 'main' into chore/init-css-modules-setup
punkbit Mar 5, 2026
77d9494
docs: πŸ“ resolve merge conflict README.md
punkbit Mar 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/eight-colts-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@clickhouse/click-ui': minor
---

This library will now use CSS Modules for styling and is distributed unbundled, giving your application full control over bundling and optimisations. This means you only include what you actually use, resulting in smaller bundle sizes and better performance!

NOTE: We're currently migrating from Styled-Components to CSS Modules. Some components may still use Styled-Components during this transition period.

To learn more about CSS modules support, check our documentation [here](https://github.com/ClickHouse/click-ui?tab=readme-ov-file#css-modules)
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ You can find the official docs for the Click UI design system and component libr
* [Distribution](#distribution)
- [Build](#build)
- [Use Click UI](#use-click-ui)
- [CSS Modules](#css-modules)
- [Deep imports support](#deep-imports-support)
- [Examples](#examples)
* [Themes](#themes)
Expand Down Expand Up @@ -316,6 +317,39 @@ export default () => {

To learn more about individual components, visit [Click UI components](https://clickhouse.design/click-ui).

### CSS Modules

This library uses [CSS Modules](https://github.com/css-modules/css-modules) for styling and is distributed unbundled, giving your application full control over bundling and optimizations. This means you only include what you actually use, resulting in smaller bundle sizes and better performance!

Most modern React frameworks support CSS Modules out of the box, including Next.js, Vite, Create React App, and TanStack Start, with no configuration required.

> [!NOTE]
> We're currently migrating from Styled-Components to CSS Modules. Some components may still use Styled-Components during this transition period.
#### Benefits

CSS Modules align naturally with component-level imports. When you import a component like `Button`, its `Button.module.css` is automatically included. If you don't use the component, neither the JavaScript, or CSS will be bundled in your application's output. Only the necessary stylesheets will be included in the output bundle.

#### Custom Build Configurations

Although most modern React setups have CSS Modules built-in, if your build tool doesn't support it by default, you'll need to configure it.

Let's assume you have an old Webpack setup. Here's an example of how that'd look like:

```js
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: { modules: true }
}
]
}
```

For other bundlers, refer to their documentation on CSS Modules configuration.

### Deep imports support

Deep imports are supported, you can import directly from path.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
"vite": "^7.3.0",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-externalize-deps": "^0.10.0",
"vite-plugin-static-copy": "^3.2.0",
"vite-tsconfig-paths": "^6.0.5",
"vitest": "^2.1.8",
"watch": "^1.0.2"
Expand Down
1 change: 1 addition & 0 deletions src/components/Button/Button.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.demoCSSModuleSetupOnlyDeleteAfter {}
3 changes: 3 additions & 0 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { styled, keyframes } from 'styled-components';
import { BaseButton } from '../commonElement';
import React from 'react';

import styles from './Button.module.css';

export type ButtonType = 'primary' | 'secondary' | 'empty' | 'danger';
type Alignment = 'center' | 'left';

Expand Down Expand Up @@ -42,6 +44,7 @@ export const Button = ({
...delegated
}: ButtonProps) => (
<StyledButton
className={styles.demoCSSModuleSetupOnlyDeleteAfter}
$styleType={type}
$align={align}
$fillWidth={fillWidth}
Expand Down
86 changes: 28 additions & 58 deletions src/components/DateDetails/DateDetails.test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { DateDetails } from '@/components/DateDetails/DateDetails';
import { renderCUI } from '@/utils/test-utils';
import { fireEvent } from '@testing-library/dom';
import { fireEvent } from '@testing-library/react';

describe('DateDetails', () => {
const actualTZ = process.env.TZ;

beforeAll(() => {
global.ResizeObserver = vi.fn(() => {
return {
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
};
});
global.ResizeObserver = vi.fn(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));

process.env.TZ = 'America/New_York';
});
Expand All @@ -22,11 +20,11 @@ describe('DateDetails', () => {
});

it('renders the DateDetails component with relevant timezone information', () => {
const baseDate = new Date('2024-12-24 11:45:00 AM');
const baseDate = new Date('2024-12-24T11:45:00');
const systemTimeZone = 'America/Los_Angeles';
vi.setSystemTime(baseDate);

const fiveMinutesAgo = new Date('2024-12-24 11:40:00 AM');
const fiveMinutesAgo = new Date('2024-12-24T11:40:00');

const { getByText } = renderCUI(
<DateDetails
Expand All @@ -39,52 +37,36 @@ describe('DateDetails', () => {
expect(trigger).toBeInTheDocument();

fireEvent.click(trigger);
expect(
getByText(content => {
return content.includes('EST');
})
).toBeInTheDocument();
expect(
getByText(content => {
return content.includes('PST');
})
).toBeInTheDocument();

expect(getByText(/Dec 24, 11:40 a\.m\..*(EST|GMT-5)/)).toBeInTheDocument();
expect(getByText(/Dec 24, 8:40 a\.m\..*(PST|GMT-8)/)).toBeInTheDocument();
expect(getByText('Dec 24, 4:40 p.m.')).toBeInTheDocument();
expect(getByText('Dec 24, 11:40 a.m. (EST)')).toBeInTheDocument();
expect(getByText('Dec 24, 8:40 a.m. (PST)')).toBeInTheDocument();
expect(getByText(fiveMinutesAgo.getTime() / 1000)).toBeInTheDocument();
expect(getByText(String(fiveMinutesAgo.getTime() / 1000))).toBeInTheDocument();
});

it('allows for not passing in a system timezone', () => {
const baseDate = new Date('2024-12-24 11:45:00 AM');
const baseDate = new Date('2024-12-24T11:45:00');
vi.setSystemTime(baseDate);

const fiveMinutesAgo = new Date('2024-12-24 11:40:00 AM');
const fiveMinutesAgo = new Date('2024-12-24T11:40:00');

const { getByText, queryByText } = renderCUI(<DateDetails date={fiveMinutesAgo} />);

const trigger = getByText('5 minutes ago');
expect(trigger).toBeInTheDocument();

fireEvent.click(trigger);
expect(
getByText(content => {
return content.includes('EST');
})
).toBeInTheDocument();

expect(getByText(/Dec 24, 11:40 a\.m\..*(EST|GMT-5)/)).toBeInTheDocument();
expect(getByText('Dec 24, 4:40 p.m.')).toBeInTheDocument();
expect(getByText('Dec 24, 11:40 a.m. (EST)')).toBeInTheDocument();
expect(queryByText('System')).not.toBeInTheDocument();
expect(getByText(fiveMinutesAgo.getTime() / 1000)).toBeInTheDocument();
expect(getByText(String(fiveMinutesAgo.getTime() / 1000))).toBeInTheDocument();
});

it("only shows the date if the previous date isn't in this year", () => {
const baseDate = new Date('2025-02-07 11:45:00 AM');
const baseDate = new Date('2025-02-07T11:45:00');
const systemTimeZone = 'America/Los_Angeles';
vi.setSystemTime(baseDate);

const oneYearAgo = new Date('2024-02-07 11:45:00 AM');
const oneYearAgo = new Date('2024-02-07T11:45:00');

const { getByText } = renderCUI(
<DateDetails
Expand All @@ -94,21 +76,20 @@ describe('DateDetails', () => {
);

const trigger = getByText('1 year ago');
expect(trigger).toBeInTheDocument();

fireEvent.click(trigger);

expect(getByText('Feb 7, 2024, 4:45 p.m.')).toBeInTheDocument();
expect(getByText('Feb 7, 2024, 11:45 a.m. (EST)')).toBeInTheDocument();
expect(getByText('Feb 7, 2024, 8:45 a.m. (PST)')).toBeInTheDocument();
expect(getByText(oneYearAgo.getTime() / 1000)).toBeInTheDocument();
expect(getByText(/Feb 7, 2024, 11:45 a\.m\..*(EST|GMT-5)/)).toBeInTheDocument();
expect(getByText(/Feb 7, 2024, 8:45 a\.m\..*(PST|GMT-8)/)).toBeInTheDocument();
expect(getByText(String(oneYearAgo.getTime() / 1000))).toBeInTheDocument();
});

it('handles Daylight Savings Time', () => {
const baseDate = new Date('2024-07-04 11:45:00 AM');
const baseDate = new Date('2024-07-04T11:45:00');
const systemTimeZone = 'America/Los_Angeles';
vi.setSystemTime(baseDate);

const fiveMinutesAgo = new Date('2024-07-04 11:40:00 AM');
const fiveMinutesAgo = new Date('2024-07-04T11:40:00');

const { getByText } = renderCUI(
<DateDetails
Expand All @@ -118,22 +99,11 @@ describe('DateDetails', () => {
);

const trigger = getByText('5 minutes ago');
expect(trigger).toBeInTheDocument();

fireEvent.click(trigger);
expect(
getByText(content => {
return content.includes('EDT');
})
).toBeInTheDocument();
expect(
getByText(content => {
return content.includes('PDT');
})
).toBeInTheDocument();

expect(getByText(/Jul 4, 11:40 a\.m\..*(EDT|GMT-4)/)).toBeInTheDocument();
expect(getByText(/Jul 4, 8:40 a\.m\..*(PDT|GMT-7)/)).toBeInTheDocument();
expect(getByText('Jul 4, 3:40 p.m.')).toBeInTheDocument();
expect(getByText('Jul 4, 11:40 a.m. (EDT)')).toBeInTheDocument();
expect(getByText('Jul 4, 8:40 a.m. (PDT)')).toBeInTheDocument();
expect(getByText(fiveMinutesAgo.getTime() / 1000)).toBeInTheDocument();
expect(getByText(String(fiveMinutesAgo.getTime() / 1000))).toBeInTheDocument();
});
});
40 changes: 40 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@ import dts from 'vite-plugin-dts';
import { externalizeDeps } from 'vite-plugin-externalize-deps';
import tsconfigPaths from 'vite-tsconfig-paths';
import { visualizer } from 'rollup-plugin-visualizer';
import { viteStaticCopy } from 'vite-plugin-static-copy';

const srcDir = path.resolve(__dirname, 'src').replace(/\\/g, '/');

// TODO: Find a solution for static files based on conf extensions
const cssExternalPlugin = () => ({
name: 'css-external',
enforce: 'pre' as const,
resolveId: (id: string) => (id.endsWith('.module.css') ? { id, external: true } : null),
});

const buildOptions: BuildOptions = {
target: 'esnext',
emptyOutDir: true,
Expand Down Expand Up @@ -51,6 +59,7 @@ const buildOptions: BuildOptions = {
const viteConfig = defineConfig({
publicDir: false,
plugins: [
cssExternalPlugin(),
react({
babel: {
plugins: [['babel-plugin-styled-components', { displayName: false }]],
Expand Down Expand Up @@ -85,6 +94,37 @@ const viteConfig = defineConfig({
useFile: path.join(process.cwd(), 'package.json'),
}),
tsconfigPaths(),
// TODO: Copying CSS Module files to both esm and cjs dist directories should have the target names, e.g. esm, cjs shared with bundled target, so that they're automatically sync.
viteStaticCopy({
targets: [
{
src: 'src/**/*.module.css',
dest: 'esm',
rename: (fileName: string, fileExt: string, srcPath: string) => {
const srcIndex = srcPath.indexOf('/src/');
const ext = fileExt.startsWith('.') ? fileExt : `.${fileExt}`;
if (srcIndex !== -1) {
const relativePath = srcPath.slice(srcIndex + 5, srcPath.lastIndexOf('/'));
return `${relativePath}/${fileName}${ext}`;
}
return `${fileName}${ext}`;
},
},
{
src: 'src/**/*.module.css',
dest: 'cjs',
rename: (fileName: string, fileExt: string, srcPath: string) => {
const srcIndex = srcPath.indexOf('/src/');
const ext = fileExt.startsWith('.') ? fileExt : `.${fileExt}`;
if (srcIndex !== -1) {
const relativePath = srcPath.slice(srcIndex + 5, srcPath.lastIndexOf('/'));
return `${relativePath}/${fileName}${ext}`;
}
return `${fileName}${ext}`;
},
},
],
}),
// WARNING: Keep the visualizer last
...(process.env.ANALYZE === 'true'
? [
Expand Down
Loading
Loading