diff --git a/.changeset/migrate-generic-label-to-css-modules.md b/.changeset/migrate-generic-label-to-css-modules.md
new file mode 100644
index 000000000..05b33afb2
--- /dev/null
+++ b/.changeset/migrate-generic-label-to-css-modules.md
@@ -0,0 +1,5 @@
+---
+'@clickhouse/click-ui': patch
+---
+
+Migrate GenericLabel from styled-components to css modules with no change in behavior
diff --git a/src/components/GenericLabel/GenericLabel.module.css b/src/components/GenericLabel/GenericLabel.module.css
new file mode 100644
index 000000000..dbc7c6cee
--- /dev/null
+++ b/src/components/GenericLabel/GenericLabel.module.css
@@ -0,0 +1,31 @@
+/* stylelint-disable custom-property-pattern -- design tokens use camelCase (e.g. genericLabel) */
+
+.generic-label {
+ color: var(--click-field-color-genericLabel-default);
+ font: var(--click-field-typography-genericLabel-default);
+ cursor: pointer;
+}
+
+.generic-label:hover {
+ color: var(--click-field-color-genericLabel-hover);
+ font: var(--click-field-typography-genericLabel-hover);
+}
+
+.generic-label:focus,
+.generic-label:focus-within {
+ color: var(--click-field-color-genericLabel-active);
+ font: var(--click-field-typography-genericLabel-active);
+}
+
+/* stylelint-disable no-descending-specificity -- disabled state intentionally
+ defined after hover/focus to neutralize them; the original styled-components
+ emits no hover/focus rules when disabled is true. */
+.generic-label.generic-label_disabled,
+.generic-label.generic-label_disabled:hover,
+.generic-label.generic-label_disabled:focus,
+.generic-label.generic-label_disabled:focus-within {
+ color: var(--click-field-color-genericLabel-disabled);
+ font: var(--click-field-typography-genericLabel-disabled);
+ cursor: not-allowed;
+}
+/* stylelint-enable no-descending-specificity */
diff --git a/src/components/GenericLabel/GenericLabel.stories.tsx b/src/components/GenericLabel/GenericLabel.stories.tsx
index e5dc3917c..777ee131a 100644
--- a/src/components/GenericLabel/GenericLabel.stories.tsx
+++ b/src/components/GenericLabel/GenericLabel.stories.tsx
@@ -23,3 +23,31 @@ export const Playground: Story = {
),
};
+
+export const Default: Story = {
+ render: () => (
+
+ Form Field generic label
+
+
+ ),
+};
+
+export const Disabled: Story = {
+ render: () => (
+
+ Form Field generic label
+
+
+ ),
+};
diff --git a/src/components/GenericLabel/GenericLabel.tsx b/src/components/GenericLabel/GenericLabel.tsx
index a07481436..8069807f7 100644
--- a/src/components/GenericLabel/GenericLabel.tsx
+++ b/src/components/GenericLabel/GenericLabel.tsx
@@ -1,42 +1,25 @@
-import { styled } from 'styled-components';
+import { cn, cva } from '@/lib/cva';
import { GenericLabelProps } from './GenericLabel.types';
+import styles from './GenericLabel.module.css';
-interface FormFieldLableProps {
- disabled?: boolean;
- htmlFor?: string;
-}
+const genericLabelVariants = cva(styles['generic-label'], {
+ variants: {
+ disabled: {
+ true: styles['generic-label_disabled'],
+ },
+ },
+});
-const FormFieldLabel = styled.label`
- ${({ theme, disabled }) => `
- ${
- disabled
- ? `
- color: ${theme.click.field.color.genericLabel.disabled};
- font: ${theme.click.field.typography.genericLabel.disabled};
- cursor: not-allowed;
- `
- : `
- cursor: pointer;
- color: ${theme.click.field.color.genericLabel.default};
- font: ${theme.click.field.typography.genericLabel.default};
- &:hover {
- color: ${theme.click.field.color.genericLabel.hover};
- font: ${theme.click.field.typography.genericLabel.hover};
- }
- &:focus, &:focus-within {
- color: ${theme.click.field.color.genericLabel.active};
- font: ${theme.click.field.typography.genericLabel.active};
- }
- `
- };
- `}
-`;
-
-export const GenericLabel = ({ disabled, children, ...props }: GenericLabelProps) => (
- (
+
+
);
diff --git a/tests/display/generic-label.spec.ts b/tests/display/generic-label.spec.ts
new file mode 100644
index 000000000..ee951c0e6
--- /dev/null
+++ b/tests/display/generic-label.spec.ts
@@ -0,0 +1,120 @@
+import { test as it, expect } from '@playwright/test';
+import { getStoryUrl } from '../utils';
+
+const { describe, use } = it;
+
+const labelLocator = 'label';
+
+describe('GenericLabel Visual Regression', () => {
+ describe('Light Theme (Storybook Global)', () => {
+ describe('States', () => {
+ it('default matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('forms-genericlabel--default', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const label = page.locator(labelLocator).first();
+ await expect(label).toBeVisible({ timeout: 10000 });
+ await expect(label).toHaveScreenshot('generic-label-default-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('disabled matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('forms-genericlabel--disabled', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const label = page.locator(labelLocator).first();
+ await expect(label).toBeVisible({ timeout: 10000 });
+ await expect(label).toHaveScreenshot('generic-label-disabled-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+ });
+
+ describe('Interactive States', () => {
+ it('hover state matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('forms-genericlabel--default', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const label = page.locator(labelLocator).first();
+ await expect(label).toBeVisible({ timeout: 10000 });
+ await label.hover();
+ await page.waitForTimeout(100);
+ await expect(label).toHaveScreenshot('generic-label-hover-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('focus-within state matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('forms-genericlabel--default', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ await page.locator('body').click();
+ const label = page.locator(labelLocator).first();
+ await expect(label).toBeVisible({ timeout: 10000 });
+ await page.locator('#default-input').focus();
+ await page.waitForTimeout(100);
+ await expect(label).toHaveScreenshot('generic-label-focus-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+ });
+ });
+
+ describe('Dark Theme (System prefers-color-scheme)', () => {
+ use({ colorScheme: 'dark' });
+
+ describe('States', () => {
+ it('default matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('forms-genericlabel--default'), {
+ waitUntil: 'networkidle',
+ });
+ const label = page.locator(labelLocator).first();
+ await expect(label).toBeVisible({ timeout: 10000 });
+ await expect(label).toHaveScreenshot('generic-label-default-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('disabled matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('forms-genericlabel--disabled'), {
+ waitUntil: 'networkidle',
+ });
+ const label = page.locator(labelLocator).first();
+ await expect(label).toBeVisible({ timeout: 10000 });
+ await expect(label).toHaveScreenshot('generic-label-disabled-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+ });
+
+ describe('Interactive States', () => {
+ it('hover state matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('forms-genericlabel--default'), {
+ waitUntil: 'networkidle',
+ });
+ const label = page.locator(labelLocator).first();
+ await expect(label).toBeVisible({ timeout: 10000 });
+ await label.hover();
+ await page.waitForTimeout(100);
+ await expect(label).toHaveScreenshot('generic-label-hover-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('focus-within state matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('forms-genericlabel--default'), {
+ waitUntil: 'networkidle',
+ });
+ await page.locator('body').click();
+ const label = page.locator(labelLocator).first();
+ await expect(label).toBeVisible({ timeout: 10000 });
+ await page.locator('#default-input').focus();
+ await page.waitForTimeout(100);
+ await expect(label).toHaveScreenshot('generic-label-focus-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+ });
+ });
+});
diff --git a/tests/display/generic-label.spec.ts-snapshots/generic-label-default-dark-chromium-linux.png b/tests/display/generic-label.spec.ts-snapshots/generic-label-default-dark-chromium-linux.png
new file mode 100644
index 000000000..f1904b765
Binary files /dev/null and b/tests/display/generic-label.spec.ts-snapshots/generic-label-default-dark-chromium-linux.png differ
diff --git a/tests/display/generic-label.spec.ts-snapshots/generic-label-default-light-chromium-linux.png b/tests/display/generic-label.spec.ts-snapshots/generic-label-default-light-chromium-linux.png
new file mode 100644
index 000000000..e6fcb740e
Binary files /dev/null and b/tests/display/generic-label.spec.ts-snapshots/generic-label-default-light-chromium-linux.png differ
diff --git a/tests/display/generic-label.spec.ts-snapshots/generic-label-disabled-dark-chromium-linux.png b/tests/display/generic-label.spec.ts-snapshots/generic-label-disabled-dark-chromium-linux.png
new file mode 100644
index 000000000..aa71f2947
Binary files /dev/null and b/tests/display/generic-label.spec.ts-snapshots/generic-label-disabled-dark-chromium-linux.png differ
diff --git a/tests/display/generic-label.spec.ts-snapshots/generic-label-disabled-light-chromium-linux.png b/tests/display/generic-label.spec.ts-snapshots/generic-label-disabled-light-chromium-linux.png
new file mode 100644
index 000000000..93b2c6cf8
Binary files /dev/null and b/tests/display/generic-label.spec.ts-snapshots/generic-label-disabled-light-chromium-linux.png differ
diff --git a/tests/display/generic-label.spec.ts-snapshots/generic-label-focus-dark-chromium-linux.png b/tests/display/generic-label.spec.ts-snapshots/generic-label-focus-dark-chromium-linux.png
new file mode 100644
index 000000000..f4ee03f6c
Binary files /dev/null and b/tests/display/generic-label.spec.ts-snapshots/generic-label-focus-dark-chromium-linux.png differ
diff --git a/tests/display/generic-label.spec.ts-snapshots/generic-label-focus-light-chromium-linux.png b/tests/display/generic-label.spec.ts-snapshots/generic-label-focus-light-chromium-linux.png
new file mode 100644
index 000000000..4395e82dd
Binary files /dev/null and b/tests/display/generic-label.spec.ts-snapshots/generic-label-focus-light-chromium-linux.png differ
diff --git a/tests/display/generic-label.spec.ts-snapshots/generic-label-hover-dark-chromium-linux.png b/tests/display/generic-label.spec.ts-snapshots/generic-label-hover-dark-chromium-linux.png
new file mode 100644
index 000000000..18fdfefb1
Binary files /dev/null and b/tests/display/generic-label.spec.ts-snapshots/generic-label-hover-dark-chromium-linux.png differ
diff --git a/tests/display/generic-label.spec.ts-snapshots/generic-label-hover-light-chromium-linux.png b/tests/display/generic-label.spec.ts-snapshots/generic-label-hover-light-chromium-linux.png
new file mode 100644
index 000000000..7d9a6e240
Binary files /dev/null and b/tests/display/generic-label.spec.ts-snapshots/generic-label-hover-light-chromium-linux.png differ