Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
10503ae
WIP feat(checkbox)/adds checkbox component in ui with story and unit …
sachin-thakur-bruno Aug 3, 2026
b57a055
updated nord border color
sachin-thakur-bruno Aug 3, 2026
1a70b70
updated check icon path
sachin-thakur-bruno Aug 3, 2026
c4e3c81
added indeterminate prop and story for it
sachin-thakur-bruno Aug 4, 2026
00e3412
removed default test id
sachin-thakur-bruno Aug 4, 2026
76be3fe
updated radius
sachin-thakur-bruno Aug 4, 2026
ef81c5d
memoize the refs
sachin-thakur-bruno Aug 4, 2026
74fd445
Merge remote-tracking branch 'upstream/main' into chore/checkbox-ui
sachin-thakur-bruno Aug 4, 2026
0aa7043
updated tests
sachin-thakur-bruno Aug 4, 2026
5cce5dd
Merge branch 'main' into chore/checkbox-ui
sachin-thakur-bruno Aug 5, 2026
d290395
Merge remote-tracking branch 'upstream/main' into chore/checkbox-ui
sachin-thakur-bruno Aug 12, 2026
aa97b28
Merge branch 'chore/checkbox-ui' of github.com:sachin-thakur-bruno/br…
sachin-thakur-bruno Aug 12, 2026
4efb610
used use layout effect
sachin-thakur-bruno Aug 12, 2026
994e47e
removed bprder 3
sachin-thakur-bruno Aug 24, 2026
d35525e
Merge remote-tracking branch 'upstream/main' into chore/checkbox-ui
sachin-thakur-bruno Aug 24, 2026
9659b61
added border 3 for dark and light themes
sachin-thakur-bruno Aug 24, 2026
cbef029
Merge remote-tracking branch 'upstream/main' into chore/checkbox-ui
sachin-thakur-bruno Aug 26, 2026
9d8779d
Merge remote-tracking branch 'upstream/main' into chore/checkbox-ui
sachin-thakur-bruno Aug 26, 2026
2de1bc2
updated intermediate
sachin-thakur-bruno Aug 26, 2026
36378a0
Merge remote-tracking branch 'upstream/main' into chore/checkbox-ui
sachin-thakur-bruno Aug 28, 2026
6e83fd6
label size based on checkbox size
sachin-thakur-bruno Aug 28, 2026
169641c
remove spreading rest props
sachin-thakur-bruno Aug 31, 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
5 changes: 3 additions & 2 deletions packages/bruno-app/src/themes/DesignSystem/Overview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ export const IntroductionRender = () => {
{ name: 'Primary Colors', desc: '4 variants for brand identity', color: palette.primary.SOLID },
{ name: 'Backgrounds', desc: '6 layered surface colors', color: theme.background.mantle },
{ name: 'Text', desc: '8 semantic text colors', color: theme.text },
{ name: 'Borders', desc: '3 hierarchy levels', color: theme.border.border2 },
{ name: 'Borders', desc: '4 hierarchy levels', color: theme.border.border2 },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add border3 to the Border Hierarchy preview.

The overview now states that four border levels exist and lists border3, but the Border Hierarchy preview still renders only border0, border1, and border2. Add a border3 row so both sections show the same hierarchy.

Also applies to: 1309-1310

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bruno-app/src/themes/DesignSystem/Overview.jsx` at line 286, Update
the Border Hierarchy preview data in Overview.jsx to add a border3 row alongside
border0, border1, and border2, using the existing row shape and
theme.border.border3 color so the preview displays all four hierarchy levels.

{ name: 'Overlays', desc: '3 depth levels', color: theme.overlay.overlay1 },
{ name: 'Hues', desc: '14 hue-spread colors', color: palette.hues.BLUE }
].map(({ name, desc, color }) => (
Expand Down Expand Up @@ -1306,7 +1306,8 @@ export const BordersAndOverlaysRender = () => {
{[
{ name: 'border0', color: theme.border.border0, desc: 'Subtle separations, card outlines' },
{ name: 'border1', color: theme.border.border1, desc: 'Standard dividers, input borders' },
{ name: 'border2', color: theme.border.border2, desc: 'Focus states, selected items' }
{ name: 'border2', color: theme.border.border2, desc: 'Focus states, selected items' },
{ name: 'border3', color: theme.border.border3, desc: 'Stronger outlines, e.g. unchecked checkboxes' }
].map(({ name, color, desc }) => (
<div
key={name}
Expand Down
1 change: 1 addition & 0 deletions packages/bruno-app/src/themes/dark/catppuccin-frappe.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ const catppuccinFrappeTheme = {
lg: '10px',
xl: '12px'
},
border3: colors.OVERLAY0,
border2: colors.SURFACE2,
border1: colors.SURFACE1,
border0: colors.SURFACE0
Expand Down
1 change: 1 addition & 0 deletions packages/bruno-app/src/themes/dark/catppuccin-macchiato.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ const catppuccinMacchiatoTheme = {
lg: '10px',
xl: '12px'
},
border3: colors.OVERLAY0,
border2: colors.SURFACE2,
border1: colors.SURFACE1,
border0: colors.SURFACE0
Expand Down
1 change: 1 addition & 0 deletions packages/bruno-app/src/themes/dark/catppuccin-mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ const catppuccinMochaTheme = {
lg: '10px',
xl: '12px'
},
border3: colors.OVERLAY0,
border2: colors.SURFACE2,
border1: colors.SURFACE1,
border0: colors.SURFACE0
Expand Down
1 change: 1 addition & 0 deletions packages/bruno-app/src/themes/dark/dark-monochrome.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ const darkMonochromeTheme = {
lg: '10px',
xl: '12px'
},
border3: colors.GRAY_5,
border2: colors.GRAY_4,
border1: colors.GRAY_3,
border0: colors.GRAY_2
Expand Down
1 change: 1 addition & 0 deletions packages/bruno-app/src/themes/dark/dark-pastel.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ const darkPastelTheme = {
lg: '10px',
xl: '12px'
},
border3: colors.GRAY_6,
border2: colors.GRAY_5,
border1: colors.GRAY_4,
border0: colors.GRAY_3
Expand Down
2 changes: 2 additions & 0 deletions packages/bruno-app/src/themes/dark/dark.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const palette = {
OVERLAY0: '#444444'
},
border: {
BORDER3: '#989898',
BORDER2: '#444444',
BORDER1: '#333333',
BORDER0: '#2a2a2a'
Expand Down Expand Up @@ -180,6 +181,7 @@ const darkTheme = {
lg: '10px',
xl: '12px'
},
border3: palette.border.BORDER3,
border2: palette.border.BORDER2,
border1: palette.border.BORDER1,
border0: palette.border.BORDER0
Expand Down
3 changes: 2 additions & 1 deletion packages/bruno-app/src/themes/dark/nord.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// Frost: nord7-nord10 (#8fbcbb, #88c0d0, #81a1c1, #5e81ac)
// Aurora: nord11-nord15 (#bf616a, #d08770, #ebcb8b, #a3be8c, #b48ead)

import { rgba } from 'polished';
import { rgba, lighten } from 'polished';

const colors = {
// Polar Night
Expand Down Expand Up @@ -155,6 +155,7 @@ const nordTheme = {
lg: '10px',
xl: '12px'
},
border3: lighten(0.15, colors.NORD3),
border2: colors.NORD3,
border1: colors.NORD2,
border0: colors.NORD1
Expand Down
1 change: 1 addition & 0 deletions packages/bruno-app/src/themes/dark/vscode.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ const vscodeDarkTheme = {
lg: '10px',
xl: '12px'
},
border3: colors.GRAY_6,
border2: colors.GRAY_5,
border1: colors.BORDER,
border0: colors.BORDER_LIGHT
Expand Down
1 change: 1 addition & 0 deletions packages/bruno-app/src/themes/light/catppuccin-latte.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ const catppuccinLatteTheme = {
lg: '10px',
xl: '12px'
},
border3: colors.OVERLAY0,
border2: colors.SURFACE2,
border1: colors.SURFACE1,
border0: colors.SURFACE0
Expand Down
1 change: 1 addition & 0 deletions packages/bruno-app/src/themes/light/light-monochrome.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ const lightMonochromeTheme = {
lg: '10px',
xl: '12px'
},
border3: colors.GRAY_6,
border2: colors.GRAY_5,
border1: colors.GRAY_4,
border0: colors.GRAY_3
Expand Down
1 change: 1 addition & 0 deletions packages/bruno-app/src/themes/light/light-pastel.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ const lightPastelTheme = {
lg: '10px',
xl: '12px'
},
border3: colors.GRAY_5,
border2: colors.GRAY_4,
border1: colors.GRAY_3,
border0: colors.GRAY_2
Expand Down
2 changes: 2 additions & 0 deletions packages/bruno-app/src/themes/light/light.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const palette = {
OVERLAY0: '#C0C0C0'
},
border: {
BORDER3: '#B1B1B1',
BORDER2: '#cccccc',
BORDER1: '#e5e5e5',
BORDER0: '#efefef'
Expand Down Expand Up @@ -166,6 +167,7 @@ const lightTheme = {
lg: '10px',
xl: '12px'
},
border3: palette.border.BORDER3,
border2: palette.border.BORDER2,
border1: palette.border.BORDER1,
border0: palette.border.BORDER0
Expand Down
1 change: 1 addition & 0 deletions packages/bruno-app/src/themes/light/vscode.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ const vscodeLightTheme = {
lg: '10px',
xl: '12px'
},
border3: colors.GRAY_5,
border2: colors.GRAY_4,
border1: colors.BORDER,
border0: colors.GRAY_2
Expand Down
3 changes: 2 additions & 1 deletion packages/bruno-app/src/themes/schema/oss.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,12 @@ export const ossSchema = {
required: ['sm', 'base', 'md', 'lg', 'xl'],
additionalProperties: false
},
border3: { type: 'string' },
border2: { type: 'string' },
border1: { type: 'string' },
border0: { type: 'string' }
},
required: ['radius', 'border2', 'border1', 'border0'],
required: ['radius', 'border3', 'border2', 'border1', 'border0'],
additionalProperties: false
},

Expand Down
187 changes: 187 additions & 0 deletions packages/bruno-app/src/ui/Checkbox/Checkbox.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import '@testing-library/jest-dom';
import React, { createRef, useState } from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeProvider } from 'styled-components';
import Checkbox from './index';

const theme = {
mode: 'light',
text: '#343434',
border: { border3: '#B1B1B1' },
primary: { solid: '#D37F17' },
button2: { color: { primary: { text: '#ffffff' } } },
font: { size: { sm: '0.75rem' } }
};

const renderWithTheme = (ui) => render(<ThemeProvider theme={theme}>{ui}</ThemeProvider>);

const renderCheckbox = (props = {}) => {
const hasAccessibleName = 'label' in props || 'ariaLabel' in props || 'ariaLabelledBy' in props;
return renderWithTheme(
<Checkbox onChange={() => {}} {...(hasAccessibleName ? {} : { ariaLabel: 'Checkbox' })} {...props} />
);
};

describe('Checkbox', () => {
let user;

beforeEach(() => {
user = userEvent.setup();
});

it('renders unchecked by default', () => {
renderCheckbox();
expect(screen.getByRole('checkbox')).not.toBeChecked();
});

it('reflects the checked prop', () => {
renderCheckbox({ checked: true });
expect(screen.getByRole('checkbox')).toBeChecked();
});

it('calls onChange when clicked', async () => {
const onChange = jest.fn();
renderCheckbox({ onChange });

await user.click(screen.getByRole('checkbox'));

expect(onChange).toHaveBeenCalledTimes(1);
});

it('does not call onChange when disabled', async () => {
const onChange = jest.fn();
renderCheckbox({ onChange, disabled: true });

const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeDisabled();
await user.click(checkbox);

expect(onChange).not.toHaveBeenCalled();
});

it('updates when driven as a controlled component', async () => {
const Controlled = () => {
const [checked, setChecked] = useState(false);
return <Checkbox checked={checked} onChange={(e) => setChecked(e.target.checked)} />;
};
renderWithTheme(<Controlled />);

const checkbox = screen.getByRole('checkbox');
expect(checkbox).not.toBeChecked();

await user.click(checkbox);
expect(checkbox).toBeChecked();

await user.click(checkbox);
expect(checkbox).not.toBeChecked();
});

it('renders the optional label and associates it with the input', () => {
renderCheckbox({ label: 'Accept terms' });

expect(screen.getByText('Accept terms')).toBeInTheDocument();
expect(screen.getByRole('checkbox', { name: 'Accept terms' })).toBeInTheDocument();
});

it('toggles via keyboard (space)', async () => {
const onChange = jest.fn();
renderCheckbox({ onChange });

const checkbox = screen.getByRole('checkbox');
checkbox.focus();
await user.keyboard(' ');

expect(onChange).toHaveBeenCalledTimes(1);
});

it('applies the data-testid to the input', () => {
renderCheckbox({ 'data-testid': 'my-checkbox' });
expect(screen.getByTestId('my-checkbox')).toBe(screen.getByRole('checkbox'));
});

it('forwards the ref to the input element', () => {
const ref = createRef();
renderCheckbox({ ref });

expect(ref.current).toBe(screen.getByRole('checkbox'));
});

it('forwards additional props to the input', () => {
renderCheckbox({ required: true, name: 'terms' });

const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeRequired();
expect(checkbox).toHaveAttribute('name', 'terms');
});

describe('indeterminate', () => {
it('sets the DOM indeterminate property when the prop is true', () => {
renderCheckbox({ indeterminate: true });
expect(screen.getByRole('checkbox').indeterminate).toBe(true);
});

it('does not set the DOM indeterminate property by default', () => {
renderCheckbox();
expect(screen.getByRole('checkbox').indeterminate).toBe(false);
});

it('clears the DOM indeterminate property when the prop turns false', () => {
const { rerender } = renderCheckbox({ indeterminate: true });
expect(screen.getByRole('checkbox').indeterminate).toBe(true);

rerender(
<ThemeProvider theme={theme}>
<Checkbox onChange={() => {}} ariaLabel="Checkbox" indeterminate={false} />
</ThemeProvider>
);

expect(screen.getByRole('checkbox').indeterminate).toBe(false);
});

it('re-applies the DOM property on re-render even though the browser clears it on click', async () => {
const onChange = jest.fn();
const { rerender } = renderCheckbox({ indeterminate: true, onChange });

const checkbox = screen.getByRole('checkbox');
await user.click(checkbox);
// Clicking a native checkbox always resets `indeterminate` to false in the browser,
// regardless of what the app does with the click. this is the behavior our effect
// (re-applied on every render, no dependency array) must correct for.
checkbox.indeterminate = false;

rerender(
<ThemeProvider theme={theme}>
<Checkbox onChange={onChange} ariaLabel="Checkbox" indeterminate={true} />
</ThemeProvider>
);

expect(checkbox.indeterminate).toBe(true);
});

it('still forwards the ref to the input element when indeterminate', () => {
const ref = createRef();
renderCheckbox({ indeterminate: true, ref });

expect(ref.current).toBe(screen.getByRole('checkbox'));
expect(ref.current.indeterminate).toBe(true);
});
});

describe('accessible name', () => {
it('is derived from ariaLabel when there is no visible label', () => {
renderCheckbox({ ariaLabel: 'Select all' });
expect(screen.getByRole('checkbox', { name: 'Select all' })).toBeInTheDocument();
});

it('is derived from an external element via ariaLabelledBy', () => {
renderWithTheme(
<>
<span id="ext-label">Select all</span>
<Checkbox onChange={() => {}} ariaLabelledBy="ext-label" />
</>
);
expect(screen.getByRole('checkbox', { name: 'Select all' })).toBeInTheDocument();
});
});
});
Loading
Loading