proper layering work
This commit is contained in:
40
src/App.tsx
40
src/App.tsx
@@ -11,6 +11,7 @@ import { SearchPage } from "./components/SearchPage";
|
||||
import { DiscoverPage } from "./components/DiscoverPage";
|
||||
import { Player } from "./components/Player";
|
||||
import { SettingsScreen } from "./components/SettingsScreen";
|
||||
import { ThemeProvider } from "./context/ThemeContext";
|
||||
import { useAuthStore } from "./stores/auth";
|
||||
import { useFeedStore } from "./stores/feed";
|
||||
import { useAppStore } from "./stores/app";
|
||||
@@ -32,27 +33,31 @@ export function App() {
|
||||
// Centralized keyboard handler for all tab navigation and shortcuts
|
||||
useAppKeyboard({
|
||||
get activeTab() {
|
||||
return activeTab();
|
||||
return activeTab()
|
||||
},
|
||||
onTabChange: setActiveTab,
|
||||
inputFocused: inputFocused(),
|
||||
navigationEnabled: layerDepth() === 0,
|
||||
layerDepth,
|
||||
onLayerChange: (newDepth) => {
|
||||
setLayerDepth(newDepth)
|
||||
},
|
||||
onAction: (action) => {
|
||||
if (action === "escape") {
|
||||
if (layerDepth() > 0) {
|
||||
setLayerDepth(0);
|
||||
setInputFocused(false);
|
||||
setLayerDepth(0)
|
||||
setInputFocused(false)
|
||||
} else {
|
||||
setShowAuthPanel(false);
|
||||
setInputFocused(false);
|
||||
setShowAuthPanel(false)
|
||||
setInputFocused(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (action === "enter" && layerDepth() === 0) {
|
||||
setLayerDepth(1);
|
||||
setLayerDepth(1)
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const renderContent = () => {
|
||||
const tab = activeTab();
|
||||
@@ -180,14 +185,17 @@ export function App() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
theme={appStore.resolveTheme()}
|
||||
header={
|
||||
<TabNavigation activeTab={activeTab()} onTabSelect={setActiveTab} />
|
||||
}
|
||||
footer={<Navigation activeTab={activeTab()} onTabSelect={setActiveTab} />}
|
||||
>
|
||||
<box style={{ padding: 1 }}>{renderContent()}</box>
|
||||
</Layout>
|
||||
<ThemeProvider>
|
||||
<Layout
|
||||
theme={appStore.resolveTheme()}
|
||||
layerDepth={layerDepth()}
|
||||
header={
|
||||
<TabNavigation activeTab={activeTab()} onTabSelect={setActiveTab} />
|
||||
}
|
||||
footer={<Navigation activeTab={activeTab()} onTabSelect={setActiveTab} />}
|
||||
>
|
||||
<box style={{ padding: 1 }}>{renderContent()}</box>
|
||||
</Layout>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
30
src/components/LayerIndicator.tsx
Normal file
30
src/components/LayerIndicator.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useRenderer } from "@opentui/solid"
|
||||
|
||||
export function LayerIndicator({ layerDepth }: { layerDepth: number }) {
|
||||
const renderer = useRenderer()
|
||||
|
||||
const getLayerIndicator = () => {
|
||||
const indicators = []
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const isActive = i <= layerDepth
|
||||
const color = isActive ? "#f6c177" : "#4c566a"
|
||||
const size = isActive ? "●" : "○"
|
||||
indicators.push(
|
||||
<text fg={color} marginRight={1}>
|
||||
{size}
|
||||
</text>
|
||||
)
|
||||
}
|
||||
return indicators
|
||||
}
|
||||
|
||||
return (
|
||||
<box flexDirection="row" alignItems="center">
|
||||
<text fg="#7d8590" marginRight={1}>Depth:</text>
|
||||
{getLayerIndicator()}
|
||||
<text fg="#7d8590" marginLeft={1}>
|
||||
{layerDepth}
|
||||
</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,109 @@
|
||||
import type { JSX } from "solid-js"
|
||||
import type { ThemeColors } from "../types/settings"
|
||||
import type { ThemeColors, LayerBackgrounds } from "../types/settings"
|
||||
import { LayerIndicator } from "./LayerIndicator"
|
||||
|
||||
type LayerConfig = {
|
||||
depth: number
|
||||
background: string
|
||||
}
|
||||
|
||||
type LayoutProps = {
|
||||
header?: JSX.Element
|
||||
footer?: JSX.Element
|
||||
children?: JSX.Element
|
||||
theme?: ThemeColors
|
||||
layerDepth?: number
|
||||
}
|
||||
|
||||
export function Layout(props: LayoutProps) {
|
||||
const theme = props.theme
|
||||
|
||||
// Get layer configuration based on depth
|
||||
const getLayerConfig = (depth: number): LayerConfig => {
|
||||
if (!theme?.layerBackgrounds) {
|
||||
return { depth: 0, background: "transparent" }
|
||||
}
|
||||
|
||||
const backgrounds = theme.layerBackgrounds
|
||||
const depthMap: Record<number, LayerConfig> = {
|
||||
0: { depth: 0, background: backgrounds.layer0 },
|
||||
1: { depth: 1, background: backgrounds.layer1 },
|
||||
2: { depth: 2, background: backgrounds.layer2 },
|
||||
3: { depth: 3, background: backgrounds.layer3 },
|
||||
}
|
||||
|
||||
return depthMap[depth] || { depth: 0, background: "transparent" }
|
||||
}
|
||||
|
||||
// Get current layer background
|
||||
const currentLayer = getLayerConfig(props.layerDepth || 0)
|
||||
|
||||
return (
|
||||
<box
|
||||
flexDirection="column"
|
||||
width="100%"
|
||||
height="100%"
|
||||
backgroundColor={props.theme?.background}
|
||||
backgroundColor={theme?.background}
|
||||
>
|
||||
{props.header ? <box style={{ height: 3 }}>{props.header}</box> : <text></text>}
|
||||
<box style={{ flexGrow: 1 }}>{props.children}</box>
|
||||
{props.footer ? <box style={{ height: 1 }}>{props.footer}</box> : <text></text>}
|
||||
{/* Header */}
|
||||
{props.header ? (
|
||||
<box
|
||||
style={{
|
||||
height: 4,
|
||||
backgroundColor: theme?.surface,
|
||||
}}
|
||||
>
|
||||
<box style={{ padding: 1 }}>
|
||||
{props.header}
|
||||
</box>
|
||||
</box>
|
||||
) : (
|
||||
<box style={{ height: 4 }} />
|
||||
)}
|
||||
|
||||
{/* Main content area with layer background */}
|
||||
<box
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
backgroundColor: currentLayer.background,
|
||||
paddingLeft: 2,
|
||||
paddingRight: 2,
|
||||
}}
|
||||
>
|
||||
<box style={{ flexGrow: 1 }}>
|
||||
{props.children}
|
||||
</box>
|
||||
</box>
|
||||
|
||||
{/* Footer */}
|
||||
{props.footer ? (
|
||||
<box
|
||||
style={{
|
||||
height: 2,
|
||||
backgroundColor: theme?.surface,
|
||||
}}
|
||||
>
|
||||
<box style={{ padding: 1 }}>
|
||||
{props.footer}
|
||||
</box>
|
||||
</box>
|
||||
) : (
|
||||
<box style={{ height: 2 }} />
|
||||
)}
|
||||
|
||||
{/* Layer indicator */}
|
||||
{props.layerDepth !== undefined && (
|
||||
<box
|
||||
style={{
|
||||
height: 1,
|
||||
backgroundColor: theme?.surface,
|
||||
}}
|
||||
>
|
||||
<box style={{ padding: 1 }}>
|
||||
<LayerIndicator layerDepth={props.layerDepth} />
|
||||
</box>
|
||||
</box>
|
||||
)}
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,67 +1,16 @@
|
||||
import type { ThemeColors, ThemeName } from "../types/settings"
|
||||
import { BASE_THEME_COLORS, BASE_LAYER_BACKGROUND, THEMES_DESKTOP } from "../types/desktop-theme"
|
||||
|
||||
export const DEFAULT_THEME: ThemeColors = {
|
||||
background: "transparent",
|
||||
surface: "#1b1f27",
|
||||
primary: "#6fa8ff",
|
||||
secondary: "#a9b1d6",
|
||||
accent: "#f6c177",
|
||||
text: "#e6edf3",
|
||||
muted: "#7d8590",
|
||||
warning: "#f0b429",
|
||||
error: "#f47067",
|
||||
success: "#3fb950",
|
||||
...BASE_THEME_COLORS,
|
||||
layerBackgrounds: BASE_LAYER_BACKGROUND,
|
||||
}
|
||||
|
||||
export const THEMES: Record<ThemeName, ThemeColors> = {
|
||||
system: DEFAULT_THEME,
|
||||
catppuccin: {
|
||||
background: "transparent",
|
||||
surface: "#1e1e2e",
|
||||
primary: "#89b4fa",
|
||||
secondary: "#cba6f7",
|
||||
accent: "#f9e2af",
|
||||
text: "#cdd6f4",
|
||||
muted: "#7f849c",
|
||||
warning: "#fab387",
|
||||
error: "#f38ba8",
|
||||
success: "#a6e3a1",
|
||||
},
|
||||
gruvbox: {
|
||||
background: "transparent",
|
||||
surface: "#282828",
|
||||
primary: "#fabd2f",
|
||||
secondary: "#83a598",
|
||||
accent: "#fe8019",
|
||||
text: "#ebdbb2",
|
||||
muted: "#928374",
|
||||
warning: "#fabd2f",
|
||||
error: "#fb4934",
|
||||
success: "#b8bb26",
|
||||
},
|
||||
tokyo: {
|
||||
background: "transparent",
|
||||
surface: "#1a1b26",
|
||||
primary: "#7aa2f7",
|
||||
secondary: "#bb9af7",
|
||||
accent: "#e0af68",
|
||||
text: "#c0caf5",
|
||||
muted: "#565f89",
|
||||
warning: "#e0af68",
|
||||
error: "#f7768e",
|
||||
success: "#9ece6a",
|
||||
},
|
||||
nord: {
|
||||
background: "transparent",
|
||||
surface: "#2e3440",
|
||||
primary: "#88c0d0",
|
||||
secondary: "#81a1c1",
|
||||
accent: "#ebcb8b",
|
||||
text: "#eceff4",
|
||||
muted: "#4c566a",
|
||||
warning: "#ebcb8b",
|
||||
error: "#bf616a",
|
||||
success: "#a3be8c",
|
||||
},
|
||||
catppuccin: THEMES_DESKTOP.variants.find((v) => v.name === "catppuccin")!.colors,
|
||||
gruvbox: THEMES_DESKTOP.variants.find((v) => v.name === "gruvbox")!.colors,
|
||||
tokyo: THEMES_DESKTOP.variants.find((v) => v.name === "tokyo")!.colors,
|
||||
nord: THEMES_DESKTOP.variants.find((v) => v.name === "nord")!.colors,
|
||||
custom: DEFAULT_THEME,
|
||||
}
|
||||
|
||||
50
src/context/ThemeContext.tsx
Normal file
50
src/context/ThemeContext.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { createContext, useContext, createSignal } from "solid-js"
|
||||
import type { ThemeColors, ThemeName } from "../types/settings"
|
||||
|
||||
type ThemeContextType = {
|
||||
themeName: () => ThemeName
|
||||
setThemeName: (theme: ThemeName) => void
|
||||
resolvedTheme: ThemeColors
|
||||
isSystemTheme: () => boolean
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType>()
|
||||
|
||||
export function ThemeProvider({ children }: { children: any }) {
|
||||
const [themeName, setThemeName] = createSignal<ThemeName>("system")
|
||||
|
||||
const isSystemTheme = () => themeName() === "system"
|
||||
|
||||
const resolvedTheme = {
|
||||
background: "transparent",
|
||||
surface: "#1b1f27",
|
||||
primary: "#6fa8ff",
|
||||
secondary: "#a9b1d6",
|
||||
accent: "#f6c177",
|
||||
text: "#e6edf3",
|
||||
muted: "#7d8590",
|
||||
warning: "#f0b429",
|
||||
error: "#f47067",
|
||||
success: "#3fb950",
|
||||
layerBackgrounds: {
|
||||
layer0: "transparent",
|
||||
layer1: "#1e222e",
|
||||
layer2: "#161b22",
|
||||
layer3: "#0d1117",
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ themeName, setThemeName, resolvedTheme, isSystemTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext)
|
||||
if (!context) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { useKeyboard, useRenderer } from "@opentui/solid"
|
||||
import type { TabId } from "../components/Tab"
|
||||
import type { Accessor } from "solid-js"
|
||||
|
||||
const TAB_ORDER: TabId[] = ["discover", "feeds", "search", "player", "settings"]
|
||||
|
||||
@@ -14,6 +15,8 @@ type ShortcutOptions = {
|
||||
onAction?: (action: string) => void
|
||||
inputFocused?: boolean
|
||||
navigationEnabled?: boolean
|
||||
layerDepth?: Accessor<number>
|
||||
onLayerChange?: (newDepth: number) => void
|
||||
}
|
||||
|
||||
export function useAppKeyboard(options: ShortcutOptions) {
|
||||
@@ -55,6 +58,26 @@ export function useAppKeyboard(options: ShortcutOptions) {
|
||||
return
|
||||
}
|
||||
|
||||
// Layer navigation with left/right arrows
|
||||
if (options.layerDepth !== undefined && options.onLayerChange) {
|
||||
const currentDepth = options.layerDepth()
|
||||
const maxLayers = 3
|
||||
|
||||
if (key.name === "right") {
|
||||
if (currentDepth < maxLayers) {
|
||||
options.onLayerChange(currentDepth + 1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (key.name === "left") {
|
||||
if (currentDepth > 0) {
|
||||
options.onLayerChange(currentDepth - 1)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Tab navigation with left/right arrows OR [ and ]
|
||||
if (key.name === "right" || key.name === "]") {
|
||||
options.onTabChange(getNextTab(options.activeTab))
|
||||
|
||||
152
src/types/desktop-theme.ts
Normal file
152
src/types/desktop-theme.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import type {
|
||||
DesktopTheme,
|
||||
ThemeColors,
|
||||
ThemeName,
|
||||
ThemeToken,
|
||||
ThemeVariant,
|
||||
} from "../types/settings"
|
||||
|
||||
// Base theme colors
|
||||
export const BASE_THEME_COLORS: ThemeColors = {
|
||||
background: "transparent",
|
||||
surface: "#1b1f27",
|
||||
primary: "#6fa8ff",
|
||||
secondary: "#a9b1d6",
|
||||
accent: "#f6c177",
|
||||
text: "#e6edf3",
|
||||
muted: "#7d8590",
|
||||
warning: "#f0b429",
|
||||
error: "#f47067",
|
||||
success: "#3fb950",
|
||||
}
|
||||
|
||||
// Base layer backgrounds
|
||||
export const BASE_LAYER_BACKGROUND: ThemeColors["layerBackgrounds"] = {
|
||||
layer0: "transparent",
|
||||
layer1: "#1e222e",
|
||||
layer2: "#161b22",
|
||||
layer3: "#0d1117",
|
||||
}
|
||||
|
||||
// Theme tokens
|
||||
export const BASE_THEME_TOKENS: ThemeToken = {
|
||||
"background": "transparent",
|
||||
"surface": "#1b1f27",
|
||||
"primary": "#6fa8ff",
|
||||
"secondary": "#a9b1d6",
|
||||
"accent": "#f6c177",
|
||||
"text": "#e6edf3",
|
||||
"muted": "#7d8590",
|
||||
"warning": "#f0b429",
|
||||
"error": "#f47067",
|
||||
"success": "#3fb950",
|
||||
"layer0": "transparent",
|
||||
"layer1": "#1e222e",
|
||||
"layer2": "#161b22",
|
||||
"layer3": "#0d1117",
|
||||
}
|
||||
|
||||
// Desktop theme structure
|
||||
export const THEMES_DESKTOP: DesktopTheme = {
|
||||
name: "PodTUI",
|
||||
variants: [
|
||||
{
|
||||
name: "catppuccin",
|
||||
colors: {
|
||||
background: "transparent",
|
||||
surface: "#1e1e2e",
|
||||
primary: "#89b4fa",
|
||||
secondary: "#cba6f7",
|
||||
accent: "#f9e2af",
|
||||
text: "#cdd6f4",
|
||||
muted: "#7f849c",
|
||||
warning: "#fab387",
|
||||
error: "#f38ba8",
|
||||
success: "#a6e3a1",
|
||||
layerBackgrounds: {
|
||||
layer0: "transparent",
|
||||
layer1: "#181825",
|
||||
layer2: "#11111b",
|
||||
layer3: "#0a0a0f",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gruvbox",
|
||||
colors: {
|
||||
background: "transparent",
|
||||
surface: "#282828",
|
||||
primary: "#fabd2f",
|
||||
secondary: "#83a598",
|
||||
accent: "#fe8019",
|
||||
text: "#ebdbb2",
|
||||
muted: "#928374",
|
||||
warning: "#fabd2f",
|
||||
error: "#fb4934",
|
||||
success: "#b8bb26",
|
||||
layerBackgrounds: {
|
||||
layer0: "transparent",
|
||||
layer1: "#32302a",
|
||||
layer2: "#1d2021",
|
||||
layer3: "#0d0c0c",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tokyo",
|
||||
colors: {
|
||||
background: "transparent",
|
||||
surface: "#1a1b26",
|
||||
primary: "#7aa2f7",
|
||||
secondary: "#bb9af7",
|
||||
accent: "#e0af68",
|
||||
text: "#c0caf5",
|
||||
muted: "#565f89",
|
||||
warning: "#e0af68",
|
||||
error: "#f7768e",
|
||||
success: "#9ece6a",
|
||||
layerBackgrounds: {
|
||||
layer0: "transparent",
|
||||
layer1: "#16161e",
|
||||
layer2: "#0f0f15",
|
||||
layer3: "#08080b",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nord",
|
||||
colors: {
|
||||
background: "transparent",
|
||||
surface: "#2e3440",
|
||||
primary: "#88c0d0",
|
||||
secondary: "#81a1c1",
|
||||
accent: "#ebcb8b",
|
||||
text: "#eceff4",
|
||||
muted: "#4c566a",
|
||||
warning: "#ebcb8b",
|
||||
error: "#bf616a",
|
||||
success: "#a3be8c",
|
||||
layerBackgrounds: {
|
||||
layer0: "transparent",
|
||||
layer1: "#3b4252",
|
||||
layer2: "#242933",
|
||||
layer3: "#1a1c23",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultVariant: "catppuccin",
|
||||
tokens: BASE_THEME_TOKENS,
|
||||
}
|
||||
|
||||
// Helper function to get theme by name
|
||||
export function getThemeByName(name: ThemeName): ThemeVariant | undefined {
|
||||
return THEMES_DESKTOP.variants.find((variant) => variant.name === name)
|
||||
}
|
||||
|
||||
// Helper function to get default theme
|
||||
export function getDefaultTheme(): ThemeVariant {
|
||||
return THEMES_DESKTOP.variants.find(
|
||||
(variant) => variant.name === THEMES_DESKTOP.defaultVariant
|
||||
)!
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
export type ThemeName = "system" | "catppuccin" | "gruvbox" | "tokyo" | "nord" | "custom"
|
||||
|
||||
export type LayerBackgrounds = {
|
||||
layer0: string
|
||||
layer1: string
|
||||
layer2: string
|
||||
layer3: string
|
||||
}
|
||||
|
||||
export type ThemeColors = {
|
||||
background: string
|
||||
surface: string
|
||||
@@ -11,6 +18,27 @@ export type ThemeColors = {
|
||||
warning: string
|
||||
error: string
|
||||
success: string
|
||||
layerBackgrounds?: LayerBackgrounds
|
||||
}
|
||||
|
||||
export type ThemeVariant = {
|
||||
name: string
|
||||
colors: ThemeColors
|
||||
}
|
||||
|
||||
export type ThemeToken = {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export type ResolvedTheme = ThemeColors & {
|
||||
layerBackgrounds: LayerBackgrounds
|
||||
}
|
||||
|
||||
export type DesktopTheme = {
|
||||
name: string
|
||||
variants: ThemeVariant[]
|
||||
defaultVariant: string
|
||||
tokens: ThemeToken
|
||||
}
|
||||
|
||||
export type AppSettings = {
|
||||
|
||||
Reference in New Issue
Block a user