Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 5 additions & 0 deletions .changeset/frank-wings-strive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shipfox/react-ui": minor
---

Add checkbox and label components
81 changes: 81 additions & 0 deletions libs/react/ui/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,36 @@
--shadow-button-danger-focus:
0 -1px 0 0 var(--color-alpha-white-16), 0 0 0 1px var(--color-alpha-white-12), 0 0 0 1px
var(--color-red-700), 0 0 0 2px var(--color-alpha-white-88), 0 0 0 4px var(--color-primary-500);

/* Checkbox */
/* Checkbox - Unchecked States */
--checkbox-unchecked-bg: var(--background-components-base);
--checkbox-unchecked-bg-hover: var(--background-components-hover);
--checkbox-unchecked-border: var(--color-alpha-black-8);
--checkbox-unchecked-shadow:
0px 1px 2px 0px var(--color-alpha-black-12), 0px 0px 0px 1px var(--color-alpha-black-8);
--checkbox-unchecked-focus-shadow:
0px 1px 2px 0px rgba(30, 58, 138, 0.5), 0px 0px 0px 1px #ff4b00, 0px 0px 0px 2px
var(--color-neutral-0), 0px 0px 0px 4px rgba(255, 75, 0, 0.6);

/* Checkbox - Checked States */
--checkbox-checked-bg: var(--foreground-highlight-interactive);
--checkbox-checked-bg-hover: var(--foreground-highlight-interactive-hover);
--checkbox-checked-border: rgba(255, 75, 0, 0.69);
--checkbox-checked-shadow:
0px 1px 2px 0px rgba(30, 58, 138, 0.5), 0px 0px 0px 1px rgba(255, 75, 0, 0.69);
--checkbox-checked-focus-shadow:
0px 1px 2px 0px rgba(30, 58, 138, 0.5), 0px 0px 0px 1px #ff4b00, 0px 0px 0px 2px
var(--color-neutral-0), 0px 0px 0px 4px rgba(255, 75, 0, 0.6);

/* Checkbox - Indeterminate States */
--checkbox-indeterminate-bg: var(--foreground-highlight-interactive);
--checkbox-indeterminate-bg-hover: var(--foreground-highlight-interactive-hover);
--checkbox-indeterminate-border: rgba(255, 75, 0, 0.69);
--checkbox-indeterminate-shadow:
0px 1px 2px 0px rgba(30, 58, 138, 0.5), 0px 0px 0px 1px rgba(255, 75, 0, 0.69);
--checkbox-indeterminate-focus-shadow:
0px 0px 0px 1px var(--color-neutral-0), 0px 0px 0px 3px rgba(255, 75, 0, 0.6);
--shadow-tooltip:
0 0 0 1px var(--color-alpha-black-8), 0 2px 4px 0 var(--color-alpha-black-8), 0 4px 8px 0
var(--color-alpha-black-8);
Expand Down Expand Up @@ -482,6 +512,40 @@
--shadow-button-danger-focus:
0 -1px 0 0 var(--color-alpha-white-16), 0 0 0 1px var(--color-alpha-white-12), 0 0 0 1px
var(--color-red-700), 0 0 0 2px var(--color-neutral-950), 0 0 0 4px var(--color-primary-500);

/* Checkbox */
/* Checkbox - Unchecked States */
--checkbox-unchecked-bg: var(--background-components-base);
--checkbox-unchecked-bg-hover: var(--background-components-hover);
--checkbox-unchecked-border: var(--color-alpha-white-10);
--checkbox-unchecked-shadow:
0px 1px 2px 0px var(--color-alpha-black-12), 0px 0px 0px 1px var(--color-alpha-white-10);
--checkbox-unchecked-focus-shadow:
0px 1px 2px 0px rgba(30, 58, 138, 0.5), 0px 0px 0px 1px #ff4b00, 0px 0px 0px 2px var(
--color-alpha-black-80
),
0px 0px 0px 4px rgba(255, 75, 0, 0.6);

/* Checkbox - Checked States */
--checkbox-checked-bg: var(--foreground-highlight-interactive);
--checkbox-checked-bg-hover: var(--foreground-highlight-interactive-hover);
--checkbox-checked-border: rgba(255, 75, 0, 0.69);
--checkbox-checked-shadow:
0px 1px 2px 0px rgba(30, 58, 138, 0.5), 0px 0px 0px 1px rgba(255, 75, 0, 0.69);
--checkbox-checked-focus-shadow:
0px 1px 2px 0px rgba(30, 58, 138, 0.5), 0px 0px 0px 1px #ff4b00, 0px 0px 0px 2px var(
--color-alpha-black-80
),
0px 0px 0px 4px rgba(255, 75, 0, 0.6);

/* Checkbox - Indeterminate States */
--checkbox-indeterminate-bg: var(--foreground-highlight-interactive);
--checkbox-indeterminate-bg-hover: var(--foreground-highlight-interactive-hover);
--checkbox-indeterminate-border: rgba(255, 75, 0, 0.69);
--checkbox-indeterminate-shadow:
0px 1px 2px 0px rgba(30, 58, 138, 0.5), 0px 0px 0px 1px rgba(255, 75, 0, 0.69);
--checkbox-indeterminate-focus-shadow:
0px 0px 0px 1px var(--color-alpha-black-80), 0px 0px 0px 3px rgba(255, 75, 0, 0.6);
--shadow-tooltip:
0 -1px 0 0 var(--color-alpha-white-4), 0 2px 4px 0 var(--color-alpha-black-40), 0 0 0 1px
var(--color-alpha-white-10), 0 4px 8px 0 var(--color-alpha-black-40);
Expand Down Expand Up @@ -808,6 +872,23 @@
--shadow-button-neutral-focus: var(--shadow-button-neutral-focus);
--shadow-button-danger: var(--shadow-button-danger);
--shadow-button-danger-focus: var(--shadow-button-danger-focus);

/* Checkbox */
--color-checkbox-unchecked-bg: var(--checkbox-unchecked-bg);
--color-checkbox-unchecked-bg-hover: var(--checkbox-unchecked-bg-hover);
--color-checkbox-unchecked-border: var(--checkbox-unchecked-border);
--shadow-checkbox-unchecked: var(--checkbox-unchecked-shadow);
--shadow-checkbox-unchecked-focus: var(--checkbox-unchecked-focus-shadow);
--color-checkbox-checked-bg: var(--checkbox-checked-bg);
--color-checkbox-checked-bg-hover: var(--checkbox-checked-bg-hover);
--color-checkbox-checked-border: var(--checkbox-checked-border);
--shadow-checkbox-checked: var(--checkbox-checked-shadow);
--shadow-checkbox-checked-focus: var(--checkbox-checked-focus-shadow);
--color-checkbox-indeterminate-bg: var(--checkbox-indeterminate-bg);
--color-checkbox-indeterminate-bg-hover: var(--checkbox-indeterminate-bg-hover);
--color-checkbox-indeterminate-border: var(--checkbox-indeterminate-border);
--shadow-checkbox-indeterminate: var(--checkbox-indeterminate-shadow);
--shadow-checkbox-indeterminate-focus: var(--checkbox-indeterminate-focus-shadow);
--shadow-tooltip: var(--shadow-tooltip);
}

Expand Down
125 changes: 125 additions & 0 deletions libs/react/ui/src/components/checkbox/checkbox-label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {useId} from 'react';
import {cn} from 'utils/cn';
import {Icon} from '../icon/icon';
import {Label} from '../label/label';
import {Checkbox, type CheckboxProps} from './checkbox';

export type CheckboxLabelProps = Omit<CheckboxProps, 'id'> & {
id?: string;
label: string;
optional?: boolean;
description?: string;
showInfoIcon?: boolean;
border?: boolean;
className?: string;
labelClassName?: string;
descriptionClassName?: string;
};

export function CheckboxLabel({
id,
label,
optional = false,
description,
showInfoIcon = false,
border = false,
className,
labelClassName,
descriptionClassName,
...checkboxProps
}: CheckboxLabelProps) {
const generateId = useId();
const checkboxId = id || generateId;
const isDisabled = checkboxProps.disabled ?? false;

const renderContent = (checkboxId: string) => (
<div className="flex flex-col gap-4 flex-1 min-w-0">
<div className="flex gap-4 items-center">
<Label
className={cn(
'text-sm leading-20 overflow-hidden text-ellipsis whitespace-nowrap',
isDisabled
? 'font-normal text-foreground-neutral-subtle'
: 'font-medium text-foreground-neutral-base',
labelClassName,
)}
htmlFor={checkboxId}
>
{label}
</Label>
{optional && (
<span className="text-sm leading-20 font-regular text-foreground-neutral-muted whitespace-nowrap">
(Optional)
</span>
)}
{showInfoIcon && (
<Icon
name="info"
className="size-16 text-foreground-neutral-muted shrink-0"
aria-hidden="true"
/>
)}
</div>
{description && (
<p
className={cn(
'text-sm leading-20',
isDisabled ? 'text-foreground-neutral-disabled' : 'text-foreground-neutral-subtle',
descriptionClassName,
)}
>
{description}
</p>
)}
</div>
);

if (border) {
return (
<Label
htmlFor={checkboxId}
className={cn(
// Base container styles with border
'flex items-start gap-10 rounded-8 p-8 transition-all duration-100',
// Unchecked state - default
'bg-checkbox-unchecked-bg shadow-checkbox-unchecked',
// Unchecked state - hover
'hover:bg-checkbox-unchecked-bg-hover',
// Unchecked state - focus
'has-data-[state=unchecked]:focus-visible:shadow-border-interactive-with-active',
// Checked state - default
'has-data-[state=checked]:bg-background-neutral-base has-data-[state=checked]:shadow-checkbox-checked',
// Checked state - hover
'has-data-[state=checked]:hover:bg-background-neutral-hover',
// Checked state - focus
'has-data-[state=checked]:focus-visible:shadow-checkbox-checked-focus',
// Indeterminate state - default
'has-data-[state=indeterminate]:bg-background-neutral-base has-data-[state=indeterminate]:shadow-checkbox-indeterminate',
// Indeterminate state - hover
'has-data-[state=indeterminate]:hover:bg-background-neutral-hover',
// Indeterminate state - focus
'has-data-[state=indeterminate]:focus-visible:shadow-checkbox-indeterminate-focus',
// Disabled state
isDisabled && 'opacity-50 cursor-not-allowed',
!isDisabled && 'cursor-pointer',
className,
)}
>
<span className="p-4">
<Checkbox id={checkboxId} {...checkboxProps} />
</span>
{renderContent(checkboxId)}
</Label>
);
}

// Without border variant
return (
<div className={cn('flex items-start gap-10', className)}>
<span className="p-2">
<Checkbox id={checkboxId} {...checkboxProps} />
</span>
{renderContent(checkboxId)}
</div>
);
}
90 changes: 90 additions & 0 deletions libs/react/ui/src/components/checkbox/checkbox-links.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {Label} from 'components/label';
import {type ReactNode, useId} from 'react';
import {cn} from 'utils/cn';
import {Checkbox, type CheckboxProps} from './checkbox';

export type CheckboxLink = {
label: string;
href?: string;
onClick?: () => void;
};

export type CheckboxLinksProps = Omit<CheckboxProps, 'id'> & {
id?: string;
label: string;
links: CheckboxLink[];
separator?: ReactNode;
className?: string;
labelClassName?: string;
linkClassName?: string;
};

export function CheckboxLinks({
id,
label,
links,
separator,
className,
labelClassName,
linkClassName,
...checkboxProps
}: CheckboxLinksProps) {
const generateId = useId();
const checkboxId = id || generateId;
const isDisabled = checkboxProps.disabled ?? false;
const defaultSeparator = (
<span className="size-3 rounded-full bg-foreground-neutral-muted" aria-hidden="true" />
);

return (
<div className={cn('flex gap-10 items-start', className)}>
<span className="p-2">
<Checkbox id={checkboxId} {...checkboxProps} />
</span>
<div className="flex flex-col gap-4 items-start flex-1">
<Label
htmlFor={checkboxId}
className={cn(
'text-sm leading-20 font-medium text-foreground-neutral-base',
isDisabled && 'cursor-not-allowed opacity-50',
labelClassName,
)}
>
{label}
</Label>
<div className="flex gap-6 items-center">
{links.map((link, index) => (
<div key={link.label} className="flex gap-6 items-center">
{link.href ? (
<a
href={link.href}
onClick={link.onClick}
className={cn(
'text-sm leading-20 font-medium text-foreground-highlight-interactive',
'hover:text-foreground-highlight-interactive-hover',
linkClassName,
)}
>
{link.label}
</a>
) : (
<button
type="button"
onClick={link.onClick}
className={cn(
'text-sm leading-20 font-medium text-foreground-highlight-interactive',
'hover:text-foreground-highlight-interactive-hover',
linkClassName,
)}
>
{link.label}
</button>
)}
{index < links.length - 1 && (separator ?? defaultSeparator)}
</div>
))}
</div>
</div>
</div>
);
}
Loading