multiple fixes

Signed-off-by: Hadi <[email protected]>
This commit is contained in:
Hadi
2026-09-01 16:35:10 +02:00
parent 038842878a
commit b329bdc074
19 changed files with 321 additions and 214 deletions
+10 -6
View File
@@ -1,6 +1,6 @@
import type { ColorInput } from "@opentui/core";
import { useTerminalDimensions } from "@opentui/solid";
import { Match, Switch, type ParentProps } from "solid-js";
import { Show, type ParentProps } from "solid-js";
import { theme } from "../index.ts";
export interface MinSizeGuardProps extends ParentProps {
@@ -17,21 +17,25 @@ export function MinSizeGuard(props: MinSizeGuardProps) {
const tooSmall = () => dimensions().width < minWidth() || dimensions().height < minHeight();
return (
<Switch>
<Match when={tooSmall()}>
<>
{props.children}
<Show when={tooSmall()}>
<box
position="absolute"
top={0}
left={0}
width={dimensions().width}
height={dimensions().height}
alignItems="center"
justifyContent="center"
backgroundColor={props.backgroundColor ?? theme.background}
zIndex={9999}
>
<text fg={props.textColor ?? theme.muted}>
{`Terminal too small\nMinimum size: ${minWidth()}x${minHeight()} — current: ${dimensions().width}x${dimensions().height}`}
</text>
</box>
</Match>
<Match when={!tooSmall()}>{props.children}</Match>
</Switch>
</Show>
</>
);
}
+2 -1
View File
@@ -1,4 +1,4 @@
import { type ColorInput, RGBA } from "@opentui/core";
import { type ColorInput, type MouseEvent, RGBA } from "@opentui/core";
import { Portal, useTerminalDimensions } from "@opentui/solid";
import { Show, type ParentProps } from "solid-js";
import { theme } from "../index.ts";
@@ -41,6 +41,7 @@ export function Modal(props: ModalProps) {
paddingRight={2}
paddingTop={1}
paddingBottom={1}
onMouseDown={theme.mouse ? (event: MouseEvent) => event.stopPropagation() : undefined}
>
{props.children}
</box>
+45 -12
View File
@@ -1,14 +1,25 @@
import { type ColorInput, type SelectOption, TextAttributes } from "@opentui/core";
import { theme } from "../index.ts";
import { type ColorInput, type MouseEvent, type SelectOption, type SelectRenderable, TextAttributes } from "@opentui/core";
import { Show } from "solid-js";
import { presets, theme } from "../index.ts";
export interface SidebarHandle {
moveUp(): void;
moveDown(): void;
selectCurrent(): void;
focus(): void;
}
function itemIndexAtScreenY(el: SelectRenderable, screenY: number): number | null {
const internals = el as unknown as { scrollOffset: number; linesPerItem: number };
if (!internals.linesPerItem) return null;
const localY = screenY - el.screenY;
if (localY < 0) return null;
const index = internals.scrollOffset + Math.floor(localY / internals.linesPerItem);
return index >= 0 && index < el.options.length ? index : null;
}
export interface SidebarProps {
title: string;
title?: string;
items: SelectOption[];
width?: number;
focused?: boolean;
@@ -30,6 +41,21 @@ export interface SidebarProps {
}
export function Sidebar(props: SidebarProps) {
let select: SelectRenderable | undefined;
const handleMouseDown = (event: MouseEvent) => {
if (!select) return;
const index = itemIndexAtScreenY(select, event.y);
if (index !== null) select.setSelectedIndex(index);
};
const handleMouseScroll = (event: MouseEvent) => {
if (!select || !event.scroll) return;
const steps = Math.max(1, event.scroll.delta);
if (event.scroll.direction === "down") select.moveDown(steps);
else if (event.scroll.direction === "up") select.moveUp(steps);
};
return (
<box
width={props.width ?? 24}
@@ -38,14 +64,19 @@ export function Sidebar(props: SidebarProps) {
borderColor={props.focused ? (props.accentColor ?? theme.primary) : (props.mutedColor ?? theme.muted)}
flexDirection="column"
>
<text attributes={TextAttributes.BOLD}>{props.title}</text>
<Show when={props.title}>
<text attributes={TextAttributes.BOLD}>{props.title}</text>
<text> </text>
</Show>
<select
ref={(el) => {
select = el ?? undefined;
props.ref?.(
el && {
moveUp: () => el.moveUp(),
moveDown: () => el.moveDown(),
selectCurrent: () => el.selectCurrent(),
focus: () => el.focus(),
},
);
}}
@@ -53,14 +84,16 @@ export function Sidebar(props: SidebarProps) {
focused={props.focused}
showScrollIndicator
options={props.items}
backgroundColor={props.backgroundColor}
textColor={props.textColor}
focusedBackgroundColor={props.focusedBackgroundColor}
focusedTextColor={props.focusedTextColor}
selectedBackgroundColor={props.selectedBackgroundColor}
selectedTextColor={props.selectedTextColor}
descriptionColor={props.descriptionColor}
selectedDescriptionColor={props.selectedDescriptionColor}
onMouseDown={theme.mouse ? handleMouseDown : undefined}
onMouseScroll={theme.mouse ? handleMouseScroll : undefined}
backgroundColor={props.backgroundColor ?? "transparent"}
textColor={props.textColor ?? presets.select.textColor}
focusedBackgroundColor={props.focusedBackgroundColor ?? "transparent"}
focusedTextColor={props.focusedTextColor ?? presets.select.focusedTextColor}
selectedBackgroundColor={props.selectedBackgroundColor ?? presets.select.selectedBackgroundColor}
selectedTextColor={props.selectedTextColor ?? presets.select.selectedTextColor}
descriptionColor={props.descriptionColor ?? presets.select.descriptionColor}
selectedDescriptionColor={props.selectedDescriptionColor ?? presets.select.selectedDescriptionColor}
onChange={(_index, option) => {
if (!option) return;
props.onSelect(option);
+30 -14
View File
@@ -1,4 +1,4 @@
import { type ColorInput, CliRenderEvents, TextAttributes, type BoxRenderable } from "@opentui/core";
import { type BorderCharacters, type BoxRenderable, type ColorInput, BorderChars, CliRenderEvents, TextAttributes } from "@opentui/core";
import { onResize, useRenderer } from "@opentui/solid";
import { createMemo, createSignal, For } from "solid-js";
import { theme } from "../index.ts";
@@ -31,27 +31,31 @@ function segmentWidth(label: string): number {
return innerWidth(label) + 2;
}
function topEdge(label: string): string {
return `╭${"─".repeat(innerWidth(label))}╮`;
function topEdge(chars: BorderCharacters, label: string): string {
return `${chars.topLeft}${chars.horizontal.repeat(innerWidth(label))}${chars.topRight}`;
}
function bottomEdge(label: string, isFirst: boolean, isLast: boolean, isActive: boolean): string {
const left = isFirst ? (isActive ? "│" : "├") : isActive ? "╯" : "┴";
const right = isLast ? (isActive ? "│" : "┤") : isActive ? "╰" : "┴";
const fill = isActive ? " " : "─";
function bottomEdge(chars: BorderCharacters, label: string, isFirst: boolean, isLast: boolean, isActive: boolean): string {
const left = isFirst ? (isActive ? chars.vertical : chars.leftT) : isActive ? chars.bottomRight : chars.bottomT;
const right = isLast ? (isActive ? chars.vertical : chars.rightT) : isActive ? chars.bottomLeft : chars.bottomT;
const fill = isActive ? " " : chars.horizontal;
return left + fill.repeat(innerWidth(label)) + right;
}
function buildBottomRow(segments: { label: string; isActive: boolean }[], available: number): string {
function buildBottomRow(
chars: BorderCharacters,
segments: { label: string; isActive: boolean }[],
available: number,
): string {
const tabsWidth = segments.reduce((sum, s) => sum + segmentWidth(s.label), 0);
const remainder = available - tabsWidth;
const extending = remainder > 0;
const tabs = segments
.map((s, i) => bottomEdge(s.label, i === 0, i === segments.length - 1 && !extending, s.isActive))
.map((s, i) => bottomEdge(chars, s.label, i === 0, i === segments.length - 1 && !extending, s.isActive))
.join("");
return extending ? tabs + "─".repeat(remainder - 1) + "╮" : tabs;
return extending ? tabs + chars.horizontal.repeat(remainder - 1) + chars.topRight : tabs;
}
function fitCount(items: TabItem[], start: number, available: number): number {
@@ -117,6 +121,7 @@ export function Tabs(props: TabsProps) {
const muted = () => props.mutedColor ?? theme.muted;
const borderColor = () => (props.focused ? accent() : muted());
const chars = BorderChars[theme.borderStyle];
return (
<box
@@ -126,25 +131,36 @@ export function Tabs(props: TabsProps) {
remeasureNextFrame();
}}
>
<text fg={borderColor()}>{segments().map((s) => topEdge(s.label)).join("")}</text>
<text selectable={false} fg={borderColor()}>
{segments()
.map((s) => topEdge(chars, s.label))
.join("")}
</text>
<box flexDirection="row">
<For each={segments()}>
{(s) => (
<>
<text fg={borderColor()}>│</text>
<text selectable={false} fg={borderColor()}>
{chars.vertical}
</text>
<text
selectable={false}
fg={s.isActive ? accent() : muted()}
attributes={s.isActive ? TextAttributes.BOLD : undefined}
onMouseDown={theme.mouse && s.value !== undefined ? () => props.onChange(s.value!) : undefined}
>
{` ${s.label} `}
</text>
<text fg={borderColor()}>│</text>
<text selectable={false} fg={borderColor()}>
{chars.vertical}
</text>
</>
)}
</For>
</box>
<text fg={borderColor()}>{buildBottomRow(segments(), width())}</text>
<text selectable={false} fg={borderColor()}>
{buildBottomRow(chars, segments(), width())}
</text>
</box>
);
}