fix: linux display main page

This commit is contained in:
2026-09-08 21:57:38 +02:00
parent ac23f98fcd
commit 8f5a7c22cc
14 changed files with 442 additions and 195 deletions
@@ -0,0 +1,55 @@
/* ChoiceSelect.css */
.choice-select {
position: relative;
display: flex;
align-items: stretch;
}
.choice-select[data-disabled] {
opacity: 0.5;
}
.choice-select__input {
flex: 1 1 auto;
height: var(--card-control-height);
padding: 0 var(--space-6) 0 var(--space-3);
background: var(--bg-panel);
color: var(--text-primary);
border: var(--border-width) solid var(--border-hairline);
border-radius: var(--radius-sm);
font-family: var(--font-ui);
font-size: var(--font-sm);
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
outline: none;
cursor: pointer;
transition: border-color var(--dur-base) var(--ease-standard);
}
.choice-select__input:focus {
border-color: var(--accent-brand);
}
.choice-select__input:disabled {
cursor: not-allowed;
}
.choice-select__chevron {
position: absolute;
right: var(--space-2);
top: 50%;
height: 70%;
aspect-ratio: 1;
transform: translateY(-50%);
pointer-events: none;
color: var(--accent-brand);
font-size: var(--font-sm);
}
@@ -0,0 +1,44 @@
// ChoiceSelect.tsx
import { TriangleDownIcon } from '../../assets/icons/Triangle';
import './ChoiceSelect.css';
type ChoiceSelectProps<T extends string> = {
value: T;
options: readonly T[];
formatLabel?: (value: T) => string;
onChange: (value: T) => void;
id?: string;
disabled?: boolean;
};
function ChoiceSelect<T extends string>({
value,
options,
formatLabel = (v) => v,
onChange,
id,
disabled,
}: ChoiceSelectProps<T>) {
return (
<div className="choice-select" data-disabled={disabled || undefined}>
<select
id={id}
className="choice-select__input"
value={value}
disabled={disabled}
onChange={(e) => onChange(e.target.value as T)}
>
{options.map((option) => (
<option key={option} value={option}>
{formatLabel(option)}
</option>
))}
</select>
<span className="choice-select__chevron" aria-hidden="true">
<TriangleDownIcon />
</span>
</div>
);
}
export default ChoiceSelect;
+11 -4
View File
@@ -20,14 +20,13 @@
font-family: var(--font-ui);
font-size: 3cqh;
font-weight: var(--font-weight-semibold);
text-align: center;
text-align: left;
color: var(--accent-brand);
background: transparent;
border: none;
border-radius: var(--radius-sm);
padding: 0.5ch;
/* padding: 0.5ch; */
white-space: nowrap;
overflow: hidden;
@@ -35,7 +34,7 @@
}
.choice-stepper__input:focus {
outline: none;
outline: none;
}
.choice-stepper__arrows {
@@ -78,3 +77,11 @@
width: 100%;
height: 100%;
}
.choice-stepper[data-disabled] {
opacity: 0.5;
}
.choice-stepper__input:disabled {
cursor: not-allowed;
}
+151 -43
View File
@@ -1,60 +1,97 @@
import { useEffect, useState } from 'react';
import { JSX, useEffect, useState } from 'react';
import Button from '../Button/Button';
import { TriangleUpIcon, TriangleDownIcon } from '../../assets/icons/Triangle';
import './ChoiceStepper.css';
type ChoiceStepperProps<T> = {
function StepperArrows({
onUp,
onDown,
canUp,
canDown,
disabled,
}: {
onUp: () => void;
onDown: () => void;
canUp: boolean;
canDown: boolean;
disabled?: boolean;
}) {
return (
<div className="choice-stepper__arrows">
<Button
variant="toggle"
className="choice-stepper__arrow-btn"
onClick={onUp}
disabled={disabled || !canUp}
>
<TriangleUpIcon />
</Button>
<Button
variant="toggle"
className="choice-stepper__arrow-btn"
onClick={onDown}
disabled={disabled || !canDown}
>
<TriangleDownIcon />
</Button>
</div>
);
}
// ---- List variant ----
type ChoiceStepperListProps<T> = {
value: T;
values: readonly T[];
formatLabel: (value: T) => string;
onChange: (value: T) => void;
id?: string;
disabled?: boolean;
className?: string;
};
function ChoiceStepper<T>({ value, values, formatLabel, onChange }: ChoiceStepperProps<T>) {
function ChoiceStepperList<T>({
value,
values,
formatLabel,
onChange,
id,
disabled,
className,
}: ChoiceStepperListProps<T>) {
const index = values.indexOf(value);
const canUp = index === -1 || index < values.length - 1;
const canDown = index > 0;
const [text, setText] = useState(formatLabel(value));
useEffect(() => {
setText(formatLabel(value));
}, [value, formatLabel]);
useEffect(() => setText(formatLabel(value)), [value, formatLabel]);
const step = (direction: 1 | -1) => {
const currentIndex = index === -1 ? 0 : index;
const nextIndex = Math.min(values.length - 1, Math.max(0, currentIndex + direction));
if (nextIndex !== currentIndex) {
onChange(values[nextIndex]);
}
if (nextIndex !== currentIndex) onChange(values[nextIndex]);
};
const commitText = () => {
const match = values.find(
(v) => formatLabel(v).trim().toLowerCase() === text.trim().toLowerCase()
);
if (match !== undefined) {
onChange(match);
} else {
setText(formatLabel(value));
}
if (match !== undefined) onChange(match);
else setText(formatLabel(value));
};
return (
<div className="choice-stepper">
<div className={`choice-stepper ${className ?? ''}`} data-disabled={disabled || undefined}>
<input
id={id}
className="choice-stepper__input"
value={text}
disabled={disabled}
onChange={(e) => setText(e.target.value)}
onBlur={commitText}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.currentTarget.blur();
} else if (e.key === 'ArrowUp') {
if (e.key === 'Enter') e.currentTarget.blur();
else if (e.key === 'ArrowUp') {
e.preventDefault();
step(1);
} else if (e.key === 'ArrowDown') {
@@ -63,28 +100,99 @@ function ChoiceStepper<T>({ value, values, formatLabel, onChange }: ChoiceSteppe
}
}}
/>
<div className="choice-stepper__arrows">
<Button
variant="toggle"
className="choice-stepper__arrow-btn"
onClick={() => step(1)}
disabled={!canUp}
>
<TriangleUpIcon />
</Button>
<Button
variant="toggle"
className="choice-stepper__arrow-btn"
onClick={() => step(-1)}
disabled={!canDown}
>
<TriangleDownIcon />
</Button>
</div>
<StepperArrows
onUp={() => step(1)}
onDown={() => step(-1)}
canUp={canUp}
canDown={canDown}
disabled={disabled}
/>
</div>
);
}
// ---- Range variant (concrete, no generic) ----
type ChoiceStepperRangeProps = {
value: number;
min: number;
max: number;
step?: number;
formatLabel: (value: number) => string;
onChange: (value: number) => void;
id?: string;
disabled?: boolean;
className?: string;
};
function ChoiceStepperRange({
value,
min,
max,
step: stepSize = 1,
formatLabel,
onChange,
id,
disabled,
className,
}: ChoiceStepperRangeProps) {
const [text, setText] = useState(formatLabel(value));
useEffect(() => setText(formatLabel(value)), [value, formatLabel]);
const clamp = (n: number) => Math.min(max, Math.max(min, n));
const canUp = value < max;
const canDown = value > min;
const step = (direction: 1 | -1) => {
const next = clamp(value + direction * stepSize);
if (next !== value) onChange(next);
};
const commitText = () => {
const parsed = Number(text);
if (Number.isFinite(parsed)) onChange(clamp(parsed));
else setText(formatLabel(value));
};
return (
<div className={`choice-stepper ${className ?? ''}`} data-disabled={disabled || undefined}>
<input
id={id}
className="choice-stepper__input"
value={text}
disabled={disabled}
onChange={(e) => setText(e.target.value)}
onBlur={commitText}
onKeyDown={(e) => {
if (e.key === 'Enter') e.currentTarget.blur();
else if (e.key === 'ArrowUp') {
e.preventDefault();
step(1);
} else if (e.key === 'ArrowDown') {
e.preventDefault();
step(-1);
}
}}
/>
<StepperArrows
onUp={() => step(1)}
onDown={() => step(-1)}
canUp={canUp}
canDown={canDown}
disabled={disabled}
/>
</div>
);
}
function ChoiceStepper<T>(props: ChoiceStepperListProps<T>): JSX.Element;
function ChoiceStepper(props: ChoiceStepperRangeProps): JSX.Element;
function ChoiceStepper(props: ChoiceStepperListProps<unknown> | ChoiceStepperRangeProps) {
if ('values' in props) {
return <ChoiceStepperList {...props} />;
}
return <ChoiceStepperRange {...props} />;
}
export default ChoiceStepper;