mirror of
https://github.com/anotherhadi/ilovetui.git
synced 2026-10-05 10:58:26 +02:00
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user