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
5 changes: 5 additions & 0 deletions .changeset/bumpy-donkeys-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wtc": patch
---

Design tweaks and cleanup.
12 changes: 6 additions & 6 deletions src/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function Home() {
name: "github.open",
title: "Open GitHub",
desc: "Repository workflows",
category: "Navigation",
category: "GitHub",
run: () => {
navigate({ page: "github" });
dialog.clear();
Expand All @@ -83,7 +83,7 @@ function Home() {
name: "teamwork.open",
title: "Open Teamwork",
desc: "My assigned work",
category: "Navigation",
category: "Teamwork",
run: () => {
navigate({
page: "teamwork",
Expand All @@ -96,7 +96,7 @@ function Home() {
name: "teamwork.project.open",
title: "Open Teamwork Project",
desc: "Project-specific Teamwork context",
category: "Navigation",
category: "Teamwork",
run: () => {
navigate({ page: "teamwork", tab: "project" });
dialog.clear();
Expand All @@ -106,7 +106,7 @@ function Home() {
name: "teamwork.timers.open",
title: "Open Teamwork Timers",
desc: "Local timer tracking",
category: "Navigation",
category: "Teamwork",
run: () => {
navigate({ page: "teamwork", tab: "timers" });
dialog.clear();
Expand All @@ -116,7 +116,7 @@ function Home() {
name: "teamwork.timesheet.open",
title: "Open Teamwork Timesheet",
desc: "Open Teamwork time tracking in browser",
category: "Navigation",
category: "Teamwork",
run: () => {
dialog.clear();
void openUrlInBrowser(TEAMWORK_TIMESHEET_URL);
Expand All @@ -126,7 +126,7 @@ function Home() {
name: "settings.open",
title: "Open Settings",
desc: "Configuration and setup",
category: "Navigation",
category: "Settings",
run: () => {
navigate({ page: "settings" });
dialog.clear();
Expand Down
81 changes: 66 additions & 15 deletions src/tui/components/dialog-select.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createMemo, createSignal } from "solid-js";
import { createMemo, createSignal, For, Show } from "solid-js";
import { InputRenderable, TextAttributes } from "@opentui/core";
import { useBindings } from "@opentui/keymap/solid";
import { tokens } from "../tokens.ts";
Expand All @@ -17,14 +17,20 @@ export interface DialogSelectOption<T> {
value: T;
/** Optional secondary text shown beside the title. */
description?: string;
/** Optional grouping metadata, currently used by filtering. */
/** Optional grouping metadata rendered as a section heading. */
category?: string;
/** Reserved for future contextual footer text. */
footer?: string;
/** Action to run when the option is selected by keyboard or mouse. */
onSelect?: () => void;
}

interface DialogOptionSection<T> {
category: string;
options: DialogSelectOption<T>[];
startIndex: number;
}

/**
* Filters dialog select options using the title, description, and category.
*
Expand Down Expand Up @@ -58,6 +64,28 @@ export function DialogSelect<T>(props: { title: string; options: DialogSelectOpt
const [query, setQuery] = createSignal("");
const [selectedIndex, setSelectedIndex] = createSignal(0);
const filtered = createMemo(() => filterDialogSelectOptions(props.options, query()));

const sections = createMemo(() => {
const items = filtered();
const groups = new Map<string, DialogSelectOption<T>[]>();
for (const item of items) {
const cat = item.category ?? "Other";
const group = groups.get(cat);
if (group) {
group.push(item);
} else {
groups.set(cat, [item]);
}
}
let startIndex = 0;
const result: DialogOptionSection<T>[] = [];
for (const [category, groupOptions] of groups) {
result.push({ category, options: groupOptions, startIndex });
startIndex += groupOptions.length;
}
return result;
});

let input: InputRenderable | undefined;

const move = (direction: 1 | -1) => {
Expand Down Expand Up @@ -108,7 +136,7 @@ export function DialogSelect<T>(props: { title: string; options: DialogSelectOpt
}));

return (
<box paddingLeft={2} paddingRight={2} gap={1} flexDirection="column">
<box paddingLeft={2} paddingRight={2} paddingTop={1} gap={1} flexDirection="column">
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={tokens.text}>
{props.title}
Expand All @@ -134,18 +162,41 @@ export function DialogSelect<T>(props: { title: string; options: DialogSelectOpt
}, 1);
}}
/>
<box flexDirection="column" height={10} gap={0}>
{filtered().map((option, index) => (
<box
backgroundColor={index === selectedIndex() ? tokens.selectionBg : undefined}
onMouseUp={() => option.onSelect?.()}
>
<text fg={index === selectedIndex() ? tokens.selectionText : tokens.text}>
{option.title}
</text>
{option.description && <text fg={tokens.textDim}> — {option.description}</text>}
</box>
))}
<box flexDirection="column" flexGrow={1} gap={0}>
Comment thread
marlonmarcello marked this conversation as resolved.
<For each={sections()}>
{(section, sectionIndex) => (
<box flexDirection="column" gap={0}>
<Show when={sectionIndex() > 0}>
<text> </text>
</Show>
<text paddingLeft={1} fg={tokens.accent}>
{section.category}
</text>
<For each={section.options}>
{(option, localIndex) => {
const globalIndex = section.startIndex + localIndex();
return (
<box
backgroundColor={
globalIndex === selectedIndex() ? tokens.selectionBg : undefined
}
onMouseUp={() => option.onSelect?.()}
>
<text
fg={globalIndex === selectedIndex() ? tokens.selectionText : tokens.text}
>
{option.title}
</text>
{option.description && (
<text fg={tokens.textDim}> — {option.description}</text>
)}
</box>
);
}}
</For>
</box>
)}
</For>
{filtered().length === 0 && <text fg={tokens.textDim}>No matching commands</text>}
</box>
<text fg={tokens.textDim}>↑↓ navigate · enter select · esc close</text>
Expand Down
8 changes: 4 additions & 4 deletions src/tui/components/layout/accordion-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ function descriptions(value: AccordionSectionProps["description"]): readonly str
export function AccordionSection(props: AccordionSectionProps) {
return (
<box
border
borderStyle="rounded"
borderColor={tokens.border}
padding={1}
flexDirection="column"
gap={1}
backgroundColor={tokens.surfaceOverlay}
border={["left"]}
borderColor={tokens.accentSoft}
padding={1}
>
<box flexDirection="column" gap={0} onMouseUp={props.onToggle}>
<box flexDirection="row" gap={1}>
Expand Down
41 changes: 41 additions & 0 deletions src/tui/components/layout/card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Show, type ParentProps } from "solid-js";

import { tokens } from "../../tokens.ts";

/** Props for a titled content card used to group related UI sections. */
export interface CardProps extends ParentProps {
title?: string;
status?: string;
active?: boolean;
}

/**
* Full-rounded-border grouping container for content sections.
*
* Use `Card` for major visual groups (pinned task lists, settings sections,
* timer lists) and nest `ListItem` entries inside it. Avoid nesting Cards
* more than one level deep.
*/
export function Card(props: CardProps) {
return (
<box
border
borderStyle="rounded"
borderColor={props.active ? tokens.borderFocus : tokens.border}
padding={1}
flexDirection="column"
gap={1}
>
<Show when={props.title}>
<box flexDirection="row" gap={1}>
<text fg={props.active ? tokens.accent : tokens.text}>{props.title}</text>
<Show when={props.status}>
<text fg={tokens.textDim}>{props.status}</text>
</Show>
</box>
</Show>

{props.children}
</box>
);
}
51 changes: 51 additions & 0 deletions src/tui/components/layout/list-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Show, type JSX } from "solid-js";

import { tokens } from "../../tokens.ts";

/** Props for a compact selectable list item inside a Card. */
export interface ListItemProps {
id?: string;
title: string;
metadata?: readonly string[];
selected?: boolean;
badge?: JSX.Element;
}

/** Separator used between inline metadata segments. */
const METADATA_SEPARATOR = " • ";

/**
* Compact selectable entry for use inside a `Card`.
*
* Renders a thin left accent bar, title, optional inline metadata, and an
* optional right-aligned badge. Use for task entries, timer entries, and
* any other list-style content.
*/
export function ListItem(props: ListItemProps) {
const metadataLine = () => {
const parts = props.metadata;
if (!parts || parts.length === 0) return null;
return parts.join(METADATA_SEPARATOR);
};

return (
<box id={props.id} flexDirection="row" gap={1}>
<box width={1} backgroundColor={props.selected ? tokens.accent : undefined} />

<box flexDirection="column" flexGrow={1} gap={0}>
<text fg={props.selected ? tokens.accent : tokens.text}>
{props.selected ? "● " : ""}
{props.title}
</text>

<Show when={metadataLine()}>
<text fg={tokens.textDim}>{metadataLine()}</text>
</Show>
</box>

<Show when={props.badge}>
<box justifyContent="flex-end">{props.badge}</box>
</Show>
</box>
);
}
54 changes: 0 additions & 54 deletions src/tui/components/layout/section.tsx

This file was deleted.

Loading
Loading