fixes & update

Signed-off-by: Hadi <[email protected]>
This commit is contained in:
Hadi
2026-10-04 18:46:49 +02:00
parent 238d764b0a
commit 18668f4aea
80 changed files with 1682 additions and 754 deletions
+8 -1
View File
@@ -17,6 +17,13 @@ import { theme, presets } from "ilovetui";
import "ilovetui/solid";
```
Entry points: `.`, `./solid`, `./context`, `./project-config`, `./keymap`, `./components`, `./components/help`, `./components/spinner`. See the source for what each exports.
Entry points: `.`, `./solid`, `./context`, `./project-config`, `./keymap`, `./components`, `./components/help`, `./components/spinner`, `./components/section-header`. See the source for what each exports.
The theme is base16 and user-overridable via `~/.config/ilovetui/config.yaml`. Copy [`src/default.yaml`](https://github.com/anotherhadi/ilovetui/blob/main/src/default.yaml) there as a starting point.
An app can layer its own theme overrides on top of that file (e.g. a `theme:` section of its own config, validated with `ThemeConfigSchema`) by calling `configureTheme(overrides)` once at startup, before the first render. Invalid theme files are reported as warning toasts and skipped; unknown keys are ignored, so other TUIs can keep their own keys in the shared file.
## Focus and keymap scoping
Wrap each focus region in a `<Pane>` (`focusable` when the pane itself is the focus target). Inside it, `usePane()` gives `target` — pass it to `useBindings` so the layer is only active while focus is within the pane — and `focused()` for styling. `<Modal>` takes focus while open, restores it on close, and binds its `dismissKey` (default `escape`) plus any `commands`/`bindings` you pass, scoped to itself. Note that keymap layers are ordered by `priority` then registration order, not by tree depth: keep overlapping layers on disjoint targets, or give them explicit priorities.
+25
View File
@@ -0,0 +1,25 @@
// @bun
// src/context/focus.ts
import { CliRenderEvents } from "@opentui/core";
import { useRenderer } from "@opentui/solid";
import { createEffect, createSignal, onCleanup } from "solid-js";
function isInside2(node, root) {
if (!root)
return false;
for (let current = node;current; current = current.parent) {
if (current === root)
return true;
}
return false;
}
function useFocusWithin2(target) {
const renderer = useRenderer();
const [focused, setFocused] = createSignal(false);
const sync = () => setFocused(isInside2(renderer.currentFocusedRenderable, target() ?? null));
renderer.on(CliRenderEvents.FOCUSED_RENDERABLE, sync);
onCleanup(() => renderer.off(CliRenderEvents.FOCUSED_RENDERABLE, sync));
createEffect(sync);
return focused;
}
export { isInside2, useFocusWithin2 };
+5 -5
View File
@@ -2,20 +2,20 @@
// src/context/notifications.ts
import { createSignal } from "solid-js";
var DEFAULT_DURATION = 3000;
var [notifications, setNotifications] = createSignal([]);
var [notifications2, setNotifications] = createSignal([]);
var nextId = 0;
function notify(message, options = {}) {
function notify2(message, options = {}) {
const id = options.id ?? `toast-${nextId++}`;
const toast = { id, title: options.title, message, kind: options.kind ?? "info" };
setNotifications((prev) => [...prev.filter((n) => n.id !== id), toast]);
const duration = options.duration ?? DEFAULT_DURATION;
if (duration > 0) {
setTimeout(() => dismiss(id), duration);
setTimeout(() => dismiss2(id), duration);
}
return id;
}
function dismiss(id) {
function dismiss2(id) {
setNotifications((prev) => prev.filter((n) => n.id !== id));
}
export { notifications, notify, dismiss };
export { notifications2, notify2, dismiss2 };
-22
View File
@@ -1,22 +0,0 @@
// @bun
// src/yaml.ts
import { existsSync, readFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";
import { parse as parseYAML } from "yaml";
function configPath() {
const configDir = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
return join(configDir, "ilovetui", "config.yaml");
}
function readYamlFile(path) {
if (!existsSync(path))
return;
try {
return parseYAML(readFileSync(path, "utf8")) ?? undefined;
} catch (err) {
console.error(`ilovetui: failed to parse ${path}, ignoring it.`, err);
return;
}
}
export { configPath, readYamlFile };
+5 -5
View File
@@ -1,23 +1,23 @@
// @bun
import {
theme
} from "./chunk-x7m82z3z.js";
theme2
} from "./chunk-p6jvscj3.js";
// src/components/Box.tsx
import { spread as _$spread } from "@opentui/solid";
import { mergeProps as _$mergeProps } from "@opentui/solid";
import { memo as _$memo } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
function Box(props) {
function Box2(props) {
return (() => {
var _el$ = _$createElement("box");
_$spread(_el$, _$mergeProps(props, {
get borderStyle() {
return props.borderStyle ?? theme.borderStyle;
return props.borderStyle ?? theme2.borderStyle;
}
}), false);
return _el$;
})();
}
export { Box };
export { Box2 };
+13 -13
View File
@@ -1,11 +1,11 @@
// @bun
import {
theme
} from "./chunk-x7m82z3z.js";
notifications2,
dismiss2
} from "./chunk-6ggk5fg7.js";
import {
dismiss,
notifications
} from "./chunk-y6txxzxk.js";
theme2
} from "./chunk-p6jvscj3.js";
// src/components/NotificationHost.tsx
import { effect as _$effect } from "@opentui/solid";
@@ -23,12 +23,12 @@ var KIND_LABEL = {
warning: "Warning",
error: "Error"
};
function NotificationHost(props = {}) {
function NotificationHost2(props = {}) {
const defaultColor = () => ({
info: theme.primary,
success: theme.success,
warning: theme.warning,
error: theme.error
info: theme2.primary,
success: theme2.success,
warning: theme2.warning,
error: theme2.error
});
const colorFor = (kind) => props.colors?.[kind] ?? defaultColor()[kind];
return _$createComponent(Portal, {
@@ -41,7 +41,7 @@ function NotificationHost(props = {}) {
_$setProp(_el$, "zIndex", 1000);
_$insert(_el$, _$createComponent(For, {
get each() {
return notifications();
return notifications2();
},
children: (toast) => (() => {
var _el$2 = _$createElement("box"), _el$3 = _$createElement("text"), _el$4 = _$createElement("text");
@@ -52,7 +52,7 @@ function NotificationHost(props = {}) {
_$insert(_el$3, () => toast.title ?? KIND_LABEL[toast.kind]);
_$insert(_el$4, () => toast.message);
_$effect((_p$) => {
var _v$ = theme.borderStyle, _v$2 = colorFor(toast.kind), _v$3 = props.backgroundColor ?? theme.background, _v$4 = props.width ?? 32, _v$5 = theme.mouse ? () => dismiss(toast.id) : undefined, _v$6 = colorFor(toast.kind);
var _v$ = theme2.borderStyle, _v$2 = colorFor(toast.kind), _v$3 = props.backgroundColor ?? theme2.background, _v$4 = props.width ?? 32, _v$5 = theme2.mouse ? () => dismiss2(toast.id) : undefined, _v$6 = colorFor(toast.kind);
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "borderStyle", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "borderColor", _v$2, _p$.t));
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$2, "backgroundColor", _v$3, _p$.a));
@@ -76,4 +76,4 @@ function NotificationHost(props = {}) {
});
}
export { NotificationHost };
export { NotificationHost2 };
+6 -6
View File
@@ -1,7 +1,7 @@
// @bun
import {
theme
} from "./chunk-x7m82z3z.js";
theme2
} from "./chunk-p6jvscj3.js";
// src/components/HelpBar.tsx
import { createTextNode as _$createTextNode } from "@opentui/solid";
@@ -28,9 +28,9 @@ var KEY_DISPLAY = {
};
var SEPARATOR = " ";
var ELLIPSIS = "\u2026";
function HelpBar(props = {}) {
const accent = () => props.accentColor ?? theme.primary;
const muted = () => props.mutedColor ?? theme.muted;
function HelpBar2(props = {}) {
const accent = () => props.accentColor ?? theme2.primary;
const muted = () => props.mutedColor ?? theme2.muted;
const renderer = useRenderer();
const [width, setWidth] = createSignal(0);
let container;
@@ -118,4 +118,4 @@ function HelpBar(props = {}) {
})();
}
export { HelpBar };
export { HelpBar2 };
+44
View File
@@ -0,0 +1,44 @@
// @bun
import {
theme2
} from "./chunk-p6jvscj3.js";
// src/components/KeyValue.tsx
import { memo as _$memo } from "@opentui/solid";
import { effect as _$effect } from "@opentui/solid";
import { insertNode as _$insertNode } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { Show } from "solid-js";
function KeyValue2(props) {
return (() => {
var _el$ = _$createElement("box"), _el$2 = _$createElement("text");
_$insertNode(_el$, _el$2);
_$setProp(_el$, "flexShrink", 0);
_$setProp(_el$, "flexDirection", "row");
_$setProp(_el$, "justifyContent", "space-between");
_$insert(_el$2, () => props.label);
_$insert(_el$, _$createComponent(Show, {
get when() {
return props.children;
},
get fallback() {
return (() => {
var _el$3 = _$createElement("text");
_$insert(_el$3, () => props.value ?? "");
_$effect((_$p) => _$setProp(_el$3, "fg", props.valueColor, _$p));
return _el$3;
})();
},
get children() {
return props.children;
}
}), null);
_$effect((_$p) => _$setProp(_el$2, "fg", theme2.muted, _$p));
return _el$;
})();
}
export { KeyValue2 };
-9
View File
@@ -1,9 +0,0 @@
// @bun
// src/context/help.ts
import { createSignal } from "solid-js";
var [helpOpen, setHelpOpen] = createSignal(false);
function toggleHelp() {
setHelpOpen((open) => !open);
}
export { helpOpen, toggleHelp };
+9
View File
@@ -0,0 +1,9 @@
// @bun
// src/context/help.ts
import { createSignal } from "solid-js";
var [helpOpen2, setHelpOpen] = createSignal(false);
function toggleHelp2() {
setHelpOpen((open) => !open);
}
export { helpOpen2, toggleHelp2 };
+18 -11
View File
@@ -1,11 +1,11 @@
// @bun
import {
Modal
} from "./chunk-zjfxbvmq.js";
helpOpen2,
toggleHelp2
} from "./chunk-ee2qaswx.js";
import {
helpOpen,
toggleHelp
} from "./chunk-cyvpvpws.js";
Modal2
} from "./chunk-t2cszvff.js";
// src/components/HelpModal.tsx
import { createComponent as _$createComponent } from "@opentui/solid";
@@ -18,6 +18,7 @@ import { createElement as _$createElement } from "@opentui/solid";
import { TextAttributes } from "@opentui/core";
import { formatCommandBindings } from "@opentui/keymap/extras";
import { useKeymapSelector } from "@opentui/keymap/solid";
import { createSignal } from "solid-js";
var KEY_DISPLAY = {
up: "\u2191",
down: "\u2193",
@@ -26,9 +27,11 @@ var KEY_DISPLAY = {
enter: "Enter",
escape: "Esc"
};
function HelpModal(props = {}) {
function HelpModal2(props = {}) {
const [scope, setScope] = createSignal(null);
const entries = useKeymapSelector((keymap) => keymap.getCommandEntries({
visibility: "active"
visibility: "active",
focused: scope()
}).map((entry) => ({
command: entry.command.name,
keys: formatCommandBindings(entry.bindings, {
@@ -36,11 +39,15 @@ function HelpModal(props = {}) {
}) ?? "",
label: typeof entry.command.desc === "string" ? entry.command.desc : entry.command.name
})));
return _$createComponent(Modal, {
return _$createComponent(Modal2, {
get open() {
return helpOpen();
return helpOpen2();
},
onDismiss: toggleHelp2,
onOpen: (previous) => setScope(() => previous),
get dismissKey() {
return props.dismissKey;
},
onDismiss: toggleHelp,
get accentColor() {
return props.accentColor;
},
@@ -77,4 +84,4 @@ function HelpModal(props = {}) {
});
}
export { HelpModal };
export { HelpModal2 };
-60
View File
@@ -1,60 +0,0 @@
// @bun
// src/config.ts
var COLOR_KEYS = [
"base00",
"base01",
"base02",
"base03",
"base04",
"base05",
"base06",
"base07",
"base08",
"base09",
"base0a",
"base0b",
"base0c",
"base0d",
"base0e",
"base0f"
];
function mergeColors(base, user) {
const merged = {};
for (const key of COLOR_KEYS) {
merged[key] = user[key] || base[key] || "";
}
return merged;
}
function mergeConfig(base, user) {
return {
nerdFonts: user.nerd_fonts ?? base.nerd_fonts ?? false,
emojiFonts: user.emoji_fonts ?? base.emoji_fonts ?? false,
reducedMotion: user.reduced_motion ?? base.reduced_motion ?? false,
mouse: user.mouse ?? base.mouse ?? true,
border: user.border || base.border || "rounded",
colors: mergeColors(base.colors ?? {}, user.colors ?? {})
};
}
function normalizeColor(hex) {
const trimmed = hex.trim();
if (trimmed && !trimmed.startsWith("#"))
return `#${trimmed}`;
return trimmed;
}
function normalizeColors(colors) {
const out = {};
for (const key of COLOR_KEYS)
out[key] = normalizeColor(colors[key]);
return out;
}
var OPENTUI_BORDER_STYLES = [
"single",
"double",
"rounded",
"heavy"
];
function resolveBorderStyle(name) {
return OPENTUI_BORDER_STYLES.includes(name) ? name : "rounded";
}
export { COLOR_KEYS, mergeColors, mergeConfig, normalizeColor, normalizeColors, resolveBorderStyle };
+2 -2
View File
@@ -2,7 +2,7 @@
// src/context/helper.tsx
import { createComponent as _$createComponent } from "@opentui/solid";
import { createContext, useContext } from "solid-js";
function createSimpleContext(input) {
function createSimpleContext2(input) {
const ctx = createContext();
return {
provider: (props) => {
@@ -23,4 +23,4 @@ function createSimpleContext(input) {
};
}
export { createSimpleContext };
export { createSimpleContext2 };
+35
View File
@@ -0,0 +1,35 @@
// @bun
import {
theme2
} from "./chunk-p6jvscj3.js";
// src/components/Spinner.tsx
import { setProp as _$setProp } from "@opentui/solid";
import { effect as _$effect } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import"opentui-spinner/solid";
function Spinner2(props) {
return (() => {
var _el$ = _$createElement("spinner");
_$effect((_p$) => {
var { name: _v$, frames: _v$2, interval: _v$3 } = props, _v$4 = props.autoplay ?? !theme2.reducedMotion, _v$5 = props.color ?? theme2.primary, _v$6 = props.backgroundColor;
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "name", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "frames", _v$2, _p$.t));
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "interval", _v$3, _p$.a));
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$, "autoplay", _v$4, _p$.o));
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$, "color", _v$5, _p$.i));
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$, "backgroundColor", _v$6, _p$.n));
return _p$;
}, {
e: undefined,
t: undefined,
a: undefined,
o: undefined,
i: undefined,
n: undefined
});
return _el$;
})();
}
export { Spinner2 };
+65
View File
@@ -0,0 +1,65 @@
// @bun
// src/config.ts
import { z } from "zod";
var COLOR_KEYS2 = [
"base00",
"base01",
"base02",
"base03",
"base04",
"base05",
"base06",
"base07",
"base08",
"base09",
"base0a",
"base0b",
"base0c",
"base0d",
"base0e",
"base0f"
];
var OPENTUI_BORDER_STYLES2 = ["single", "double", "rounded", "heavy"];
var hexColor = z.string().regex(/^#?[0-9a-fA-F]{6}$/, 'expected a hex color like "#1e1e2e"');
var colorsShape = {};
for (const key of COLOR_KEYS2)
colorsShape[key] = hexColor.optional();
var ThemeConfigSchema2 = z.object({
nerd_fonts: z.boolean().optional(),
emoji_fonts: z.boolean().optional(),
reduced_motion: z.boolean().optional(),
mouse: z.boolean().optional(),
border: z.enum(OPENTUI_BORDER_STYLES2).optional(),
colors: z.object(colorsShape).optional()
});
function layerConfigs2(layers) {
const out = {};
for (const layer of layers) {
const { colors, ...rest } = layer;
Object.assign(out, rest);
if (colors)
out.colors = { ...out.colors, ...colors };
}
return out;
}
function resolveConfig2(config) {
const colors = {};
for (const key of COLOR_KEYS2)
colors[key] = normalizeColor2(config.colors?.[key] ?? "");
return {
nerdFonts: config.nerd_fonts ?? false,
emojiFonts: config.emoji_fonts ?? false,
reducedMotion: config.reduced_motion ?? false,
mouse: config.mouse ?? true,
border: config.border ?? "rounded",
colors
};
}
function normalizeColor2(hex) {
const trimmed = hex.trim();
if (trimmed && !trimmed.startsWith("#"))
return `#${trimmed}`;
return trimmed;
}
export { COLOR_KEYS2, OPENTUI_BORDER_STYLES2, ThemeConfigSchema2, layerConfigs2, resolveConfig2, normalizeColor2 };
+79
View File
@@ -0,0 +1,79 @@
// @bun
import {
ThemeConfigSchema2,
layerConfigs2,
resolveConfig2
} from "./chunk-njvj322f.js";
import {
notify2
} from "./chunk-6ggk5fg7.js";
import {
buildPresets2
} from "./chunk-yyg967q8.js";
import {
configPath2
} from "./chunk-xykm10y9.js";
// src/index.ts
import { existsSync, readFileSync } from "fs";
import { join } from "path";
import { parse as parseYAML } from "yaml";
import { z } from "zod";
function reportThemeError(path, message) {
const text = `${path}: ${message}`;
console.error(`[ilovetui theme] ${text}`);
notify2(text, { kind: "warning", duration: 0 });
}
function readThemeFile(path) {
if (!existsSync(path))
return {};
let raw;
try {
raw = parseYAML(readFileSync(path, "utf8"));
} catch (error) {
reportThemeError(path, error instanceof Error ? error.message : String(error));
return {};
}
const result = ThemeConfigSchema2.safeParse(raw ?? {});
if (!result.success) {
reportThemeError(path, z.prettifyError(result.error));
return {};
}
return result.data;
}
var fileLayers = [readThemeFile(join(import.meta.dir, "default.yaml")), readThemeFile(configPath2())];
function buildTheme(overrides) {
const resolved = resolveConfig2(layerConfigs2([...fileLayers, overrides]));
const colors = resolved.colors;
return {
...colors,
background: colors.base00,
subtleBg: colors.base01,
selection: colors.base02,
subtle: colors.base03,
muted: colors.base04,
text: colors.base05,
primary: colors.base0d,
success: colors.base0b,
warning: colors.base09,
error: colors.base08,
nerdFonts: resolved.nerdFonts,
emojiFonts: resolved.emojiFonts,
reducedMotion: resolved.reducedMotion,
mouse: resolved.mouse,
borderStyle: resolved.border
};
}
var theme2 = buildTheme({});
var presets2 = buildPresets2(theme2);
function configureTheme2(overrides = {}) {
Object.assign(theme2, buildTheme(overrides));
const next = buildPresets2(theme2);
for (const key of Object.keys(next))
Object.assign(presets2[key], next[key]);
}
function icon2(codepoint) {
return theme2.nerdFonts ? `${codepoint} ` : "";
}
export { theme2, presets2, configureTheme2, icon2 };
+5 -5
View File
@@ -1,7 +1,7 @@
// @bun
import {
theme
} from "./chunk-x7m82z3z.js";
theme2
} from "./chunk-p6jvscj3.js";
// src/components/MinSizeGuard.tsx
import { createComponent as _$createComponent } from "@opentui/solid";
@@ -13,7 +13,7 @@ import { createElement as _$createElement } from "@opentui/solid";
import { memo as _$memo } from "@opentui/solid";
import { useTerminalDimensions } from "@opentui/solid";
import { Show } from "solid-js";
function MinSizeGuard(props) {
function MinSizeGuard2(props) {
const dimensions = useTerminalDimensions();
const minWidth = () => props.minWidth ?? 80;
const minHeight = () => props.minHeight ?? 24;
@@ -34,7 +34,7 @@ function MinSizeGuard(props) {
_$insert(_el$2, () => `Terminal too small
Minimum size: ${minWidth()}x${minHeight()} \u2014 current: ${dimensions().width}x${dimensions().height}`);
_$effect((_p$) => {
var _v$ = dimensions().width, _v$2 = dimensions().height, _v$3 = props.backgroundColor ?? theme.background, _v$4 = props.textColor ?? theme.muted;
var _v$ = dimensions().width, _v$2 = dimensions().height, _v$3 = props.backgroundColor ?? theme2.background, _v$4 = props.textColor ?? theme2.muted;
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "width", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "height", _v$2, _p$.t));
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "backgroundColor", _v$3, _p$.a));
@@ -51,4 +51,4 @@ Minimum size: ${minWidth()}x${minHeight()} \u2014 current: ${dimensions().width}
})];
}
export { MinSizeGuard };
export { MinSizeGuard2 };
+11 -11
View File
@@ -1,10 +1,10 @@
// @bun
import {
Badge
} from "./chunk-bscbaa87.js";
theme2
} from "./chunk-p6jvscj3.js";
import {
theme
} from "./chunk-x7m82z3z.js";
Badge2
} from "./chunk-v9cetqzr.js";
// src/components/Toggle.tsx
import { setProp as _$setProp } from "@opentui/solid";
@@ -15,10 +15,10 @@ import { memo as _$memo } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { TextAttributes } from "@opentui/core";
import { Show } from "solid-js";
function Toggle(props) {
const onColor = () => props.onColor ?? theme.success;
const offColor = () => props.offColor ?? theme.muted;
const nerdFonts = () => props.withNerdfont ?? theme.nerdFonts;
function Toggle2(props) {
const onColor = () => props.onColor ?? theme2.success;
const offColor = () => props.offColor ?? theme2.muted;
const nerdFonts = () => props.withNerdfont ?? theme2.nerdFonts;
return (() => {
var _el$ = _$createElement("box");
_$insert(_el$, _$createComponent(Show, {
@@ -42,7 +42,7 @@ function Toggle(props) {
})();
},
get children() {
return _$createComponent(Badge, {
return _$createComponent(Badge2, {
get label() {
return props.on ? " \u25CF" : "\u25CF ";
},
@@ -53,9 +53,9 @@ function Toggle(props) {
});
}
}));
_$effect((_$p) => _$setProp(_el$, "onMouseDown", theme.mouse ? props.onToggle : undefined, _$p));
_$effect((_$p) => _$setProp(_el$, "onMouseDown", theme2.mouse ? props.onToggle : undefined, _$p));
return _el$;
})();
}
export { Toggle };
export { Toggle2 };
+12 -7
View File
@@ -1,8 +1,8 @@
// @bun
import {
presets,
theme
} from "./chunk-x7m82z3z.js";
theme2,
presets2
} from "./chunk-p6jvscj3.js";
// src/components/Sidebar.tsx
import { use as _$use } from "@opentui/solid";
@@ -14,7 +14,7 @@ import { insert as _$insert } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { TextAttributes } from "@opentui/core";
import { Show } from "solid-js";
import { createEffect, Show } from "solid-js";
function truncateWithEllipsis(text, maxWidth) {
if (maxWidth <= 0)
return "";
@@ -35,7 +35,7 @@ function itemIndexAtScreenY(el, screenY) {
var BORDER_WIDTH = 2;
var ITEM_LEFT_PADDING = 1;
var ITEM_INDICATOR_WIDTH = 2;
function Sidebar(props) {
function Sidebar2(props) {
let select;
const contentWidth = () => Math.max(0, (props.width ?? 24) - BORDER_WIDTH);
const itemTextWidth = () => Math.max(0, contentWidth() - ITEM_LEFT_PADDING - ITEM_INDICATOR_WIDTH);
@@ -65,6 +65,11 @@ function Sidebar(props) {
else if (event.scroll.direction === "up")
select.moveUp(steps);
};
createEffect(() => {
const index = props.items.findIndex((item) => item.value === props.value);
if (select && index !== -1 && select.getSelectedIndex() !== index)
select.setSelectedIndex(index);
});
return (() => {
var _el$ = _$createElement("box"), _el$5 = _$createElement("select");
_$insertNode(_el$, _el$5);
@@ -109,7 +114,7 @@ function Sidebar(props) {
props.onConfirm?.(option);
});
_$effect((_p$) => {
var _v$ = props.width ?? 24, _v$2 = theme.borderStyle, _v$3 = props.focused ? props.accentColor ?? theme.primary : props.mutedColor ?? theme.muted, _v$4 = props.focused, _v$5 = displayItems(), _v$6 = theme.mouse ? handleMouseDown : undefined, _v$7 = theme.mouse ? handleMouseScroll : undefined, _v$8 = props.backgroundColor ?? "transparent", _v$9 = props.textColor ?? presets.select.textColor, _v$0 = props.focusedBackgroundColor ?? "transparent", _v$1 = props.focusedTextColor ?? presets.select.focusedTextColor, _v$10 = props.selectedBackgroundColor ?? presets.select.selectedBackgroundColor, _v$11 = props.selectedTextColor ?? presets.select.selectedTextColor, _v$12 = props.descriptionColor ?? presets.select.descriptionColor, _v$13 = props.selectedDescriptionColor ?? presets.select.selectedDescriptionColor;
var _v$ = props.width ?? 24, _v$2 = theme2.borderStyle, _v$3 = props.focused ? props.accentColor ?? theme2.primary : props.mutedColor ?? theme2.muted, _v$4 = props.focused, _v$5 = displayItems(), _v$6 = theme2.mouse ? handleMouseDown : undefined, _v$7 = theme2.mouse ? handleMouseScroll : undefined, _v$8 = props.backgroundColor ?? "transparent", _v$9 = props.textColor ?? presets2.select.textColor, _v$0 = props.focusedBackgroundColor ?? "transparent", _v$1 = props.focusedTextColor ?? presets2.select.focusedTextColor, _v$10 = props.selectedBackgroundColor ?? presets2.select.selectedBackgroundColor, _v$11 = props.selectedTextColor ?? presets2.select.selectedTextColor, _v$12 = props.descriptionColor ?? presets2.select.descriptionColor, _v$13 = props.selectedDescriptionColor ?? presets2.select.selectedDescriptionColor;
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "width", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "borderStyle", _v$2, _p$.t));
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "borderColor", _v$3, _p$.a));
@@ -147,4 +152,4 @@ function Sidebar(props) {
})();
}
export { Sidebar };
export { Sidebar2 };
+71
View File
@@ -0,0 +1,71 @@
// @bun
import {
theme2,
presets2
} from "./chunk-p6jvscj3.js";
// src/components/ListRow.tsx
import { effect as _$effect } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { insertNode as _$insertNode } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { TextAttributes } from "@opentui/core";
import { Show } from "solid-js";
function ListRow2(props) {
const tone = (fallback) => props.selected ? presets2.select.selectedTextColor : fallback;
return (() => {
var _el$ = _$createElement("box"), _el$2 = _$createElement("box"), _el$3 = _$createElement("box"), _el$4 = _$createElement("text");
_$insertNode(_el$, _el$2);
_$setProp(_el$, "flexShrink", 0);
_$setProp(_el$, "flexDirection", "column");
_$setProp(_el$, "paddingLeft", 1);
_$setProp(_el$, "paddingRight", 1);
_$insertNode(_el$2, _el$3);
_$setProp(_el$2, "flexDirection", "row");
_$setProp(_el$2, "justifyContent", "space-between");
_$setProp(_el$2, "alignItems", "center");
_$setProp(_el$2, "height", 1);
_$insertNode(_el$3, _el$4);
_$setProp(_el$3, "flexGrow", 1);
_$setProp(_el$3, "flexShrink", 1);
_$setProp(_el$3, "minWidth", 0);
_$setProp(_el$3, "marginRight", 1);
_$setProp(_el$3, "overflow", "hidden");
_$setProp(_el$4, "wrapMode", "none");
_$insert(_el$4, () => props.label);
_$insert(_el$2, _$createComponent(Show, {
get when() {
return props.right;
},
get children() {
var _el$5 = _$createElement("box");
_$setProp(_el$5, "flexShrink", 0);
_$setProp(_el$5, "flexDirection", "row");
_$setProp(_el$5, "alignItems", "center");
_$insert(_el$5, () => props.right?.(tone));
return _el$5;
}
}), null);
_$insert(_el$, () => props.below?.(tone), null);
_$effect((_p$) => {
var _v$ = props.id, _v$2 = props.selected ? presets2.select.selectedBackgroundColor : "transparent", _v$3 = theme2.mouse && props.onClick ? () => props.onClick?.() : undefined, _v$4 = tone(), _v$5 = props.bold ? TextAttributes.BOLD : undefined;
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "id", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "backgroundColor", _v$2, _p$.t));
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "onMouseDown", _v$3, _p$.a));
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$4, "fg", _v$4, _p$.o));
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$4, "attributes", _v$5, _p$.i));
return _p$;
}, {
e: undefined,
t: undefined,
a: undefined,
o: undefined,
i: undefined
});
return _el$;
})();
}
export { ListRow2 };
+140
View File
@@ -0,0 +1,140 @@
// @bun
import {
theme2
} from "./chunk-p6jvscj3.js";
// src/components/Modal.tsx
import { insertNode as _$insertNode } from "@opentui/solid";
import { effect as _$effect } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { use as _$use } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { RGBA } from "@opentui/core";
import { useBindings } from "@opentui/keymap/solid";
import { Portal, useRenderer, useTerminalDimensions } from "@opentui/solid";
import { createSignal, onCleanup, onMount, Show } from "solid-js";
function Modal2(props) {
return _$createComponent(Portal, {
get children() {
return _$createComponent(Show, {
get when() {
return props.open;
},
get children() {
return _$createComponent(ModalPanel, props);
}
});
}
});
}
function ModalPanel(props) {
const renderer = useRenderer();
const dimensions = useTerminalDimensions();
const [panel, setPanel] = createSignal();
const screenCap = () => Math.max(5, dimensions().height - 4);
const maxPanelHeight = () => Math.min(props.maxHeight ?? screenCap(), screenCap());
const maxContentHeight = () => Math.max(1, maxPanelHeight() - 4);
const maxPanelWidth = () => Math.max(10, dimensions().width - 4);
const panelWidth = () => Math.min(props.width ?? 50, maxPanelWidth());
let previousFocus = null;
onMount(() => {
previousFocus = renderer.currentFocusedRenderable;
props.onOpen?.(previousFocus);
panel()?.focus();
});
onCleanup(() => {
if (previousFocus && !previousFocus.isDestroyed)
previousFocus.focus();
});
const dismissKey = props.dismissKey ?? "escape";
if (dismissKey !== false || props.commands || props.bindings) {
useBindings(() => ({
target: panel,
commands: [...dismissKey !== false ? [{
name: "modal_dismiss",
desc: "close",
shortHelp: true,
run: () => props.onDismiss?.()
}] : [], ...props.commands ?? []],
bindings: [...dismissKey !== false ? [{
key: dismissKey,
cmd: "modal_dismiss"
}] : [], ...props.bindings ?? []]
}));
}
return (() => {
var _el$ = _$createElement("box"), _el$2 = _$createElement("box");
_$insertNode(_el$, _el$2);
_$setProp(_el$, "position", "absolute");
_$setProp(_el$, "top", 0);
_$setProp(_el$, "left", 0);
_$setProp(_el$, "alignItems", "center");
_$setProp(_el$, "justifyContent", "center");
_$setProp(_el$, "zIndex", 2000);
_$use(setPanel, _el$2);
_$setProp(_el$2, "focusable", true);
_$setProp(_el$2, "border", true);
_$setProp(_el$2, "flexDirection", "column");
_$setProp(_el$2, "paddingLeft", 2);
_$setProp(_el$2, "paddingRight", 2);
_$setProp(_el$2, "paddingTop", 1);
_$setProp(_el$2, "paddingBottom", 1);
_$insert(_el$2, _$createComponent(Show, {
get when() {
return props.scrollable;
},
get fallback() {
return (() => {
var _el$4 = _$createElement("box");
_$setProp(_el$4, "flexDirection", "column");
_$setProp(_el$4, "overflow", "hidden");
_$insert(_el$4, () => props.children);
_$effect((_$p) => _$setProp(_el$4, "maxHeight", maxContentHeight(), _$p));
return _el$4;
})();
},
get children() {
var _el$3 = _$createElement("scrollbox");
_$setProp(_el$3, "flexShrink", 1);
_$setProp(_el$3, "scrollY", true);
_$setProp(_el$3, "scrollX", false);
_$setProp(_el$3, "focusable", false);
_$insert(_el$3, () => props.children);
_$effect((_$p) => _$setProp(_el$3, "maxHeight", maxContentHeight(), _$p));
return _el$3;
}
}));
_$effect((_p$) => {
var _v$ = dimensions().width, _v$2 = dimensions().height, _v$3 = props.backdropColor ?? RGBA.fromInts(0, 0, 0, 150), _v$4 = theme2.mouse ? props.onDismiss : undefined, _v$5 = panelWidth(), _v$6 = maxPanelHeight(), _v$7 = theme2.borderStyle, _v$8 = props.accentColor ?? theme2.primary, _v$9 = props.accentColor ?? theme2.primary, _v$0 = props.backgroundColor ?? theme2.background, _v$1 = theme2.mouse ? (event) => event.stopPropagation() : undefined;
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "width", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "height", _v$2, _p$.t));
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "backgroundColor", _v$3, _p$.a));
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$, "onMouseDown", _v$4, _p$.o));
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$2, "width", _v$5, _p$.i));
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$2, "maxHeight", _v$6, _p$.n));
_v$7 !== _p$.s && (_p$.s = _$setProp(_el$2, "borderStyle", _v$7, _p$.s));
_v$8 !== _p$.h && (_p$.h = _$setProp(_el$2, "borderColor", _v$8, _p$.h));
_v$9 !== _p$.r && (_p$.r = _$setProp(_el$2, "focusedBorderColor", _v$9, _p$.r));
_v$0 !== _p$.d && (_p$.d = _$setProp(_el$2, "backgroundColor", _v$0, _p$.d));
_v$1 !== _p$.l && (_p$.l = _$setProp(_el$2, "onMouseDown", _v$1, _p$.l));
return _p$;
}, {
e: undefined,
t: undefined,
a: undefined,
o: undefined,
i: undefined,
n: undefined,
s: undefined,
h: undefined,
r: undefined,
d: undefined,
l: undefined
});
return _el$;
})();
}
export { Modal2 };
+5 -5
View File
@@ -1,7 +1,7 @@
// @bun
import {
theme
} from "./chunk-x7m82z3z.js";
theme2
} from "./chunk-p6jvscj3.js";
// src/components/SignalBars.tsx
import { effect as _$effect } from "@opentui/solid";
@@ -11,7 +11,7 @@ import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { For } from "solid-js";
var BARS = ["\u2581", "\u2583", "\u2585", "\u2587"];
function SignalBars(props) {
function SignalBars2(props) {
const level = () => Math.floor(props.signal * BARS.length / 100);
return (() => {
var _el$ = _$createElement("box");
@@ -21,7 +21,7 @@ function SignalBars(props) {
children: (bar, i) => (() => {
var _el$2 = _$createElement("text");
_$insert(_el$2, bar);
_$effect((_$p) => _$setProp(_el$2, "fg", i() < level() ? props.filledColor : props.emptyColor ?? theme.muted, _$p));
_$effect((_$p) => _$setProp(_el$2, "fg", i() < level() ? props.filledColor : props.emptyColor ?? theme2.muted, _$p));
return _el$2;
})()
}));
@@ -29,4 +29,4 @@ function SignalBars(props) {
})();
}
export { SignalBars };
export { SignalBars2 };
+7 -7
View File
@@ -1,7 +1,7 @@
// @bun
import {
theme
} from "./chunk-x7m82z3z.js";
theme2
} from "./chunk-p6jvscj3.js";
// src/components/Badge.tsx
import { insert as _$insert } from "@opentui/solid";
@@ -14,11 +14,11 @@ import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { TextAttributes } from "@opentui/core";
import { Show } from "solid-js";
function Badge(props) {
const textColor = () => props.textColor ?? theme.background;
const backgroundColor = () => props.backgroundColor ?? theme.background;
function Badge2(props) {
const textColor = () => props.textColor ?? theme2.background;
const backgroundColor = () => props.backgroundColor ?? theme2.background;
const attributes = () => props.bold === false ? undefined : TextAttributes.BOLD;
const nerdFonts = () => props.withNerdfont ?? theme.nerdFonts;
const nerdFonts = () => props.withNerdfont ?? theme2.nerdFonts;
return (() => {
var _el$ = _$createElement("box"), _el$4 = _$createElement("text");
_$insertNode(_el$, _el$4);
@@ -80,4 +80,4 @@ function Badge(props) {
})();
}
export { Badge };
export { Badge2 };
+42
View File
@@ -0,0 +1,42 @@
// @bun
import {
useFocusWithin2
} from "./chunk-59dzyjgd.js";
// src/components/Pane.tsx
import { createComponent as _$createComponent } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { use as _$use } from "@opentui/solid";
import { spread as _$spread } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { createContext, createSignal, splitProps, useContext } from "solid-js";
var PaneContext = createContext();
function Pane2(props) {
const [local, rest] = splitProps(props, ["handle", "children"]);
const [el, setEl] = createSignal();
const focused = useFocusWithin2(el);
const handle = {
target: el,
focused,
focus: () => el()?.focus()
};
local.handle?.(handle);
return _$createComponent(PaneContext.Provider, {
value: handle,
get children() {
var _el$ = _$createElement("box");
_$use(setEl, _el$);
_$spread(_el$, rest, true);
_$insert(_el$, () => local.children);
return _el$;
}
});
}
function usePane2() {
const pane = useContext(PaneContext);
if (!pane)
throw new Error("usePane() must be used inside a <Pane>");
return pane;
}
export { Pane2, usePane2 };
+9 -9
View File
@@ -1,7 +1,7 @@
// @bun
import {
theme
} from "./chunk-x7m82z3z.js";
theme2
} from "./chunk-p6jvscj3.js";
// src/components/Tabs.tsx
import { effect as _$effect } from "@opentui/solid";
@@ -14,7 +14,7 @@ import { createElement as _$createElement } from "@opentui/solid";
import { BorderChars, CliRenderEvents, TextAttributes } from "@opentui/core";
import { onResize, useRenderer } from "@opentui/solid";
import { createMemo, createSignal, Index } from "solid-js";
function stepTabValue(items, value, delta) {
function stepTabValue2(items, value, delta) {
const index = items.findIndex((t) => t.value === value);
if (index === -1)
return value;
@@ -58,7 +58,7 @@ function fitCount(items, start, available) {
}
return Math.max(count, 1);
}
function Tabs(props) {
function Tabs2(props) {
const renderer = useRenderer();
const [width, setWidth] = createSignal(0);
let container;
@@ -98,10 +98,10 @@ function Tabs(props) {
}
return shown;
});
const accent = () => props.accentColor ?? theme.primary;
const muted = () => props.mutedColor ?? theme.muted;
const accent = () => props.accentColor ?? theme2.primary;
const muted = () => props.mutedColor ?? theme2.muted;
const borderColor = () => props.focused ? accent() : muted();
const chars = BorderChars[theme.borderStyle];
const chars = BorderChars[theme2.borderStyle];
return (() => {
var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("box"), _el$4 = _$createElement("text");
_$insertNode(_el$, _el$2);
@@ -131,7 +131,7 @@ function Tabs(props) {
_$setProp(_el$6, "selectable", false);
_$insert(_el$6, () => ` ${s().label} `);
_$effect((_p$) => {
var _v$3 = s().isActive ? accent() : muted(), _v$4 = s().isActive ? TextAttributes.BOLD : undefined, _v$5 = theme.mouse && s().value !== undefined ? () => props.onChange(s().value) : undefined;
var _v$3 = s().isActive ? accent() : muted(), _v$4 = s().isActive ? TextAttributes.BOLD : undefined, _v$5 = theme2.mouse && s().value !== undefined ? () => props.onChange(s().value) : undefined;
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$6, "fg", _v$3, _p$.e));
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$6, "attributes", _v$4, _p$.t));
_v$5 !== _p$.a && (_p$.a = _$setProp(_el$6, "onMouseDown", _v$5, _p$.a));
@@ -165,4 +165,4 @@ function Tabs(props) {
})();
}
export { stepTabValue, Tabs };
export { stepTabValue2, Tabs2 };
+52
View File
@@ -0,0 +1,52 @@
// @bun
import {
theme2
} from "./chunk-p6jvscj3.js";
import {
Box2
} from "./chunk-8r4rajaz.js";
import {
Tabs2
} from "./chunk-vf2ybhtx.js";
// src/components/TabbedPanel.tsx
import { insert as _$insert } from "@opentui/solid";
import { memo as _$memo } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
function TabbedPanel2(props) {
return (() => {
var _el$ = _$createElement("box");
_$setProp(_el$, "flexDirection", "column");
_$setProp(_el$, "flexGrow", 1);
_$insert(_el$, _$createComponent(Tabs2, {
get items() {
return props.items;
},
get value() {
return props.value;
},
get onChange() {
return props.onChange;
},
get focused() {
return props.focused;
}
}), null);
_$insert(_el$, _$createComponent(Box2, {
flexGrow: 1,
border: ["left", "right", "bottom"],
get borderColor() {
return _$memo(() => !!props.focused)() ? theme2.primary : theme2.muted;
},
flexDirection: "column",
get children() {
return props.children;
}
}), null);
return _el$;
})();
}
export { TabbedPanel2 };
-45
View File
@@ -1,45 +0,0 @@
// @bun
import {
configPath,
readYamlFile
} from "./chunk-7f8jagy5.js";
import {
mergeConfig,
normalizeColors,
resolveBorderStyle
} from "./chunk-hcq62p48.js";
import {
buildPresets
} from "./chunk-cnhhssmz.js";
// src/index.ts
import { join } from "path";
function buildTheme() {
const defaultConfigPath = join(import.meta.dir, "default.yaml");
const base = readYamlFile(defaultConfigPath) ?? {};
const user = readYamlFile(configPath()) ?? {};
const merged = mergeConfig(base, user);
const colors = normalizeColors(merged.colors);
return {
...colors,
background: colors.base00,
subtleBg: colors.base01,
selection: colors.base02,
subtle: colors.base03,
muted: colors.base04,
text: colors.base05,
primary: colors.base0d,
success: colors.base0b,
warning: colors.base09,
error: colors.base08,
nerdFonts: merged.nerdFonts,
emojiFonts: merged.emojiFonts,
reducedMotion: merged.reducedMotion,
mouse: merged.mouse,
borderStyle: resolveBorderStyle(merged.border)
};
}
var theme = buildTheme();
var presets = buildPresets(theme);
export { theme, presets };
+10
View File
@@ -0,0 +1,10 @@
// @bun
// src/yaml.ts
import { homedir } from "os";
import { join } from "path";
function configPath2() {
const configDir = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
return join(configDir, "ilovetui", "config.yaml");
}
export { configPath2 };
+2 -2
View File
@@ -1,6 +1,6 @@
// @bun
// src/presets.ts
function buildPresets(theme) {
function buildPresets2(theme) {
const select = {
backgroundColor: theme.background,
textColor: theme.text,
@@ -41,4 +41,4 @@ function buildPresets(theme) {
return { select, tabSelect, input, textarea: input, box, slider };
}
export { buildPresets };
export { buildPresets2 };
+6 -6
View File
@@ -1,7 +1,7 @@
// @bun
import {
theme
} from "./chunk-x7m82z3z.js";
theme2
} from "./chunk-p6jvscj3.js";
// src/components/ProgressBar.tsx
import { effect as _$effect } from "@opentui/solid";
@@ -23,9 +23,9 @@ function sampleGradient(stops, t) {
const [br, bg, bb, ba] = parseColor(stops[i + 1]).toInts();
return RGBA.fromInts(Math.round(ar + (br - ar) * localT), Math.round(ag + (bg - ag) * localT), Math.round(ab + (bb - ab) * localT), Math.round(aa + (ba - aa) * localT));
}
function ProgressBar(props) {
function ProgressBar2(props) {
const width = () => props.width ?? 20;
const stops = createMemo(() => Array.isArray(props.color) ? props.color : [props.color ?? theme.primary]);
const stops = createMemo(() => Array.isArray(props.color) ? props.color : [props.color ?? theme2.primary]);
const filled = createMemo(() => Math.round(width() * Math.min(Math.max(props.value, 0), 100) / 100));
const cells = createMemo(() => Array.from({
length: width()
@@ -46,7 +46,7 @@ function ProgressBar(props) {
var _c$ = _$memo(() => !!isFilled());
return () => _c$() ? props.fillChar ?? "\u2588" : props.trackChar ?? "\u2591";
})());
_$effect((_$p) => _$setProp(_el$2, "fg", isFilled() ? sampleGradient(stops(), t) : props.trackColor ?? theme.muted, _$p));
_$effect((_$p) => _$setProp(_el$2, "fg", isFilled() ? sampleGradient(stops(), t) : props.trackColor ?? theme2.muted, _$p));
return _el$2;
})();
}
@@ -55,4 +55,4 @@ function ProgressBar(props) {
})();
}
export { ProgressBar };
export { ProgressBar2 };
-100
View File
@@ -1,100 +0,0 @@
// @bun
import {
theme
} from "./chunk-x7m82z3z.js";
// src/components/Modal.tsx
import { insertNode as _$insertNode } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { effect as _$effect } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { RGBA } from "@opentui/core";
import { Portal, useTerminalDimensions } from "@opentui/solid";
import { Show } from "solid-js";
function Modal(props) {
const dimensions = useTerminalDimensions();
const screenCap = () => Math.max(5, dimensions().height - 4);
const maxPanelHeight = () => Math.min(props.maxHeight ?? screenCap(), screenCap());
const maxContentHeight = () => Math.max(1, maxPanelHeight() - 4);
const maxPanelWidth = () => Math.max(10, dimensions().width - 4);
const panelWidth = () => Math.min(props.width ?? 50, maxPanelWidth());
return _$createComponent(Portal, {
get children() {
return _$createComponent(Show, {
get when() {
return props.open;
},
get children() {
var _el$ = _$createElement("box"), _el$2 = _$createElement("box");
_$insertNode(_el$, _el$2);
_$setProp(_el$, "position", "absolute");
_$setProp(_el$, "top", 0);
_$setProp(_el$, "left", 0);
_$setProp(_el$, "alignItems", "center");
_$setProp(_el$, "justifyContent", "center");
_$setProp(_el$, "zIndex", 2000);
_$setProp(_el$2, "border", true);
_$setProp(_el$2, "flexDirection", "column");
_$setProp(_el$2, "paddingLeft", 2);
_$setProp(_el$2, "paddingRight", 2);
_$setProp(_el$2, "paddingTop", 1);
_$setProp(_el$2, "paddingBottom", 1);
_$insert(_el$2, _$createComponent(Show, {
get when() {
return props.scrollable;
},
get fallback() {
return (() => {
var _el$4 = _$createElement("box");
_$setProp(_el$4, "flexDirection", "column");
_$setProp(_el$4, "overflow", "hidden");
_$insert(_el$4, () => props.children);
_$effect((_$p) => _$setProp(_el$4, "maxHeight", maxContentHeight(), _$p));
return _el$4;
})();
},
get children() {
var _el$3 = _$createElement("scrollbox");
_$setProp(_el$3, "flexShrink", 1);
_$setProp(_el$3, "scrollY", true);
_$setProp(_el$3, "scrollX", false);
_$insert(_el$3, () => props.children);
_$effect((_$p) => _$setProp(_el$3, "maxHeight", maxContentHeight(), _$p));
return _el$3;
}
}));
_$effect((_p$) => {
var _v$ = dimensions().width, _v$2 = dimensions().height, _v$3 = props.backdropColor ?? RGBA.fromInts(0, 0, 0, 150), _v$4 = theme.mouse ? props.onDismiss : undefined, _v$5 = panelWidth(), _v$6 = maxPanelHeight(), _v$7 = theme.borderStyle, _v$8 = props.accentColor ?? theme.primary, _v$9 = props.backgroundColor ?? theme.background, _v$0 = theme.mouse ? (event) => event.stopPropagation() : undefined;
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "width", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "height", _v$2, _p$.t));
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "backgroundColor", _v$3, _p$.a));
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$, "onMouseDown", _v$4, _p$.o));
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$2, "width", _v$5, _p$.i));
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$2, "maxHeight", _v$6, _p$.n));
_v$7 !== _p$.s && (_p$.s = _$setProp(_el$2, "borderStyle", _v$7, _p$.s));
_v$8 !== _p$.h && (_p$.h = _$setProp(_el$2, "borderColor", _v$8, _p$.h));
_v$9 !== _p$.r && (_p$.r = _$setProp(_el$2, "backgroundColor", _v$9, _p$.r));
_v$0 !== _p$.d && (_p$.d = _$setProp(_el$2, "onMouseDown", _v$0, _p$.d));
return _p$;
}, {
e: undefined,
t: undefined,
a: undefined,
o: undefined,
i: undefined,
n: undefined,
s: undefined,
h: undefined,
r: undefined,
d: undefined
});
return _el$;
}
});
}
});
}
export { Modal };
+8 -7
View File
@@ -1,11 +1,12 @@
// @bun
import {
Badge
} from "../chunk-bscbaa87.js";
import"../chunk-x7m82z3z.js";
import"../chunk-7f8jagy5.js";
import"../chunk-hcq62p48.js";
import"../chunk-cnhhssmz.js";
Badge2
} from "../chunk-v9cetqzr.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
Badge
Badge2 as Badge
};
+8 -7
View File
@@ -1,11 +1,12 @@
// @bun
import {
Box
} from "../chunk-ynqvwj4d.js";
import"../chunk-x7m82z3z.js";
import"../chunk-7f8jagy5.js";
import"../chunk-hcq62p48.js";
import"../chunk-cnhhssmz.js";
Box2
} from "../chunk-8r4rajaz.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
Box
Box2 as Box
};
+8 -7
View File
@@ -1,11 +1,12 @@
// @bun
import {
HelpBar
} from "../chunk-rvq53a2a.js";
import"../chunk-x7m82z3z.js";
import"../chunk-7f8jagy5.js";
import"../chunk-hcq62p48.js";
import"../chunk-cnhhssmz.js";
HelpBar2
} from "../chunk-c1dx8cn3.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
HelpBar
HelpBar2 as HelpBar
};
+10 -9
View File
@@ -1,13 +1,14 @@
// @bun
import {
HelpModal
} from "../chunk-xxg67vfz.js";
import"../chunk-zjfxbvmq.js";
import"../chunk-x7m82z3z.js";
import"../chunk-7f8jagy5.js";
import"../chunk-hcq62p48.js";
import"../chunk-cnhhssmz.js";
import"../chunk-cyvpvpws.js";
HelpModal2
} from "../chunk-ersd5rrs.js";
import"../chunk-t2cszvff.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
import"../chunk-ee2qaswx.js";
export {
HelpModal
HelpModal2 as HelpModal
};
+12
View File
@@ -0,0 +1,12 @@
// @bun
import {
KeyValue2
} from "../chunk-c4gp0x16.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
KeyValue2 as KeyValue
};
+12
View File
@@ -0,0 +1,12 @@
// @bun
import {
ListRow2
} from "../chunk-sh9n4zha.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
ListRow2 as ListRow
};
+8 -7
View File
@@ -1,11 +1,12 @@
// @bun
import {
MinSizeGuard
} from "../chunk-eta27h3c.js";
import"../chunk-x7m82z3z.js";
import"../chunk-7f8jagy5.js";
import"../chunk-hcq62p48.js";
import"../chunk-cnhhssmz.js";
MinSizeGuard2
} from "../chunk-qdyd8q9r.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
MinSizeGuard
MinSizeGuard2 as MinSizeGuard
};
+8 -7
View File
@@ -1,11 +1,12 @@
// @bun
import {
Modal
} from "../chunk-zjfxbvmq.js";
import"../chunk-x7m82z3z.js";
import"../chunk-7f8jagy5.js";
import"../chunk-hcq62p48.js";
import"../chunk-cnhhssmz.js";
Modal2
} from "../chunk-t2cszvff.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
Modal
Modal2 as Modal
};
+8 -8
View File
@@ -1,12 +1,12 @@
// @bun
import {
NotificationHost
} from "../chunk-fqzxyvhv.js";
import"../chunk-x7m82z3z.js";
import"../chunk-7f8jagy5.js";
import"../chunk-hcq62p48.js";
import"../chunk-cnhhssmz.js";
import"../chunk-y6txxzxk.js";
NotificationHost2
} from "../chunk-ajb193xj.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
NotificationHost
NotificationHost2 as NotificationHost
};
+10
View File
@@ -0,0 +1,10 @@
// @bun
import {
Pane2,
usePane2
} from "../chunk-vazj633p.js";
import"../chunk-59dzyjgd.js";
export {
Pane2 as Pane,
usePane2 as usePane
};
+8 -7
View File
@@ -1,11 +1,12 @@
// @bun
import {
ProgressBar
} from "../chunk-5m64a9kb.js";
import"../chunk-x7m82z3z.js";
import"../chunk-7f8jagy5.js";
import"../chunk-hcq62p48.js";
import"../chunk-cnhhssmz.js";
ProgressBar2
} from "../chunk-z734n0wr.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
ProgressBar
ProgressBar2 as ProgressBar
};
+81
View File
@@ -0,0 +1,81 @@
// @bun
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-yyg967q8.js";
import"../chunk-xykm10y9.js";
import {
theme2
} from "../chunk-p6jvscj3.js";
import {
Spinner2
} from "../chunk-kmqqzz9e.js";
// src/components/SectionHeader.tsx
import { createTextNode as _$createTextNode } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { effect as _$effect } from "@opentui/solid";
import { insertNode as _$insertNode } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { memo as _$memo } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { TextAttributes } from "@opentui/core";
import { Show } from "solid-js";
function SectionHeader(props) {
return (() => {
var _el$ = _$createElement("box"), _el$2 = _$createElement("box"), _el$3 = _$createElement("text"), _el$4 = _$createElement("box");
_$insertNode(_el$, _el$2);
_$insertNode(_el$, _el$4);
_$setProp(_el$, "flexShrink", 0);
_$setProp(_el$, "flexDirection", "row");
_$setProp(_el$, "justifyContent", "space-between");
_$setProp(_el$, "alignItems", "center");
_$setProp(_el$, "height", 1);
_$insertNode(_el$2, _el$3);
_$setProp(_el$2, "flexGrow", 1);
_$setProp(_el$2, "flexShrink", 1);
_$setProp(_el$2, "minWidth", 0);
_$setProp(_el$2, "marginRight", 1);
_$setProp(_el$2, "overflow", "hidden");
_$setProp(_el$3, "wrapMode", "none");
_$insert(_el$3, () => props.title ?? "");
_$setProp(_el$4, "flexShrink", 0);
_$insert(_el$4, _$createComponent(Show, {
get when() {
return !props.loading;
},
get fallback() {
return (() => {
var _el$6 = _$createElement("box"), _el$7 = _$createElement("text"), _el$8 = _$createTextNode(` `);
_$insertNode(_el$6, _el$7);
_$setProp(_el$6, "flexDirection", "row");
_$setProp(_el$6, "alignItems", "center");
_$insert(_el$6, _$createComponent(Spinner2, {}), _el$7);
_$insertNode(_el$7, _el$8);
_$insert(_el$7, () => props.loadingLabel ?? "Loading\u2026", null);
_$effect((_$p) => _$setProp(_el$7, "fg", theme2.muted, _$p));
return _el$6;
})();
},
get children() {
var _el$5 = _$createElement("text");
_$insert(_el$5, () => props.hint ?? "");
_$effect((_$p) => _$setProp(_el$5, "fg", theme2.muted, _$p));
return _el$5;
}
}));
_$effect((_p$) => {
var _v$ = theme2.muted, _v$2 = TextAttributes.BOLD;
_v$ !== _p$.e && (_p$.e = _$setProp(_el$3, "fg", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$3, "attributes", _v$2, _p$.t));
return _p$;
}, {
e: undefined,
t: undefined
});
return _el$;
})();
}
export {
SectionHeader
};
+8 -7
View File
@@ -1,11 +1,12 @@
// @bun
import {
Sidebar
} from "../chunk-15vweq1q.js";
import"../chunk-x7m82z3z.js";
import"../chunk-7f8jagy5.js";
import"../chunk-hcq62p48.js";
import"../chunk-cnhhssmz.js";
Sidebar2
} from "../chunk-rcywynbw.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
Sidebar
Sidebar2 as Sidebar
};
+8 -7
View File
@@ -1,11 +1,12 @@
// @bun
import {
SignalBars
} from "../chunk-dctn29v0.js";
import"../chunk-x7m82z3z.js";
import"../chunk-7f8jagy5.js";
import"../chunk-hcq62p48.js";
import"../chunk-cnhhssmz.js";
SignalBars2
} from "../chunk-tz3vvffs.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
SignalBars
SignalBars2 as SignalBars
};
+8 -35
View File
@@ -1,39 +1,12 @@
// @bun
import {
theme
} from "../chunk-x7m82z3z.js";
import"../chunk-7f8jagy5.js";
import"../chunk-hcq62p48.js";
import"../chunk-cnhhssmz.js";
// src/components/Spinner.tsx
import { setProp as _$setProp } from "@opentui/solid";
import { effect as _$effect } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import"opentui-spinner/solid";
function Spinner(props) {
return (() => {
var _el$ = _$createElement("spinner");
_$effect((_p$) => {
var { name: _v$, frames: _v$2, interval: _v$3 } = props, _v$4 = props.autoplay ?? !theme.reducedMotion, _v$5 = props.color ?? theme.primary, _v$6 = props.backgroundColor;
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "name", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "frames", _v$2, _p$.t));
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "interval", _v$3, _p$.a));
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$, "autoplay", _v$4, _p$.o));
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$, "color", _v$5, _p$.i));
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$, "backgroundColor", _v$6, _p$.n));
return _p$;
}, {
e: undefined,
t: undefined,
a: undefined,
o: undefined,
i: undefined,
n: undefined
});
return _el$;
})();
}
Spinner2
} from "../chunk-kmqqzz9e.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
Spinner
Spinner2 as Spinner
};
+14
View File
@@ -0,0 +1,14 @@
// @bun
import {
TabbedPanel2
} from "../chunk-wtbg0e39.js";
import"../chunk-vf2ybhtx.js";
import"../chunk-8r4rajaz.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
TabbedPanel2 as TabbedPanel
};
+10 -9
View File
@@ -1,13 +1,14 @@
// @bun
import {
Tabs,
stepTabValue
} from "../chunk-ev4wvhd2.js";
import"../chunk-x7m82z3z.js";
import"../chunk-7f8jagy5.js";
import"../chunk-hcq62p48.js";
import"../chunk-cnhhssmz.js";
stepTabValue2,
Tabs2
} from "../chunk-vf2ybhtx.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
stepTabValue,
Tabs
Tabs2 as Tabs,
stepTabValue2 as stepTabValue
};
+9 -8
View File
@@ -1,12 +1,13 @@
// @bun
import {
Toggle
} from "../chunk-mezyz219.js";
import"../chunk-bscbaa87.js";
import"../chunk-x7m82z3z.js";
import"../chunk-7f8jagy5.js";
import"../chunk-hcq62p48.js";
import"../chunk-cnhhssmz.js";
Toggle2
} from "../chunk-qhp6k176.js";
import"../chunk-v9cetqzr.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
Toggle
Toggle2 as Toggle
};
+13 -12
View File
@@ -1,17 +1,18 @@
// @bun
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-yyg967q8.js";
import"../chunk-xykm10y9.js";
import"../chunk-p6jvscj3.js";
import {
HelpModal
} from "../chunk-xxg67vfz.js";
import"../chunk-zjfxbvmq.js";
HelpBar2
} from "../chunk-c1dx8cn3.js";
import"../chunk-ee2qaswx.js";
import"../chunk-t2cszvff.js";
import {
HelpBar
} from "../chunk-rvq53a2a.js";
import"../chunk-x7m82z3z.js";
import"../chunk-7f8jagy5.js";
import"../chunk-hcq62p48.js";
import"../chunk-cnhhssmz.js";
import"../chunk-cyvpvpws.js";
HelpModal2
} from "../chunk-ersd5rrs.js";
export {
HelpModal,
HelpBar
HelpBar2 as HelpBar,
HelpModal2 as HelpModal
};
+56 -37
View File
@@ -1,50 +1,69 @@
// @bun
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-yyg967q8.js";
import"../chunk-xykm10y9.js";
import"../chunk-p6jvscj3.js";
import {
NotificationHost
} from "../chunk-fqzxyvhv.js";
Badge2
} from "../chunk-v9cetqzr.js";
import {
Sidebar
} from "../chunk-15vweq1q.js";
Box2
} from "../chunk-8r4rajaz.js";
import {
Toggle
} from "../chunk-mezyz219.js";
KeyValue2
} from "../chunk-c4gp0x16.js";
import {
MinSizeGuard
} from "../chunk-eta27h3c.js";
ListRow2
} from "../chunk-sh9n4zha.js";
import {
Tabs,
stepTabValue
} from "../chunk-ev4wvhd2.js";
MinSizeGuard2
} from "../chunk-qdyd8q9r.js";
import {
Box
} from "../chunk-ynqvwj4d.js";
Modal2
} from "../chunk-t2cszvff.js";
import {
Modal
} from "../chunk-zjfxbvmq.js";
NotificationHost2
} from "../chunk-ajb193xj.js";
import"../chunk-59dzyjgd.js";
import {
Badge
} from "../chunk-bscbaa87.js";
Pane2,
usePane2
} from "../chunk-vazj633p.js";
import {
ProgressBar
} from "../chunk-5m64a9kb.js";
ProgressBar2
} from "../chunk-z734n0wr.js";
import {
SignalBars
} from "../chunk-dctn29v0.js";
import"../chunk-x7m82z3z.js";
import"../chunk-7f8jagy5.js";
import"../chunk-hcq62p48.js";
import"../chunk-cnhhssmz.js";
import"../chunk-y6txxzxk.js";
Sidebar2
} from "../chunk-rcywynbw.js";
import {
SignalBars2
} from "../chunk-tz3vvffs.js";
import {
stepTabValue2,
Tabs2
} from "../chunk-vf2ybhtx.js";
import {
TabbedPanel2
} from "../chunk-wtbg0e39.js";
import {
Toggle2
} from "../chunk-qhp6k176.js";
export {
stepTabValue,
Toggle,
Tabs,
SignalBars,
Sidebar,
ProgressBar,
NotificationHost,
Modal,
MinSizeGuard,
Box,
Badge
Badge2 as Badge,
Box2 as Box,
KeyValue2 as KeyValue,
ListRow2 as ListRow,
MinSizeGuard2 as MinSizeGuard,
Modal2 as Modal,
NotificationHost2 as NotificationHost,
Pane2 as Pane,
ProgressBar2 as ProgressBar,
Sidebar2 as Sidebar,
SignalBars2 as SignalBars,
TabbedPanel2 as TabbedPanel,
Tabs2 as Tabs,
Toggle2 as Toggle,
stepTabValue2 as stepTabValue,
usePane2 as usePane
};
+13 -13
View File
@@ -1,17 +1,17 @@
// @bun
import {
COLOR_KEYS,
mergeColors,
mergeConfig,
normalizeColor,
normalizeColors,
resolveBorderStyle
} from "./chunk-hcq62p48.js";
COLOR_KEYS2,
OPENTUI_BORDER_STYLES2,
ThemeConfigSchema2,
layerConfigs2,
resolveConfig2,
normalizeColor2
} from "./chunk-njvj322f.js";
export {
resolveBorderStyle,
normalizeColors,
normalizeColor,
mergeConfig,
mergeColors,
COLOR_KEYS
COLOR_KEYS2 as COLOR_KEYS,
OPENTUI_BORDER_STYLES2 as OPENTUI_BORDER_STYLES,
ThemeConfigSchema2 as ThemeConfigSchema,
layerConfigs2 as layerConfigs,
normalizeColor2 as normalizeColor,
resolveConfig2 as resolveConfig
};
+9
View File
@@ -0,0 +1,9 @@
// @bun
import {
isInside2,
useFocusWithin2
} from "../chunk-59dzyjgd.js";
export {
isInside2 as isInside,
useFocusWithin2 as useFocusWithin
};
+5 -5
View File
@@ -1,9 +1,9 @@
// @bun
import {
helpOpen,
toggleHelp
} from "../chunk-cyvpvpws.js";
helpOpen2,
toggleHelp2
} from "../chunk-ee2qaswx.js";
export {
toggleHelp,
helpOpen
helpOpen2 as helpOpen,
toggleHelp2 as toggleHelp
};
+3 -3
View File
@@ -1,7 +1,7 @@
// @bun
import {
createSimpleContext
} from "../chunk-2px9e43p.js";
createSimpleContext2
} from "../chunk-jza406qg.js";
export {
createSimpleContext
createSimpleContext2 as createSimpleContext
};
+21 -15
View File
@@ -1,21 +1,27 @@
// @bun
import {
createSimpleContext
} from "../chunk-2px9e43p.js";
createSimpleContext2
} from "../chunk-jza406qg.js";
import {
helpOpen,
toggleHelp
} from "../chunk-cyvpvpws.js";
notifications2,
notify2,
dismiss2
} from "../chunk-6ggk5fg7.js";
import {
dismiss,
notifications,
notify
} from "../chunk-y6txxzxk.js";
helpOpen2,
toggleHelp2
} from "../chunk-ee2qaswx.js";
import {
isInside2,
useFocusWithin2
} from "../chunk-59dzyjgd.js";
export {
toggleHelp,
notify,
notifications,
helpOpen,
dismiss,
createSimpleContext
createSimpleContext2 as createSimpleContext,
dismiss2 as dismiss,
helpOpen2 as helpOpen,
isInside2 as isInside,
notifications2 as notifications,
notify2 as notify,
toggleHelp2 as toggleHelp,
useFocusWithin2 as useFocusWithin
};
+7 -7
View File
@@ -1,11 +1,11 @@
// @bun
import {
dismiss,
notifications,
notify
} from "../chunk-y6txxzxk.js";
notifications2,
notify2,
dismiss2
} from "../chunk-6ggk5fg7.js";
export {
notify,
notifications,
dismiss
dismiss2 as dismiss,
notifications2 as notifications,
notify2 as notify
};
+18 -10
View File
@@ -1,15 +1,23 @@
// @bun
import {
presets,
theme
} from "./chunk-x7m82z3z.js";
theme2,
presets2,
configureTheme2,
icon2
} from "./chunk-p6jvscj3.js";
import"./chunk-yyg967q8.js";
import {
configPath
} from "./chunk-7f8jagy5.js";
import"./chunk-hcq62p48.js";
import"./chunk-cnhhssmz.js";
ThemeConfigSchema2
} from "./chunk-njvj322f.js";
import"./chunk-6ggk5fg7.js";
import {
configPath2
} from "./chunk-xykm10y9.js";
export {
theme,
presets,
configPath
ThemeConfigSchema2 as ThemeConfigSchema,
configPath2 as configPath,
configureTheme2 as configureTheme,
icon2 as icon,
presets2 as presets,
theme2 as theme
};
+4 -4
View File
@@ -19,9 +19,9 @@ function KeymapProvider(props) {
});
}
export {
useKeymapSelector,
useKeymap,
useBindings,
KeymapProvider,
reactiveMatcherFromSignal,
KeymapProvider
useBindings,
useKeymap,
useKeymapSelector
};
+3 -3
View File
@@ -1,7 +1,7 @@
// @bun
import {
buildPresets
} from "./chunk-cnhhssmz.js";
buildPresets2
} from "./chunk-yyg967q8.js";
export {
buildPresets
buildPresets2 as buildPresets
};
+47 -5
View File
@@ -1,7 +1,7 @@
// @bun
import {
notify
} from "./chunk-y6txxzxk.js";
notify2
} from "./chunk-6ggk5fg7.js";
// src/project-config.ts
import { existsSync, readFileSync } from "fs";
@@ -19,7 +19,7 @@ function readYaml(path) {
function warn(projectName, path, message) {
const text = `${path}: ${message}`;
console.error(`[${projectName} config] ${text}`);
notify(text, { kind: "warning", duration: 0 });
notify2(text, { kind: "warning", duration: 0 });
}
function loadProjectConfig(options) {
const { projectName, defaultConfigPath, defaultSchema, userSchema } = options;
@@ -54,6 +54,47 @@ ${z.prettifyError(defaultsResult.error)}`);
})();
return { defaults: defaultsResult.data, user };
}
function isPlainObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function deepMerge(base, over) {
if (!isPlainObject(base) || !isPlainObject(over))
return over === undefined ? base : over;
const out = { ...base };
for (const [key, value] of Object.entries(over))
out[key] = deepMerge(base[key], value);
return out;
}
function loadMergedProjectConfig(options) {
const { projectName, defaultConfigPath, schema } = options;
let rawDefaults;
try {
rawDefaults = readYaml(defaultConfigPath);
} catch (error) {
throw new Error(`Failed to read ${defaultConfigPath}: ${error instanceof Error ? error.message : error}`);
}
const defaults = schema.safeParse(rawDefaults);
if (!defaults.success) {
throw new Error(`${defaultConfigPath} is invalid:
${z.prettifyError(defaults.error)}`);
}
const path = userConfigPath(projectName);
if (!existsSync(path))
return defaults.data;
let rawUser;
try {
rawUser = readYaml(path);
} catch (error) {
warn(projectName, path, error instanceof Error ? error.message : String(error));
return defaults.data;
}
const merged = schema.safeParse(deepMerge(rawDefaults, rawUser ?? {}));
if (!merged.success) {
warn(projectName, path, z.prettifyError(merged.error));
return defaults.data;
}
return merged.data;
}
function keybindsSchema(names, required) {
const shape = {};
for (const name of names) {
@@ -69,7 +110,8 @@ function mergeKeybinds(names, defaults, user) {
return merged;
}
export {
mergeKeybinds,
keybindsSchema,
loadMergedProjectConfig,
loadProjectConfig,
keybindsSchema
mergeKeybinds
};
+19 -16
View File
@@ -1,11 +1,12 @@
// @bun
import"./chunk-njvj322f.js";
import"./chunk-6ggk5fg7.js";
import"./chunk-yyg967q8.js";
import"./chunk-xykm10y9.js";
import {
presets,
theme
} from "./chunk-x7m82z3z.js";
import"./chunk-7f8jagy5.js";
import"./chunk-hcq62p48.js";
import"./chunk-cnhhssmz.js";
theme2,
presets2
} from "./chunk-p6jvscj3.js";
// src/solid.ts
import { extend } from "@opentui/solid";
@@ -23,23 +24,25 @@ function withThemeDefaults(Ctor, defaults, after) {
class Themed extends Ctor {
constructor(...args) {
super(...args);
Object.assign(this, defaults);
Object.assign(this, defaults());
after?.(this);
}
}
return Themed;
}
var { borderStyle, borderColor, focusedBorderColor } = presets.box;
var boxDefaults = { borderStyle, borderColor, focusedBorderColor };
var boxDefaults = () => {
const { borderStyle, borderColor, focusedBorderColor } = presets2.box;
return { borderStyle, borderColor, focusedBorderColor };
};
extend({
box: withThemeDefaults(BoxRenderable, boxDefaults, (instance) => {
instance.border = false;
}),
select: withThemeDefaults(SelectRenderable, presets.select),
tab_select: withThemeDefaults(TabSelectRenderable, presets.tabSelect),
input: withThemeDefaults(InputRenderable, presets.input),
textarea: withThemeDefaults(TextareaRenderable, presets.textarea),
slider: withThemeDefaults(SliderRenderable, presets.slider),
text: withThemeDefaults(TextRenderable, { fg: theme.text }),
ascii_font: withThemeDefaults(ASCIIFontRenderable, { color: theme.primary })
select: withThemeDefaults(SelectRenderable, () => presets2.select),
tab_select: withThemeDefaults(TabSelectRenderable, () => presets2.tabSelect),
input: withThemeDefaults(InputRenderable, () => presets2.input),
textarea: withThemeDefaults(TextareaRenderable, () => presets2.textarea),
slider: withThemeDefaults(SliderRenderable, () => presets2.slider),
text: withThemeDefaults(TextRenderable, () => ({ fg: theme2.text })),
ascii_font: withThemeDefaults(ASCIIFontRenderable, () => ({ color: theme2.primary }))
});
+3 -5
View File
@@ -1,9 +1,7 @@
// @bun
import {
configPath,
readYamlFile
} from "./chunk-7f8jagy5.js";
configPath2
} from "./chunk-xykm10y9.js";
export {
readYamlFile,
configPath
configPath2 as configPath
};
+2 -1
View File
@@ -12,7 +12,8 @@
"./keymap": { "types": "./src/keymap/provider.tsx", "default": "./dist/keymap/provider.js" },
"./components": { "types": "./src/components/index.ts", "default": "./dist/components/index.js" },
"./components/spinner": { "types": "./src/components/Spinner.tsx", "default": "./dist/components/Spinner.js" },
"./components/help": { "types": "./src/components/help.ts", "default": "./dist/components/help.js" }
"./components/help": { "types": "./src/components/help.ts", "default": "./dist/components/help.js" },
"./components/section-header": { "types": "./src/components/SectionHeader.tsx", "default": "./dist/components/SectionHeader.js" }
},
"private": true,
"files": ["dist", "src"],
+12 -2
View File
@@ -1,6 +1,7 @@
import { type ColorInput, TextAttributes } from "@opentui/core";
import { type ColorInput, type Renderable, TextAttributes } from "@opentui/core";
import { formatCommandBindings } from "@opentui/keymap/extras";
import { useKeymapSelector } from "@opentui/keymap/solid";
import { createSignal } from "solid-js";
import { helpOpen, toggleHelp } from "../context/help.ts";
import { Modal } from "./Modal.tsx";
@@ -11,6 +12,8 @@ export interface HelpModalProps {
backgroundColor?: ColorInput;
backdropColor?: ColorInput;
width?: number;
/** See ModalProps.dismissKey. */
dismissKey?: string | false;
}
interface HelpEntry {
@@ -20,8 +23,13 @@ interface HelpEntry {
}
export function HelpModal(props: HelpModalProps = {}) {
// The modal takes focus when it opens, which would make its own "close"
// the only active command. List what was active where focus was *before*
// it opened instead.
const [scope, setScope] = createSignal<Renderable | null>(null);
const entries = useKeymapSelector((keymap): HelpEntry[] =>
keymap.getCommandEntries({ visibility: "active" }).map((entry) => ({
keymap.getCommandEntries({ visibility: "active", focused: scope() }).map((entry) => ({
command: entry.command.name,
keys: formatCommandBindings(entry.bindings, { keyNameAliases: KEY_DISPLAY }) ?? "",
label: typeof entry.command.desc === "string" ? entry.command.desc : entry.command.name,
@@ -32,6 +40,8 @@ export function HelpModal(props: HelpModalProps = {}) {
<Modal
open={helpOpen()}
onDismiss={toggleHelp}
onOpen={(previous) => setScope(() => previous)}
dismissKey={props.dismissKey}
accentColor={props.accentColor}
backgroundColor={props.backgroundColor}
backdropColor={props.backdropColor}
+23
View File
@@ -0,0 +1,23 @@
import type { ColorInput } from "@opentui/core";
import { Show, type JSX } from "solid-js";
import { theme } from "../index.ts";
export interface KeyValueProps {
label: string;
/** Plain value; ignored when children are given. */
value?: string | number;
valueColor?: ColorInput;
children?: JSX.Element;
}
/** A "Label ........ value" line, as used in details panels and modals. */
export function KeyValue(props: KeyValueProps) {
return (
<box flexShrink={0} flexDirection="row" justifyContent="space-between">
<text fg={theme.muted}>{props.label}</text>
<Show when={props.children} fallback={<text fg={props.valueColor}>{props.value ?? ""}</text>}>
{props.children}
</Show>
</box>
);
}
+49
View File
@@ -0,0 +1,49 @@
import { type ColorInput, TextAttributes } from "@opentui/core";
import { Show, type JSX } from "solid-js";
import { presets, theme } from "../index.ts";
/** Returns the selected-row text color when the row is selected, `fallback` otherwise. */
export type RowTone = (fallback?: ColorInput) => ColorInput | undefined;
export interface ListRowProps {
id?: string;
label: string;
selected: boolean;
bold?: boolean;
onClick?: () => void;
/** Right-aligned content; use `tone` for its colors so it follows the selection. */
right?: (tone: RowTone) => JSX.Element;
/** Extra lines under the main one. */
below?: (tone: RowTone) => JSX.Element;
}
/** One row of a keyboard-navigable list: truncated label on the left, details on the right. */
export function ListRow(props: ListRowProps) {
const tone: RowTone = (fallback) => (props.selected ? presets.select.selectedTextColor : fallback);
return (
<box
id={props.id}
flexShrink={0}
flexDirection="column"
paddingLeft={1}
paddingRight={1}
backgroundColor={props.selected ? presets.select.selectedBackgroundColor : "transparent"}
onMouseDown={theme.mouse && props.onClick ? () => props.onClick?.() : undefined}
>
<box flexDirection="row" justifyContent="space-between" alignItems="center" height={1}>
<box flexGrow={1} flexShrink={1} minWidth={0} marginRight={1} overflow="hidden">
<text fg={tone()} attributes={props.bold ? TextAttributes.BOLD : undefined} wrapMode="none">
{props.label}
</text>
</box>
<Show when={props.right}>
<box flexShrink={0} flexDirection="row" alignItems="center">
{props.right?.(tone)}
</box>
</Show>
</box>
{props.below?.(tone)}
</box>
);
}
+62 -8
View File
@@ -1,11 +1,26 @@
import { type ColorInput, type MouseEvent, RGBA } from "@opentui/core";
import { Portal, useTerminalDimensions } from "@opentui/solid";
import { Show, type ParentProps } from "solid-js";
import { type BoxRenderable, type ColorInput, type KeyEvent, type MouseEvent, type Renderable, RGBA } from "@opentui/core";
import type { Binding, Command } from "@opentui/keymap";
import { useBindings } from "@opentui/keymap/solid";
import { Portal, useRenderer, useTerminalDimensions } from "@opentui/solid";
import { createSignal, onCleanup, onMount, Show, type ParentProps } from "solid-js";
import { theme } from "../index.ts";
export type ModalCommand = Command<Renderable, KeyEvent>;
export type ModalBinding = Binding<Renderable, KeyEvent>;
export interface ModalProps extends ParentProps {
open: boolean;
onDismiss?: () => void;
/**
* Key that calls onDismiss while the modal has focus (default "escape").
* `false` disables it. Needs a KeymapProvider unless disabled.
*/
dismissKey?: string | false;
/** Extra keymap commands/bindings, active only while the modal has focus. */
commands?: ModalCommand[];
bindings?: ModalBinding[];
/** Called on open with whatever had focus before the modal took it. */
onOpen?: (previousFocus: Renderable | null) => void;
accentColor?: ColorInput;
backgroundColor?: ColorInput;
backdropColor?: ColorInput;
@@ -15,16 +30,54 @@ export interface ModalProps extends ParentProps {
}
export function Modal(props: ModalProps) {
return (
<Portal>
<Show when={props.open}>
<ModalPanel {...props} />
</Show>
</Portal>
);
}
// Mounted only while open: takes native focus on mount and hands it back to
// whatever had it on unmount, so keymap layers scoped to the page underneath
// go quiet while the modal is up and come back on their own afterwards.
function ModalPanel(props: ModalProps) {
const renderer = useRenderer();
const dimensions = useTerminalDimensions();
const [panel, setPanel] = createSignal<BoxRenderable>();
const screenCap = () => Math.max(5, dimensions().height - 4);
const maxPanelHeight = () => Math.min(props.maxHeight ?? screenCap(), screenCap());
const maxContentHeight = () => Math.max(1, maxPanelHeight() - 4);
const maxPanelWidth = () => Math.max(10, dimensions().width - 4);
const panelWidth = () => Math.min(props.width ?? 50, maxPanelWidth());
let previousFocus: Renderable | null = null;
onMount(() => {
previousFocus = renderer.currentFocusedRenderable;
props.onOpen?.(previousFocus);
panel()?.focus();
});
onCleanup(() => {
if (previousFocus && !previousFocus.isDestroyed) previousFocus.focus();
});
const dismissKey = props.dismissKey ?? "escape";
if (dismissKey !== false || props.commands || props.bindings) {
useBindings(() => ({
target: panel,
commands: [
...(dismissKey !== false
? [{ name: "modal_dismiss", desc: "close", shortHelp: true, run: () => props.onDismiss?.() }]
: []),
...(props.commands ?? []),
],
bindings: [...(dismissKey !== false ? [{ key: dismissKey, cmd: "modal_dismiss" }] : []), ...(props.bindings ?? [])],
}));
}
return (
<Portal>
<Show when={props.open}>
<box
width={dimensions().width}
height={dimensions().height}
@@ -38,11 +91,14 @@ export function Modal(props: ModalProps) {
onMouseDown={theme.mouse ? props.onDismiss : undefined}
>
<box
ref={setPanel}
focusable
width={panelWidth()}
maxHeight={maxPanelHeight()}
border
borderStyle={theme.borderStyle}
borderColor={props.accentColor ?? theme.primary}
focusedBorderColor={props.accentColor ?? theme.primary}
backgroundColor={props.backgroundColor ?? theme.background}
flexDirection="column"
paddingLeft={2}
@@ -59,13 +115,11 @@ export function Modal(props: ModalProps) {
</box>
}
>
<scrollbox maxHeight={maxContentHeight()} flexShrink={1} scrollY scrollX={false}>
<scrollbox maxHeight={maxContentHeight()} flexShrink={1} scrollY scrollX={false} focusable={false}>
{props.children}
</scrollbox>
</Show>
</box>
</box>
</Show>
</Portal>
);
}
+48
View File
@@ -0,0 +1,48 @@
import type { BoxRenderable } from "@opentui/core";
import type { BoxProps } from "@opentui/solid";
import { type Accessor, createContext, createSignal, splitProps, useContext } from "solid-js";
import { useFocusWithin } from "../context/focus.ts";
export interface PaneHandle {
/** The pane's root renderable — use it as a keymap layer `target`. */
target: Accessor<BoxRenderable | undefined>;
/** True while native focus is anywhere inside the pane. */
focused: Accessor<boolean>;
focus(): void;
}
const PaneContext = createContext<PaneHandle>();
export interface PaneProps extends Omit<BoxProps, "ref"> {
/** Receives the pane's handle, e.g. to focus it or target it from outside. */
handle?: (handle: PaneHandle) => void;
}
/**
* A focus region. Wrap each independently focusable area (sidebar, content,
* …) in a Pane; anything inside can call usePane() to scope keymap layers to
* it (`useBindings(() => ({ target: pane.target, … }))`) and to style itself
* from `pane.focused()`. Pass `focusable` when the pane itself should be the
* focus target (rather than e.g. a <select> inside it).
*/
export function Pane(props: PaneProps) {
const [local, rest] = splitProps(props, ["handle", "children"]);
const [el, setEl] = createSignal<BoxRenderable>();
const focused = useFocusWithin(el);
const handle: PaneHandle = { target: el, focused, focus: () => el()?.focus() };
local.handle?.(handle);
return (
<PaneContext.Provider value={handle}>
<box {...rest} ref={setEl}>
{local.children}
</box>
</PaneContext.Provider>
);
}
export function usePane(): PaneHandle {
const pane = useContext(PaneContext);
if (!pane) throw new Error("usePane() must be used inside a <Pane>");
return pane;
}
+40
View File
@@ -0,0 +1,40 @@
import { TextAttributes } from "@opentui/core";
import { Show } from "solid-js";
import { theme } from "../index.ts";
import { Spinner } from "./Spinner.tsx";
export interface SectionHeaderProps {
title?: string;
loading?: boolean;
/** Shown next to the spinner while loading (default "Loading…"). */
loadingLabel?: string;
/** Shown on the right when not loading, e.g. "r refresh". */
hint?: string;
}
// Lives in its own entry point (ilovetui/components/section-header) because
// it pulls in the optional opentui-spinner peer dependency.
export function SectionHeader(props: SectionHeaderProps) {
return (
<box flexShrink={0} flexDirection="row" justifyContent="space-between" alignItems="center" height={1}>
<box flexGrow={1} flexShrink={1} minWidth={0} marginRight={1} overflow="hidden">
<text fg={theme.muted} attributes={TextAttributes.BOLD} wrapMode="none">
{props.title ?? ""}
</text>
</box>
<box flexShrink={0}>
<Show
when={!props.loading}
fallback={
<box flexDirection="row" alignItems="center">
<Spinner />
<text fg={theme.muted}> {props.loadingLabel ?? "Loading…"}</text>
</box>
}
>
<text fg={theme.muted}>{props.hint ?? ""}</text>
</Show>
</box>
</box>
);
}
+9 -1
View File
@@ -1,5 +1,5 @@
import { type ColorInput, type MouseEvent, type SelectOption, type SelectRenderable, TextAttributes } from "@opentui/core";
import { Show } from "solid-js";
import { createEffect, Show } from "solid-js";
import { presets, theme } from "../index.ts";
export interface SidebarHandle {
@@ -27,6 +27,8 @@ function itemIndexAtScreenY(el: SelectRenderable, screenY: number): number | nul
export interface SidebarProps {
title?: string;
items: SelectOption[];
/** Value of the item to keep highlighted (e.g. the current route), if controlled. */
value?: string;
width?: number;
focused?: boolean;
onSelect: (option: SelectOption) => void;
@@ -83,6 +85,12 @@ export function Sidebar(props: SidebarProps) {
else if (event.scroll.direction === "up") select.moveUp(steps);
};
// A user effect, so it runs after the `options` prop has been applied.
createEffect(() => {
const index = props.items.findIndex((item) => item.value === props.value);
if (select && index !== -1 && select.getSelectedIndex() !== index) select.setSelectedIndex(index);
});
return (
<box
width={props.width ?? 24}
+28
View File
@@ -0,0 +1,28 @@
import type { ParentProps } from "solid-js";
import { theme } from "../index.ts";
import { Box } from "./Box.tsx";
import { type TabItem, Tabs } from "./Tabs.tsx";
export interface TabbedPanelProps extends ParentProps {
items: TabItem[];
value: string;
onChange: (value: string) => void;
focused?: boolean;
}
/** <Tabs/> on top of a bordered panel whose top edge is drawn by the tabs. */
export function TabbedPanel(props: TabbedPanelProps) {
return (
<box flexDirection="column" flexGrow={1}>
<Tabs items={props.items} value={props.value} onChange={props.onChange} focused={props.focused} />
<Box
flexGrow={1}
border={["left", "right", "bottom"]}
borderColor={props.focused ? theme.primary : theme.muted}
flexDirection="column"
>
{props.children}
</Box>
</box>
);
}
+5 -1
View File
@@ -1,10 +1,14 @@
export { Badge, type BadgeProps } from "./Badge.tsx";
export { Box, type BoxProps } from "./Box.tsx";
export { KeyValue, type KeyValueProps } from "./KeyValue.tsx";
export { ListRow, type ListRowProps, type RowTone } from "./ListRow.tsx";
export { MinSizeGuard, type MinSizeGuardProps } from "./MinSizeGuard.tsx";
export { Modal, type ModalProps } from "./Modal.tsx";
export { Modal, type ModalBinding, type ModalCommand, type ModalProps } from "./Modal.tsx";
export { NotificationHost, type NotificationHostProps } from "./NotificationHost.tsx";
export { Pane, usePane, type PaneHandle, type PaneProps } from "./Pane.tsx";
export { ProgressBar, type ProgressBarProps } from "./ProgressBar.tsx";
export { Sidebar, type SidebarHandle, type SidebarProps } from "./Sidebar.tsx";
export { SignalBars, type SignalBarsProps } from "./SignalBars.tsx";
export { TabbedPanel, type TabbedPanelProps } from "./TabbedPanel.tsx";
export { stepTabValue, Tabs, type TabItem, type TabsProps } from "./Tabs.tsx";
export { Toggle, type ToggleProps } from "./Toggle.tsx";
+48 -70
View File
@@ -1,30 +1,4 @@
export interface ColorsYAML {
base00?: string;
base01?: string;
base02?: string;
base03?: string;
base04?: string;
base05?: string;
base06?: string;
base07?: string;
base08?: string;
base09?: string;
base0a?: string;
base0b?: string;
base0c?: string;
base0d?: string;
base0e?: string;
base0f?: string;
}
export interface ConfigYAML {
nerd_fonts?: boolean;
emoji_fonts?: boolean;
reduced_motion?: boolean;
mouse?: boolean;
border?: string;
colors?: ColorsYAML;
}
import { z } from "zod";
export const COLOR_KEYS = [
"base00",
@@ -45,39 +19,64 @@ export const COLOR_KEYS = [
"base0f",
] as const;
export type ResolvedColors = Record<(typeof COLOR_KEYS)[number], string>;
export type ColorKey = (typeof COLOR_KEYS)[number];
export function mergeColors(
base: ColorsYAML,
user: ColorsYAML,
): ResolvedColors {
const merged = {} as ResolvedColors;
for (const key of COLOR_KEYS) {
merged[key] = user[key] || base[key] || "";
}
return merged;
}
export const OPENTUI_BORDER_STYLES = ["single", "double", "rounded", "heavy"] as const;
export type OpenTUIBorderStyle = (typeof OPENTUI_BORDER_STYLES)[number];
const hexColor = z.string().regex(/^#?[0-9a-fA-F]{6}$/, 'expected a hex color like "#1e1e2e"');
const colorsShape = {} as Record<ColorKey, z.ZodOptional<z.ZodString>>;
for (const key of COLOR_KEYS) colorsShape[key] = hexColor.optional();
// Not .strict(): ~/.config/ilovetui/config.yaml is shared by every
// ilovetui-based TUI, and some of them read extra keys of their own from it
// (e.g. `layout_border`). Unknown keys are stripped, known ones validated.
export const ThemeConfigSchema = z.object({
nerd_fonts: z.boolean().optional(),
emoji_fonts: z.boolean().optional(),
reduced_motion: z.boolean().optional(),
mouse: z.boolean().optional(),
border: z.enum(OPENTUI_BORDER_STYLES).optional(),
colors: z.object(colorsShape).optional(),
});
export type ConfigYAML = z.infer<typeof ThemeConfigSchema>;
export type ColorsYAML = NonNullable<ConfigYAML["colors"]>;
export type ResolvedColors = Record<ColorKey, string>;
export interface ResolvedConfig {
nerdFonts: boolean;
emojiFonts: boolean;
reducedMotion: boolean;
mouse: boolean;
border: string;
border: OpenTUIBorderStyle;
colors: ResolvedColors;
}
export function mergeConfig(
base: ConfigYAML,
user: ConfigYAML,
): ResolvedConfig {
// Later layers win, key by key (colors included).
export function layerConfigs(layers: ConfigYAML[]): ConfigYAML {
const out: ConfigYAML = {};
for (const layer of layers) {
const { colors, ...rest } = layer;
Object.assign(out, rest);
if (colors) out.colors = { ...out.colors, ...colors };
}
return out;
}
export function resolveConfig(config: ConfigYAML): ResolvedConfig {
const colors = {} as ResolvedColors;
for (const key of COLOR_KEYS) colors[key] = normalizeColor(config.colors?.[key] ?? "");
return {
nerdFonts: user.nerd_fonts ?? base.nerd_fonts ?? false,
emojiFonts: user.emoji_fonts ?? base.emoji_fonts ?? false,
reducedMotion: user.reduced_motion ?? base.reduced_motion ?? false,
mouse: user.mouse ?? base.mouse ?? true,
border: user.border || base.border || "rounded",
colors: mergeColors(base.colors ?? {}, user.colors ?? {}),
nerdFonts: config.nerd_fonts ?? false,
emojiFonts: config.emoji_fonts ?? false,
reducedMotion: config.reduced_motion ?? false,
mouse: config.mouse ?? true,
border: config.border ?? "rounded",
colors,
};
}
@@ -86,24 +85,3 @@ export function normalizeColor(hex: string): string {
if (trimmed && !trimmed.startsWith("#")) return `#${trimmed}`;
return trimmed;
}
export function normalizeColors(colors: ResolvedColors): ResolvedColors {
const out = {} as ResolvedColors;
for (const key of COLOR_KEYS) out[key] = normalizeColor(colors[key]);
return out;
}
export type OpenTUIBorderStyle = "single" | "double" | "rounded" | "heavy";
const OPENTUI_BORDER_STYLES: readonly OpenTUIBorderStyle[] = [
"single",
"double",
"rounded",
"heavy",
];
export function resolveBorderStyle(name: string): OpenTUIBorderStyle {
return (OPENTUI_BORDER_STYLES as readonly string[]).includes(name)
? (name as OpenTUIBorderStyle)
: "rounded";
}
+30
View File
@@ -0,0 +1,30 @@
import { CliRenderEvents, type Renderable } from "@opentui/core";
import { useRenderer } from "@opentui/solid";
import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js";
export function isInside(node: Renderable | null, root: Renderable | null): boolean {
if (!root) return false;
for (let current = node; current; current = current.parent) {
if (current === root) return true;
}
return false;
}
/**
* True while the renderer's native focus is `target` or one of its
* descendants. Mouse clicks move native focus on their own (the renderer
* focuses the nearest focusable ancestor of what was clicked), so this
* follows both keyboard and mouse focus changes.
*/
export function useFocusWithin(target: Accessor<Renderable | null | undefined>): Accessor<boolean> {
const renderer = useRenderer();
const [focused, setFocused] = createSignal(false);
const sync = () => setFocused(isInside(renderer.currentFocusedRenderable, target() ?? null));
renderer.on(CliRenderEvents.FOCUSED_RENDERABLE, sync);
onCleanup(() => renderer.off(CliRenderEvents.FOCUSED_RENDERABLE, sync));
createEffect(sync);
return focused;
}
+1
View File
@@ -1,3 +1,4 @@
export { createSimpleContext } from "./helper.tsx";
export { notify, dismiss, notifications, type Notification, type NotificationKind, type NotifyOptions } from "./notifications.ts";
export { helpOpen, toggleHelp } from "./help.ts";
export { isInside, useFocusWithin } from "./focus.ts";
+67 -19
View File
@@ -1,13 +1,17 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { parse as parseYAML } from "yaml";
import { z } from "zod";
import {
type ConfigYAML,
type OpenTUIBorderStyle,
mergeConfig,
normalizeColors,
resolveBorderStyle,
layerConfigs,
resolveConfig,
ThemeConfigSchema,
} from "./config.ts";
import { buildPresets } from "./presets.ts";
import { configPath, readYamlFile } from "./yaml.ts";
import { notify } from "./context/notifications.ts";
import { buildPresets, type Presets } from "./presets.ts";
import { configPath } from "./yaml.ts";
export interface Theme {
base00: string;
@@ -47,12 +51,39 @@ export interface Theme {
export { configPath } from "./yaml.ts";
function buildTheme(): Theme {
const defaultConfigPath = join(import.meta.dir, "default.yaml");
const base = readYamlFile<ConfigYAML>(defaultConfigPath) ?? {};
const user = readYamlFile<ConfigYAML>(configPath()) ?? {};
const merged = mergeConfig(base, user);
const colors = normalizeColors(merged.colors);
function reportThemeError(path: string, message: string): void {
const text = `${path}: ${message}`;
console.error(`[ilovetui theme] ${text}`);
notify(text, { kind: "warning", duration: 0 });
}
// A broken file is reported (as a sticky warning toast, since console output
// is hidden behind the TUI) and skipped as a whole, falling back to the
// layers below it.
function readThemeFile(path: string): ConfigYAML {
if (!existsSync(path)) return {};
let raw: unknown;
try {
raw = parseYAML(readFileSync(path, "utf8"));
} catch (error) {
reportThemeError(path, error instanceof Error ? error.message : String(error));
return {};
}
const result = ThemeConfigSchema.safeParse(raw ?? {});
if (!result.success) {
reportThemeError(path, z.prettifyError(result.error));
return {};
}
return result.data;
}
// Read once: the shipped defaults, then the user's shared
// ~/.config/ilovetui/config.yaml on top.
const fileLayers: ConfigYAML[] = [readThemeFile(join(import.meta.dir, "default.yaml")), readThemeFile(configPath())];
function buildTheme(overrides: ConfigYAML): Theme {
const resolved = resolveConfig(layerConfigs([...fileLayers, overrides]));
const colors = resolved.colors;
return {
...colors,
@@ -68,17 +99,34 @@ function buildTheme(): Theme {
warning: colors.base09,
error: colors.base08,
nerdFonts: merged.nerdFonts,
emojiFonts: merged.emojiFonts,
reducedMotion: merged.reducedMotion,
mouse: merged.mouse,
borderStyle: resolveBorderStyle(merged.border),
nerdFonts: resolved.nerdFonts,
emojiFonts: resolved.emojiFonts,
reducedMotion: resolved.reducedMotion,
mouse: resolved.mouse,
borderStyle: resolved.border,
};
}
export const theme: Theme = buildTheme();
export const theme: Theme = buildTheme({});
export const presets = buildPresets(theme);
export const presets: Presets = buildPresets(theme);
export type { OpenTUIBorderStyle } from "./config.ts";
/**
* Layers app-specific overrides (typically a `theme:` section of the app's own
* config, validated with `ThemeConfigSchema`) on top of the shared ilovetui
* config. `theme` and `presets` are updated in place, so call this once at
* startup, before the first render.
*/
export function configureTheme(overrides: ConfigYAML = {}): void {
Object.assign(theme, buildTheme(overrides));
const next = buildPresets(theme);
for (const key of Object.keys(next) as (keyof Presets)[]) Object.assign(presets[key], next[key]);
}
/** A Nerd Font glyph followed by a space, or "" when `nerd_fonts` is off. */
export function icon(codepoint: string): string {
return theme.nerdFonts ? `${codepoint} ` : "";
}
export { ThemeConfigSchema, type ConfigYAML as ThemeConfig, type OpenTUIBorderStyle } from "./config.ts";
export type { Presets, SelectPreset, TabSelectPreset, InputPreset, BoxPreset } from "./presets.ts";
+60
View File
@@ -71,6 +71,66 @@ export function loadProjectConfig<TDefault, TUser>(
return { defaults: defaultsResult.data, user };
}
export interface MergedProjectConfigOptions<T> {
projectName: string;
defaultConfigPath: string;
/** Schema for the complete config; the defaults file must satisfy it on its own. */
schema: z.ZodType<T>;
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
// Objects merge key by key; anything else (scalars, arrays) from `over` replaces `base`.
function deepMerge(base: unknown, over: unknown): unknown {
if (!isPlainObject(base) || !isPlainObject(over)) return over === undefined ? base : over;
const out: Record<string, unknown> = { ...base };
for (const [key, value] of Object.entries(over)) out[key] = deepMerge(base[key], value);
return out;
}
/**
* Single-schema alternative to loadProjectConfig(): the user's
* ~/.config/<projectName>/config.yaml is deep-merged over the defaults file
* and the result is validated once against `schema`, so the returned value is
* fully typed and complete — no per-field `user.x ?? defaults.x` merging.
* An unreadable or invalid user file is reported (warning toast + stderr) and
* ignored as a whole; an invalid defaults file throws.
*/
export function loadMergedProjectConfig<T>(options: MergedProjectConfigOptions<T>): T {
const { projectName, defaultConfigPath, schema } = options;
let rawDefaults: unknown;
try {
rawDefaults = readYaml(defaultConfigPath);
} catch (error) {
throw new Error(`Failed to read ${defaultConfigPath}: ${error instanceof Error ? error.message : error}`);
}
const defaults = schema.safeParse(rawDefaults);
if (!defaults.success) {
throw new Error(`${defaultConfigPath} is invalid:\n${z.prettifyError(defaults.error)}`);
}
const path = userConfigPath(projectName);
if (!existsSync(path)) return defaults.data;
let rawUser: unknown;
try {
rawUser = readYaml(path);
} catch (error) {
warn(projectName, path, error instanceof Error ? error.message : String(error));
return defaults.data;
}
const merged = schema.safeParse(deepMerge(rawDefaults, rawUser ?? {}));
if (!merged.success) {
warn(projectName, path, z.prettifyError(merged.error));
return defaults.data;
}
return merged.data;
}
export function keybindsSchema<TName extends string>(
names: readonly TName[],
required: true,
+15 -11
View File
@@ -21,31 +21,35 @@ type AnyCtor = new (...args: any[]) => any;
function withThemeDefaults<T extends AnyCtor>(
Ctor: T,
defaults: object,
defaults: () => object,
after?: (instance: any) => void,
): T {
class Themed extends Ctor {
constructor(...args: any[]) {
super(...args);
Object.assign(this, defaults);
Object.assign(this, defaults());
after?.(this);
}
}
return Themed as T;
}
const { borderStyle, borderColor, focusedBorderColor } = presets.box;
const boxDefaults = { borderStyle, borderColor, focusedBorderColor };
// Defaults are read per instance (not captured at import) so that
// configureTheme() overrides applied at startup are picked up.
const boxDefaults = () => {
const { borderStyle, borderColor, focusedBorderColor } = presets.box;
return { borderStyle, borderColor, focusedBorderColor };
};
extend({
box: withThemeDefaults(BoxRenderable, boxDefaults, (instance) => {
instance.border = false;
}),
select: withThemeDefaults(SelectRenderable, presets.select),
tab_select: withThemeDefaults(TabSelectRenderable, presets.tabSelect),
input: withThemeDefaults(InputRenderable, presets.input),
textarea: withThemeDefaults(TextareaRenderable, presets.textarea),
slider: withThemeDefaults(SliderRenderable, presets.slider),
text: withThemeDefaults(TextRenderable, { fg: theme.text }),
ascii_font: withThemeDefaults(ASCIIFontRenderable, { color: theme.primary }),
select: withThemeDefaults(SelectRenderable, () => presets.select),
tab_select: withThemeDefaults(TabSelectRenderable, () => presets.tabSelect),
input: withThemeDefaults(InputRenderable, () => presets.input),
textarea: withThemeDefaults(TextareaRenderable, () => presets.textarea),
slider: withThemeDefaults(SliderRenderable, () => presets.slider),
text: withThemeDefaults(TextRenderable, () => ({ fg: theme.text })),
ascii_font: withThemeDefaults(ASCIIFontRenderable, () => ({ color: theme.primary })),
});
-12
View File
@@ -1,19 +1,7 @@
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { parse as parseYAML } from "yaml";
export function configPath(): string {
const configDir = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
return join(configDir, "ilovetui", "config.yaml");
}
export function readYamlFile<T>(path: string): T | undefined {
if (!existsSync(path)) return undefined;
try {
return (parseYAML(readFileSync(path, "utf8")) as T) ?? undefined;
} catch (err) {
console.error(`ilovetui: failed to parse ${path}, ignoring it.`, err);
return undefined;
}
}