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
12 changes: 12 additions & 0 deletions src/components/Button/Button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ const meta: Meta<typeof Button> = {
description: 'Whether the button has a visible border.',
table: { category: 'Appearance' },
},
corners: {
control: 'select',
options: ['tight', 'normal', 'wide'],
description: 'The corner radius of the button.',
table: { category: 'Appearance' },
},
rounded: {
control: 'boolean',
description: 'Whether the button has rounded ends.',
Expand Down Expand Up @@ -102,6 +108,7 @@ export const Text: Story = {
args: {
border: false,
collapsePadding: false,
corners: 'normal',
disabled: false,
icon: undefined,
iconPosition: undefined,
Expand All @@ -119,6 +126,7 @@ export const TextIcon: Story = {
args: {
border: false,
collapsePadding: false,
corners: 'normal',
disabled: false,
icon: <Plus />,
iconPosition: 'start',
Expand All @@ -136,6 +144,7 @@ export const Icon: Story = {
args: {
border: false,
collapsePadding: false,
corners: 'normal',
disabled: false,
icon: <Mail />,
iconPosition: 'start',
Expand All @@ -152,6 +161,7 @@ export const Link: Story = {
args: {
border: false,
collapsePadding: false,
corners: 'normal',
href: 'https://example.com',
intent: 'secondary',
rounded: false,
Expand All @@ -170,6 +180,7 @@ export const VisualComparison: Story = {
iconPosition: 'start',
size: 'normal',
collapsePadding: false,
corners: 'normal',
loading: false,
disabled: false,
rounded: false,
Expand All @@ -196,6 +207,7 @@ export const FileInput: StoryObj<typeof FileInputButton> = {
args: {
accept: ['.pdf', '.png', '.jpg'],
collapsePadding: false,
corners: 'normal',
disabled: false,
icon: <Upload />,
iconPosition: 'start',
Expand Down
5 changes: 4 additions & 1 deletion src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import clsx from 'clsx';

import {
ElementCorners,
ElementIntent,
ElementSize,
ElementVariant,
Expand All @@ -23,6 +24,7 @@ export interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement
border?: boolean;
className?: string;
collapsePadding?: boolean;
corners?: boolean | ElementCorners;
href?: string;
icon?: ReactElement;
iconPosition?: 'start' | 'end';
Expand All @@ -41,6 +43,7 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
border = false,
className,
collapsePadding,
corners = 'normal',
href,
icon,
iconPosition,
Expand All @@ -58,7 +61,7 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
const resolvedClassName = clsx(getStyleClassNames({
border,
collapsePadding,
corners: 'normal',
corners,
intent,
rounded,
size,
Expand Down
8 changes: 8 additions & 0 deletions src/components/InputText/InputText.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ const meta: Meta<typeof InputText> = {
],
tags: ['autodocs'],
argTypes: {
corners: {
control: 'select',
options: ['tight', 'normal', 'wide'],
description: 'The corner radius of the input.',
table: { category: 'Appearance' },
},
disabled: {
control: 'boolean',
description: 'Whether the component is disabled.',
Expand All @@ -33,6 +39,7 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
name: 'Default',
args: {
corners: 'normal',
disabled: false,
placeholder: 'Enter your name...',
},
Expand All @@ -41,6 +48,7 @@ export const Default: Story = {
export const WithIcon: Story = {
name: 'With Icon',
args: {
corners: 'normal',
disabled: false,
placeholder: 'Search...',
icon: <Search />,
Expand Down
5 changes: 4 additions & 1 deletion src/components/InputText/InputText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ import {
} from 'react';
import clsx from 'clsx';

import { ElementCorners } from '../../types';
import { getStyleClassNames } from '../../utils/getStyleClassNames';

import styles from './InputText.module.scss';

export interface InputTextProps extends InputHTMLAttributes<HTMLInputElement> {
icon?: ReactElement;
loading?: boolean;
corners?: boolean | ElementCorners;
}

export const InputText = forwardRef<HTMLInputElement, InputTextProps>(({
className,
corners = 'normal',
icon,
loading = false, // TODO: Implement skeleton loading state
disabled,
Expand All @@ -32,7 +35,7 @@ export const InputText = forwardRef<HTMLInputElement, InputTextProps>(({
<input
ref={ref}
className={clsx(getStyleClassNames({
corners: 'normal',
corners,
variant: 'ghost',
border: true,
size: 'normal',
Expand Down
9 changes: 8 additions & 1 deletion src/components/ThemeProvider/ThemeProvider.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ import { light } from './themes/light';
import { SelectOption } from '../Select';
import { Theme } from './ThemeProvider.types';

/* `dark` is left undefined for the system option, which has no inherent light/
* dark polarity of its own - only for options backed by a registered theme, so
* consumers can filter/group options by polarity (e.g. separate light-theme and
* dark-theme pickers).
*/
export type ThemeOption = SelectOption & { dark?: boolean };

export interface ThemeContextValue {
key: string;
theme: Theme;
options: SelectOption[];
options: ThemeOption[];
setTheme: (key: string) => void;
}

Expand Down
3 changes: 2 additions & 1 deletion src/components/ThemeProvider/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ export const ThemeProvider = ({
const registry = useStore(themeStore);
const options = useMemo(() => [
{ value: SYSTEM_THEME_KEY, label: 'System' },
...Object.entries(registry).map(([k, { theme: { displayName } }]) => ({
...Object.entries(registry).map(([k, { theme: { displayName, dark } }]) => ({
value: k,
label: displayName,
dark,
})),
], [registry]);

Expand Down
1 change: 1 addition & 0 deletions src/components/ThemeProvider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export {
type ThemeProviderProps,
} from './ThemeProvider';
export { SYSTEM_THEME_KEY, THEME_STORAGE_KEY } from './ThemeProvider.constants';
export type { ThemeOption } from './ThemeProvider.context';
export { useTheme, useThemeManager } from './ThemeProvider.hooks';
export {
getThemeStyleSheet,
Expand Down
183 changes: 183 additions & 0 deletions src/components/ToggleGroup/ToggleGroup.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
@use "../../style/layers";

@layer components {
.toggleGroup {
--toggle-group-spacer-size: 0px;

display: inline-flex;
flex-direction: row;

&[data-border="true"] {
--toggle-group-spacer-size: var(--border-width);
}

&[data-orientation="vertical"] {
flex-direction: column;
}

&[data-equal="true"] {
display: inline-grid;

&[data-orientation="vertical"] {
grid-template-columns: 1fr;
}
}

&-item {
cursor: pointer;

position: relative;

display: flex;
gap: 0.5rem;
align-items: center;
justify-content: center;

line-height: 1rem;
white-space: nowrap;

&[data-reverse="true"] {
flex-direction: row-reverse;
}

&[data-disabled] {
cursor: not-allowed;
opacity: 0.4;
}

/* The item's own class names always reflect its unpressed variant;
* base-ui sets `data-pressed` natively, and these custom properties
* (set inline per-item) are what this rule swaps in - so the
* pressed/unpressed variant swap never needs to be decided in JS.
*/
&[data-pressed] {
--variant-border: var(--toggle-group-item-pressed-border-color);

color: var(--toggle-group-item-pressed-color, inherit);
background-color: var(--toggle-group-item-pressed-background-color);
}
}

&-spacer {
flex-shrink: 0;
border-color: transparent;
background-color: transparent;

/* The width itself never animates (a 1px change looks stepped/jittery
* no matter how it's eased), so it's always a 0s (instant) transition.
* The delay is what's doing the work: hiding first sets border-color/
* background-color transparent (250ms fade) while the width transition
* is delayed by that same 250ms, so the spacer keeps its full width
* until the fade finishes, then collapses in a single frame instead of
* collapsing immediately and leaving nothing for the fade to render.
*/
@media (prefers-reduced-motion: no-preference) {
transition:
background-color 250ms ease-in-out,
border-color 250ms ease-in-out,
border-left-width 0s linear 250ms,
border-top-width 0s linear 250ms;
}
}

&[data-border="true"] &-spacer {
border-color: var(--toggle-group-spacer-border-color);
background-color: var(--toggle-group-spacer-background-color);

/* Showing goes the other way: no delay, so the width snaps to its full
* size immediately, giving the color something to fade into right away.
*/
@media (prefers-reduced-motion: no-preference) {
transition:
background-color 250ms ease-in-out,
border-color 250ms ease-in-out,
border-left-width 0s linear 0s,
border-top-width 0s linear 0s;
}
}

&[data-orientation="horizontal"] &-spacer {
width: 0;
border-left-style: solid;
border-left-width: var(--toggle-group-spacer-size);
}

&[data-orientation="vertical"] &-spacer {
width: auto;
height: 0;
border-top-style: solid;
border-top-width: var(--toggle-group-spacer-size);
}

/* Higher specificity than the plain `[data-border="true"] &-spacer` rule
* above, so this wins whenever either neighboring item is pressed -
* `:has()` covers the next item, the adjacent sibling combinator covers
* the previous one - without needing to know which one in JS.
*/
&[data-border="true"] {
.toggleGroup-item[data-pressed] + .toggleGroup-spacer,
.toggleGroup-spacer:has(+ .toggleGroup-item[data-pressed]) {
border-color: var(--toggle-group-spacer-active-border-color);
background-color: var(--toggle-group-spacer-active-background-color);
}
}

&[data-orientation="horizontal"] {
Comment thread
ianpaschal marked this conversation as resolved.
.toggleGroup-item:first-child:not(:only-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}

.toggleGroup-item:last-child:not(:only-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}

/* stylelint-disable-next-line no-descending-specificity */
.toggleGroup-item:not(:first-child, :last-child) {
border-radius: 0;
}

.toggleGroup-item {
/* stylelint-disable-next-line no-descending-specificity */
&:not(:first-child) {
border-left-width: 0;
}

/* stylelint-disable-next-line no-descending-specificity */
&:not(:last-child) {
border-right-width: 0;
}
}
}

&[data-orientation="vertical"] {
.toggleGroup-item:first-child:not(:only-child) {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}

.toggleGroup-item:last-child:not(:only-child) {
border-top-left-radius: 0;
border-top-right-radius: 0;
}

/* stylelint-disable-next-line no-descending-specificity */
.toggleGroup-item:not(:first-child, :last-child) {
border-radius: 0;
}

.toggleGroup-item {
/* stylelint-disable-next-line no-descending-specificity */
&:not(:first-child) {
border-top-width: 0;
}

/* stylelint-disable-next-line no-descending-specificity */
&:not(:last-child) {
border-bottom-width: 0;
}
}
}
}
}
3 changes: 3 additions & 0 deletions src/components/ToggleGroup/ToggleGroup.module.scss.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export declare const toggleGroup: string;
export declare const toggleGroupItem: string;
export declare const toggleGroupSpacer: string;
Loading
Loading