diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6d9bd83..da54f01 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -23,8 +23,8 @@ make native # build libcavacore.dylib from the vendored C source
bun run dev # launch with hot reload (alias: make dev)
```
-The app is a TUI — it expects a real terminal (kitty, iTerm2, WezTerm, tmux,
-…). It will not render in a plain captured `bash` session.
+The app is a TUI — it expects a real terminal (Ghostty, kitty, iTerm2,
+ WezTerm, tmux, …). It will not render in a plain captured `bash` session.
## What each command does
@@ -35,14 +35,11 @@ The app is a TUI — it expects a real terminal (kitty, iTerm2, WezTerm, tmux,
| `bun run dev` | Run with hot reload |
| `bun run start` | Run once (no watch) |
| `bun test` | Run the test suite (see [Testing](#testing)) |
-| `make lint` | Type-check with `bun tsc --noEmit` |
+| `bun run lint` | Type-check |
| `bun run build` | Bundle JS into `dist/` + copy native libs (the `podtui` npm script path) |
| `make dist` | Compile the standalone binary + make the current platform's tarball |
| `make clean` | Remove `dist/` |
-> Note: `package.json` also has a `lint` script that points at a
-> `lint.ts` file that doesn't exist. Use `make lint` (real type-checking).
-
## Repository layout
```
@@ -112,10 +109,6 @@ Cavacore smoke test: `bun tests/cavacore-smoke.ts`
`-headerpad`” for a prebuilt dylib. The app dlopens the libs by path, so
the warning is cosmetic; installs complete and the app boots.
-4. **`make lint` is the truth, not the `package.json` scripts.**
- The repo's ESLint wiring is stale; `make lint` runs the real
- type-check and is what CI treats as the clean bar.
-
## Testing
```bash
@@ -135,7 +128,7 @@ Audio is a no-op during those snapshots. The last frame lands in
## Releasing
-Releases are built and published from **tags**; CI does the heavy lifting.
+Releases are built and published from **tags**
### Steps
diff --git a/package.json b/package.json
index e606e66..1a21a75 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,7 @@
"build": "bun run build.ts",
"dist": "bun dist/index.js",
"test": "bun test",
- "lint": "bun run lint.ts"
+ "lint": "bun tsc --noEmit"
},
"devDependencies": {
"@types/bun": "latest",
diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx
deleted file mode 100644
index e5b15fc..0000000
--- a/src/components/Navigation.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import type { TabId } from "./Tab"
-import { useTheme } from "@/context/ThemeContext"
-
-type NavigationProps = {
- activeTab: TabId
- onTabSelect: (tab: TabId) => void
-}
-
-export function Navigation(props: NavigationProps) {
- const { theme } = useTheme();
- return (
-
-
- {props.activeTab === "feed" ? "[" : " "}Feed{props.activeTab === "feed" ? "]" : " "}
-
- {props.activeTab === "shows" ? "[" : " "}My Shows{props.activeTab === "shows" ? "]" : " "}
-
- {props.activeTab === "discover" ? "[" : " "}Discover{props.activeTab === "discover" ? "]" : " "}
-
- {props.activeTab === "search" ? "[" : " "}Search{props.activeTab === "search" ? "]" : " "}
-
- {props.activeTab === "player" ? "[" : " "}Player{props.activeTab === "player" ? "]" : " "}
-
- {props.activeTab === "settings" ? "[" : " "}Settings{props.activeTab === "settings" ? "]" : " "}
-
-
- )
-}
diff --git a/src/pages/Player/PlaybackControls.tsx b/src/pages/Player/PlaybackControls.tsx
index 0e7bf33..46a7776 100644
--- a/src/pages/Player/PlaybackControls.tsx
+++ b/src/pages/Player/PlaybackControls.tsx
@@ -1,64 +1,86 @@
-import type { BackendName } from "../utils/audio-player"
-import { useTheme } from "@/context/ThemeContext"
+import type { BackendName } from "@/utils/audio-player";
+import { useTheme } from "@/context/ThemeContext";
type PlaybackControlsProps = {
- isPlaying: boolean
- volume: number
- speed: number
- backendName?: BackendName
- hasAudioUrl?: boolean
- onToggle: () => void
- onPrev: () => void
- onNext: () => void
- onVolumeChange: (value: number) => void
- onSpeedChange: (value: number) => void
-}
+ isPlaying: boolean;
+ volume: number;
+ speed: number;
+ backendName?: BackendName;
+ hasAudioUrl?: boolean;
+ onToggle: () => void;
+ onPrev: () => void;
+ onNext: () => void;
+ onVolumeChange: (value: number) => void;
+ onSpeedChange: (value: number) => void;
+};
const BACKEND_LABELS: Record = {
- mpv: "mpv",
- ffplay: "ffplay",
- afplay: "afplay",
- system: "system",
- none: "none",
-}
+ mpv: "mpv",
+ ffplay: "ffplay",
+ afplay: "afplay",
+ system: "system",
+ none: "none",
+};
export function PlaybackControls(props: PlaybackControlsProps) {
- const { theme } = useTheme();
- return (
-
-
- [Prev]
-
-
- {props.isPlaying ? "[Pause]" : "[Play]"}
-
-
- [Next]
-
-
- Vol
- {Math.round(props.volume * 100)}%
-
-
- Speed
- {props.speed}x
-
- {props.backendName && props.backendName !== "none" && (
-
- via
- {BACKEND_LABELS[props.backendName]}
-
- )}
- {props.backendName === "none" && (
-
- No audio player found
-
- )}
- {props.hasAudioUrl === false && (
-
- No audio URL
-
- )}
-
- )
+ const { theme } = useTheme();
+ return (
+
+
+ [Prev]
+
+
+ {props.isPlaying ? "[Pause]" : "[Play]"}
+
+
+ [Next]
+
+
+ Vol
+ {Math.round(props.volume * 100)}%
+
+
+ Speed
+ {props.speed}x
+
+ {props.backendName && props.backendName !== "none" && (
+
+ via
+ {BACKEND_LABELS[props.backendName]}
+
+ )}
+ {props.backendName === "none" && (
+
+ No audio player found
+
+ )}
+ {props.hasAudioUrl === false && (
+
+ No audio URL
+
+ )}
+
+ );
}
diff --git a/src/pages/Search/SourceBadge.tsx b/src/pages/Search/SourceBadge.tsx
index fc90246..3946b17 100644
--- a/src/pages/Search/SourceBadge.tsx
+++ b/src/pages/Search/SourceBadge.tsx
@@ -2,42 +2,37 @@ import { SourceType } from "@/types/source";
import { useTheme } from "@/context/ThemeContext";
type SourceBadgeProps = {
- sourceId: string;
- sourceName?: string;
- sourceType?: SourceType;
+ sourceId: string;
+ sourceName?: string;
+ sourceType?: SourceType;
};
const typeLabel = (sourceType?: SourceType) => {
- if (sourceType === SourceType.API) return "API";
- if (sourceType === SourceType.RSS) return "RSS";
- if (sourceType === SourceType.CUSTOM) return "Custom";
- return "Source";
-};
-
-const typeColor = (sourceType?: SourceType) => {
- if (sourceType === SourceType.API) return theme.primary;
- if (sourceType === SourceType.RSS) return theme.success;
- if (sourceType === SourceType.CUSTOM) return theme.warning;
- return theme.textMuted;
+ if (sourceType === SourceType.API) return "API";
+ if (sourceType === SourceType.RSS) return "RSS";
+ if (sourceType === SourceType.CUSTOM) return "Custom";
+ return "Source";
};
+// No module-level typeColor here — it needs the theme from the component.
+// The correct definition lives inside SourceBadge below.
export function SourceBadge(props: SourceBadgeProps) {
- const { theme } = useTheme();
- const label = () => props.sourceName || props.sourceId;
+ const { theme } = useTheme();
+ const label = () => props.sourceName || props.sourceId;
- const typeColor = (sourceType?: SourceType) => {
- if (sourceType === SourceType.API) return theme.primary;
- if (sourceType === SourceType.RSS) return theme.success;
- if (sourceType === SourceType.CUSTOM) return theme.warning;
- return theme.textMuted;
- };
+ const typeColor = (sourceType?: SourceType) => {
+ if (sourceType === SourceType.API) return theme.primary;
+ if (sourceType === SourceType.RSS) return theme.success;
+ if (sourceType === SourceType.CUSTOM) return theme.warning;
+ return theme.textMuted;
+ };
- return (
-
-
- [{typeLabel(props.sourceType)}]
-
- {label()}
-
- );
+ return (
+
+
+ [{typeLabel(props.sourceType)}]
+
+ {label()}
+
+ );
}
diff --git a/src/ui/command.tsx b/src/ui/command.tsx
index b1d0e85..fe963a9 100644
--- a/src/ui/command.tsx
+++ b/src/ui/command.tsx
@@ -1,13 +1,13 @@
import {
- createContext,
- createMemo,
- createSignal,
- onCleanup,
- useContext,
- type Accessor,
- type ParentProps,
- For,
- Show,
+ createContext,
+ createMemo,
+ createSignal,
+ onCleanup,
+ useContext,
+ type Accessor,
+ type ParentProps,
+ For,
+ Show,
} from "solid-js";
import { useKeyboard, useTerminalDimensions } from "@opentui/solid";
import { KeybindsResolved, useKeybinds } from "../context/KeybindContext";
@@ -21,313 +21,319 @@ import { SelectableBox, SelectableText } from "@/components/Selectable";
* Command option for the command palette.
*/
export type CommandOption = {
- /** Display title */
- title: string;
- /** Unique identifier */
- value: string;
- /** Description shown below title */
- description?: string;
- /** Category for grouping */
- category?: string;
- /** Keybind reference */
- keybind?: keyof KeybindsResolved;
- /** Whether this command is suggested */
- suggested?: boolean;
- /** Slash command configuration */
- slash?: {
- name: string;
- aliases?: string[];
- };
- /** Whether to hide from command list */
- hidden?: boolean;
- /** Whether command is enabled */
- enabled?: boolean;
- /** Footer text (usually keybind display) */
- footer?: string;
- /** Handler when command is selected */
- onSelect?: (dialog: ReturnType) => void;
+ /** Display title */
+ title: string;
+ /** Unique identifier */
+ value: string;
+ /** Description shown below title */
+ description?: string;
+ /** Category for grouping */
+ category?: string;
+ /** Keybind reference */
+ keybind?: keyof KeybindsResolved;
+ /** Whether this command is suggested */
+ suggested?: boolean;
+ /** Slash command configuration */
+ slash?: {
+ name: string;
+ aliases?: string[];
+ };
+ /** Whether to hide from command list */
+ hidden?: boolean;
+ /** Whether command is enabled */
+ enabled?: boolean;
+ /** Footer text (usually keybind display) */
+ footer?: string;
+ /** Handler when command is selected */
+ onSelect?: (dialog: ReturnType) => void;
};
type CommandContext = ReturnType;
const ctx = createContext();
function init() {
- const [registrations, setRegistrations] = createSignal<
- Accessor[]
- >([]);
- const [suspendCount, setSuspendCount] = createSignal(0);
- const dialog = useDialog();
- const keybind = useKeybinds();
+ const [registrations, setRegistrations] = createSignal<
+ Accessor[]
+ >([]);
+ const [suspendCount, setSuspendCount] = createSignal(0);
+ const dialog = useDialog();
+ const keybind = useKeybinds();
- const entries = createMemo(() => {
- const all = registrations().flatMap((x) => x());
- return all.map((x) => ({
- ...x,
- footer: x.keybind ? keybind.print(x.keybind) : undefined,
- }));
- });
+ const entries = createMemo(() => {
+ const all = registrations().flatMap((x) => x());
+ return all.map((x) => ({
+ ...x,
+ footer: x.keybind ? keybind.print(x.keybind) : undefined,
+ }));
+ });
- const isEnabled = (option: CommandOption) => option.enabled !== false;
- const isVisible = (option: CommandOption) =>
- isEnabled(option) && !option.hidden;
+ const isEnabled = (option: CommandOption) => option.enabled !== false;
+ const isVisible = (option: CommandOption) =>
+ isEnabled(option) && !option.hidden;
- const visibleOptions = createMemo(() =>
- entries().filter((option) => isVisible(option)),
- );
- const suggestedOptions = createMemo(() =>
- visibleOptions()
- .filter((option) => option.suggested)
- .map((option) => ({
- ...option,
- value: `suggested:${option.value}`,
- category: "Suggested",
- })),
- );
- const suspended = () => suspendCount() > 0;
+ const visibleOptions = createMemo(() =>
+ entries().filter((option) => isVisible(option)),
+ );
+ const suggestedOptions = createMemo(() =>
+ visibleOptions()
+ .filter((option) => option.suggested)
+ .map((option) => ({
+ ...option,
+ value: `suggested:${option.value}`,
+ category: "Suggested",
+ })),
+ );
+ const suspended = () => suspendCount() > 0;
- // Handle keybind shortcuts
- useKeyboard((evt) => {
- if (suspended()) return;
- if (dialog.isOpen) return;
- for (const option of entries()) {
- if (!isEnabled(option)) continue;
- if (option.keybind && keybind.match(option.keybind, evt)) {
- evt.preventDefault();
- option.onSelect?.(dialog);
- emit("command.execute", { command: option.value });
- return;
- }
- }
- });
+ // Handle keybind shortcuts
+ useKeyboard((evt) => {
+ if (suspended()) return;
+ if (dialog.isOpen) return;
+ for (const option of entries()) {
+ if (!isEnabled(option)) continue;
+ if (option.keybind && keybind.match(option.keybind, evt)) {
+ evt.preventDefault();
+ option.onSelect?.(dialog);
+ emit("command.execute", { command: option.value });
+ return;
+ }
+ }
+ });
- const result = {
- /**
- * Trigger a command by its value.
- */
- trigger(name: string) {
- for (const option of entries()) {
- if (option.value === name) {
- if (!isEnabled(option)) return;
- option.onSelect?.(dialog);
- emit("command.execute", { command: name });
- return;
- }
- }
- },
- /**
- * Get all slash commands.
- */
- slashes() {
- return visibleOptions().flatMap((option) => {
- const slash = option.slash;
- if (!slash) return [];
- return {
- display: "/" + slash.name,
- description: option.description ?? option.title,
- aliases: slash.aliases?.map((alias) => "/" + alias),
- onSelect: () => result.trigger(option.value),
- };
- });
- },
- /**
- * Enable/disable keybinds temporarily.
- */
- keybinds(enabled: boolean) {
- setSuspendCount((count) => count + (enabled ? -1 : 1));
- },
- suspended,
- /**
- * Show the command palette dialog.
- */
- show() {
- dialog.replace(() => (
-
- ));
- },
- /**
- * Register commands. Returns cleanup function.
- */
- register(cb: () => CommandOption[]) {
- const results = createMemo(cb);
- setRegistrations((arr) => [results, ...arr]);
- onCleanup(() => {
- setRegistrations((arr) => arr.filter((x) => x !== results));
- });
- },
- /**
- * Get all visible options.
- */
- get options() {
- return visibleOptions();
- },
- };
- return result;
+ const result = {
+ /**
+ * Trigger a command by its value.
+ */
+ trigger(name: string) {
+ for (const option of entries()) {
+ if (option.value === name) {
+ if (!isEnabled(option)) return;
+ option.onSelect?.(dialog);
+ emit("command.execute", { command: name });
+ return;
+ }
+ }
+ },
+ /**
+ * Get all slash commands.
+ */
+ slashes() {
+ return visibleOptions().flatMap((option) => {
+ const slash = option.slash;
+ if (!slash) return [];
+ return {
+ display: "/" + slash.name,
+ description: option.description ?? option.title,
+ aliases: slash.aliases?.map((alias) => "/" + alias),
+ onSelect: () => result.trigger(option.value),
+ };
+ });
+ },
+ /**
+ * Enable/disable keybinds temporarily.
+ */
+ keybinds(enabled: boolean) {
+ setSuspendCount((count) => count + (enabled ? -1 : 1));
+ },
+ suspended,
+ /**
+ * Show the command palette dialog.
+ */
+ show() {
+ dialog.replace(() => (
+
+ ));
+ },
+ /**
+ * Register commands. Returns cleanup function.
+ */
+ register(cb: () => CommandOption[]) {
+ const results = createMemo(cb);
+ setRegistrations((arr) => [results, ...arr]);
+ onCleanup(() => {
+ setRegistrations((arr) => arr.filter((x) => x !== results));
+ });
+ },
+ /**
+ * Get all visible options.
+ */
+ get options() {
+ return visibleOptions();
+ },
+ };
+ return result;
}
export function useCommandDialog() {
- const value = useContext(ctx);
- if (!value) {
- throw new Error("useCommandDialog must be used within a CommandProvider");
- }
- return value;
+ const value = useContext(ctx);
+ if (!value) {
+ throw new Error("useCommandDialog must be used within a CommandProvider");
+ }
+ return value;
}
export function CommandProvider(props: ParentProps) {
- const value = init();
- const dialog = useDialog();
- const keybind = useKeybinds();
+ const value = init();
+ const dialog = useDialog();
+ const keybind = useKeybinds();
- // Open command palette on ctrl+p or command_list keybind
- useKeyboard((evt) => {
- if (value.suspended()) return;
- if (dialog.isOpen) return;
- if (evt.defaultPrevented) return;
- if (keybind.match("command_list", evt)) {
- evt.preventDefault();
- value.show();
- return;
- }
- });
+ // Open the command palette via the `command` keybind (bound to `:` in
+ // keybinds.jsonc). The old hardcoded "command_list" name was never a
+ // canonical action, so the palette was unreachable dead code.
+ useKeyboard((evt) => {
+ if (value.suspended()) return;
+ if (dialog.isOpen) return;
+ if (evt.defaultPrevented) return;
+ if (keybind.match("command", evt)) {
+ evt.preventDefault();
+ value.show();
+ return;
+ }
+ });
- return {props.children};
+ return {props.children};
}
/**
* Command palette dialog component.
*/
function CommandDialog(props: {
- options: CommandOption[];
- suggestedOptions: CommandOption[];
+ options: CommandOption[];
+ suggestedOptions: CommandOption[];
}) {
- const { theme } = useTheme();
- const dialog = useDialog();
- const dimensions = useTerminalDimensions();
- const [filter, setFilter] = createSignal("");
- const [selectedIndex, setSelectedIndex] = createSignal(0);
+ const { theme } = useTheme();
+ const dialog = useDialog();
+ const dimensions = useTerminalDimensions();
+ const [filter, setFilter] = createSignal("");
+ const [selectedIndex, setSelectedIndex] = createSignal(0);
- const filteredOptions = createMemo(() => {
- const query = filter().toLowerCase();
- if (!query) {
- return [...props.suggestedOptions, ...props.options];
- }
- return props.options.filter(
- (option) =>
- option.title.toLowerCase().includes(query) ||
- option.description?.toLowerCase().includes(query) ||
- option.category?.toLowerCase().includes(query),
- );
- });
+ const filteredOptions = createMemo(() => {
+ const query = filter().toLowerCase();
+ if (!query) {
+ return [...props.suggestedOptions, ...props.options];
+ }
+ return props.options.filter(
+ (option) =>
+ option.title.toLowerCase().includes(query) ||
+ option.description?.toLowerCase().includes(query) ||
+ option.category?.toLowerCase().includes(query),
+ );
+ });
- // Reset selection when filter changes
- createMemo(() => {
- filter();
- setSelectedIndex(0);
- });
+ // Reset selection when filter changes
+ createMemo(() => {
+ filter();
+ setSelectedIndex(0);
+ });
- useKeyboard((evt) => {
- if (evt.name === "escape") {
- dialog.clear();
- evt.preventDefault();
- return;
- }
+ useKeyboard((evt) => {
+ if (evt.name === "escape") {
+ dialog.clear();
+ evt.preventDefault();
+ return;
+ }
- if (evt.name === "return" || evt.name === "enter") {
- const option = filteredOptions()[selectedIndex()];
- if (option) {
- option.onSelect?.(dialog);
- dialog.clear();
- }
- evt.preventDefault();
- return;
- }
+ if (evt.name === "return" || evt.name === "enter") {
+ const option = filteredOptions()[selectedIndex()];
+ if (option) {
+ option.onSelect?.(dialog);
+ dialog.clear();
+ }
+ evt.preventDefault();
+ return;
+ }
- if (evt.name === "up" || (evt.ctrl && evt.name === "p")) {
- setSelectedIndex((i) => Math.max(0, i - 1));
- evt.preventDefault();
- return;
- }
+ if (evt.name === "up" || (evt.ctrl && evt.name === "p")) {
+ setSelectedIndex((i) => Math.max(0, i - 1));
+ evt.preventDefault();
+ return;
+ }
- if (evt.name === "down" || (evt.ctrl && evt.name === "n")) {
- setSelectedIndex((i) => Math.min(filteredOptions().length - 1, i + 1));
- evt.preventDefault();
- return;
- }
+ if (evt.name === "down" || (evt.ctrl && evt.name === "n")) {
+ setSelectedIndex((i) => Math.min(filteredOptions().length - 1, i + 1));
+ evt.preventDefault();
+ return;
+ }
- // Handle text input
- if (evt.name && evt.name.length === 1 && !evt.ctrl && !evt.meta) {
- setFilter((f) => f + evt.name);
- return;
- }
+ // Handle text input
+ if (evt.name && evt.name.length === 1 && !evt.ctrl && !evt.meta) {
+ setFilter((f) => f + evt.name);
+ return;
+ }
- if (evt.name === "backspace") {
- setFilter((f) => f.slice(0, -1));
- return;
- }
- });
+ if (evt.name === "backspace") {
+ setFilter((f) => f.slice(0, -1));
+ return;
+ }
+ });
- const maxHeight = Math.floor(dimensions().height * 0.6);
+ const maxHeight = Math.floor(dimensions().height * 0.6);
- return (
-
- {/* Search input */}
-
- {"> "}
- {filter() || "Type to search commands..."}
-
+ return (
+
+ {/* Search input */}
+
+ {"> "}
+ {filter() || "Type to search commands..."}
+
- {/* Command list */}
-
-
- {(option, index) => (
- index() === selectedIndex()}
- flexDirection="column"
- padding={1}
- onMouseDown={() => {
- setSelectedIndex(index());
- const selectedOption = filteredOptions()[index()];
- if (selectedOption) {
- selectedOption.onSelect?.(dialog);
- dialog.clear();
- }
- }}
- >
-
- index() === selectedIndex()}
- primary
- >
- {option.title}
-
-
- index() === selectedIndex()}
- tertiary
- >
- {option.footer}
-
-
-
- index() === selectedIndex()}
- tertiary
- >
- {option.description}
-
-
-
-
- )}
-
-
-
- No commands found
-
-
-
-
- );
+ {/* Command list */}
+
+
+ {(option, index) => (
+ index() === selectedIndex()}
+ flexDirection="column"
+ padding={1}
+ onMouseDown={() => {
+ setSelectedIndex(index());
+ const selectedOption = filteredOptions()[index()];
+ if (selectedOption) {
+ selectedOption.onSelect?.(dialog);
+ dialog.clear();
+ }
+ }}
+ >
+
+ index() === selectedIndex()}
+ primary
+ >
+ {option.title}
+
+
+ index() === selectedIndex()}
+ tertiary
+ >
+ {option.footer}
+
+
+
+ index() === selectedIndex()}
+ tertiary
+ >
+ {option.description}
+
+
+
+
+ )}
+
+
+
+ No commands found
+
+
+
+
+ );
}
diff --git a/tests/yazi-pane-row.test.tsx b/tests/yazi-pane-row.test.tsx
index d7d0b88..2c8013d 100644
--- a/tests/yazi-pane-row.test.tsx
+++ b/tests/yazi-pane-row.test.tsx
@@ -106,7 +106,12 @@ async function renderPaneRow(props: TestPaneProps): Promise<{
await new Promise((r) => setTimeout(r, 40));
}
const spans = setup.captureSpans() as unknown as Frame;
- return { spans, destroy: () => setup.renderer.destroy() };
+ return {
+ spans,
+ destroy: async () => {
+ setup.renderer.destroy();
+ },
+ };
}
const cleanups: (() => void | Promise)[] = [];
diff --git a/tsconfig.json b/tsconfig.json
index b2bd7cc..88815dc 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -4,7 +4,7 @@
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
- "jsx": "preserve",
+ "jsx": "react-jsx",
"jsxImportSource": "@opentui/solid",
"strict": true,
"skipLibCheck": true,