Unify segmented controls on a shared module with grey selection

This commit is contained in:
2026-06-05 13:41:29 +03:00
parent 0f8738cd0c
commit cc0814a596
6 changed files with 65 additions and 82 deletions
+2 -35
View File
@@ -25,41 +25,8 @@
box-sizing: border-box; box-sizing: border-box;
} }
/* Segmented control — the four fit modes (spec §04). */ /* Fit-mode segmented control (spec §04) styling now lives in
.fit { SegmentedControl.module.css; the per-mode sizing classes below stay here. */
display: inline-flex;
border: var(--border-width) solid var(--border-strong);
}
.fitOption {
appearance: none;
border: none;
background: var(--bg);
color: var(--text-secondary);
font: inherit;
font-size: 12px;
line-height: 1;
padding: var(--space-2) var(--space-3);
cursor: pointer;
transition:
background var(--dur-fast) var(--ease),
color var(--dur-fast) var(--ease);
}
.fitOption + .fitOption {
border-left: var(--border-width) solid var(--border-strong);
}
.fitOption:hover {
background: var(--layer-01);
color: var(--text);
}
.fitActive,
.fitActive:hover {
background: var(--accent);
color: var(--accent-contrast);
}
/* /*
* Chart sizing. The host (passed to vega-embed) is branded `.vega-embed` * Chart sizing. The host (passed to vega-embed) is branded `.vega-embed`
-3
View File
@@ -62,9 +62,6 @@ function FitControl() {
options={FIT_OPTIONS} options={FIT_OPTIONS}
value={fitMode} value={fitMode}
onChange={setFitMode} onChange={setFitMode}
className={styles.fit}
optionClassName={styles.fitOption}
activeClassName={styles.fitActive}
/> />
); );
} }
@@ -0,0 +1,42 @@
/*
* Shared look for every SegmentedControl. The control's appearance is owned here
* — once — so the Draft/Published and fit-mode instances can't drift apart again.
* Selected option uses --layer-02 (a neutral high-contrast fill, per Carbon's
* content switcher); --accent stays reserved for primary actions (e.g. Publish),
* so "current selection" never reads as "the action to take". Call sites may still
* pass className/optionClassName/activeClassName for layout-specific additions.
*/
.group {
display: inline-flex;
border: var(--border-width) solid var(--border-strong);
}
.option {
appearance: none;
border: none;
background: var(--bg);
color: var(--text-secondary);
font: inherit;
font-size: 12px;
line-height: 1;
padding: var(--space-2) var(--space-3);
cursor: pointer;
transition:
background var(--dur-fast) var(--ease),
color var(--dur-fast) var(--ease);
}
.option + .option {
border-left: var(--border-width) solid var(--border-strong);
}
.option:hover {
background: var(--layer-01);
color: var(--text);
}
.active,
.active:hover {
background: var(--layer-02);
color: var(--text);
}
+20 -6
View File
@@ -9,10 +9,13 @@
* move focus *and* select. Native `<button>` gives Enter/Space-to-select for * move focus *and* select. Native `<button>` gives Enter/Space-to-select for
* free. One widget so every segmented control gets the same, documented model. * free. One widget so every segmented control gets the same, documented model.
* *
* Styling is injected via class props so each call site keeps its own look. * The control owns its base look (SegmentedControl.module.css) so every instance
* stays visually consistent; call sites may pass class props for layout-specific
* additions, which compose on top of the base classes.
*/ */
import { useRef } from 'react'; import { useRef } from 'react';
import styles from './SegmentedControl.module.css';
export interface SegmentedOption<T extends string> { export interface SegmentedOption<T extends string> {
value: T; value: T;
@@ -25,11 +28,11 @@ interface SegmentedControlProps<T extends string> {
options: ReadonlyArray<SegmentedOption<T>>; options: ReadonlyArray<SegmentedOption<T>>;
value: T; value: T;
onChange: (value: T) => void; onChange: (value: T) => void;
/** Class for the radiogroup container. */ /** Extra class for the radiogroup container (composed onto the base look). */
className?: string; className?: string;
/** Class for each option button. */ /** Extra class for each option button (composed onto the base look). */
optionClassName?: string; optionClassName?: string;
/** Extra class applied to the selected option. */ /** Extra class applied to the selected option (composed onto the base active look). */
activeClassName?: string; activeClassName?: string;
} }
@@ -74,7 +77,11 @@ export function SegmentedControl<T extends string>({
}; };
return ( return (
<div className={className} role="radiogroup" aria-label={label}> <div
className={[styles.group, className].filter(Boolean).join(' ')}
role="radiogroup"
aria-label={label}
>
{options.map((opt, i) => { {options.map((opt, i) => {
const selected = opt.value === value; const selected = opt.value === value;
return ( return (
@@ -89,7 +96,14 @@ export function SegmentedControl<T extends string>({
// Roving tabindex: only the selected option is a tab stop; arrows // Roving tabindex: only the selected option is a tab stop; arrows
// move within the group. // move within the group.
tabIndex={selected ? 0 : -1} tabIndex={selected ? 0 : -1}
className={[optionClassName, selected ? activeClassName : ''].filter(Boolean).join(' ')} className={[
styles.option,
optionClassName,
selected && styles.active,
selected && activeClassName,
]
.filter(Boolean)
.join(' ')}
onClick={() => onChange(opt.value)} onClick={() => onChange(opt.value)}
onKeyDown={(e) => onKeyDown(e, i)} onKeyDown={(e) => onKeyDown(e, i)}
> >
+1 -35
View File
@@ -19,41 +19,7 @@
flex: 1 1 auto; flex: 1 1 auto;
} }
/* Draft / Published segmented toggle. */ /* Draft/Published toggle styling now lives in SegmentedControl.module.css. */
.viewToggle {
display: inline-flex;
border: var(--border-width) solid var(--border-strong);
}
.viewOption {
appearance: none;
border: none;
background: var(--bg);
color: var(--text-secondary);
font: inherit;
font-size: 12px;
line-height: 1;
padding: var(--space-2) var(--space-3);
cursor: pointer;
transition:
background var(--dur-fast) var(--ease),
color var(--dur-fast) var(--ease);
}
.viewOption + .viewOption {
border-left: var(--border-width) solid var(--border-strong);
}
.viewOption:hover {
background: var(--layer-01);
color: var(--text);
}
.viewActive,
.viewActive:hover {
background: var(--layer-02);
color: var(--text);
}
/* Publish / Revert buttons. */ /* Publish / Revert buttons. */
.action { .action {
-3
View File
@@ -81,9 +81,6 @@ function EditorToolbar() {
options={VIEW_OPTIONS} options={VIEW_OPTIONS}
value={editorView} value={editorView}
onChange={setEditorView} onChange={setEditorView}
className={styles.viewToggle}
optionClassName={styles.viewOption}
activeClassName={styles.viewActive}
/> />
<span className={styles.spacer} /> <span className={styles.spacer} />