Skip to content

Commit

Permalink
feat: add theming using base16 themes
Browse files Browse the repository at this point in the history
fix issue where page refreshes on query plan update
minor ui tweaks
  • Loading branch information
80avin committed Apr 8, 2024
1 parent 353efb2 commit 9b13eee
Show file tree
Hide file tree
Showing 11 changed files with 359 additions and 71 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ Query Plan Visualizer
- read verbose query plan
- all available properties should be captured. data type of a property should be automatically inferred.
- allow defining custom properties (dimensions)

- custom properties can be javascript like expressions like `deltaCost = '$.statsGroups[0].cost[1] - $.statsGroups[0].cost[0]'`
- use [ jsep ](https://www.npmjs.com/package/jsep) to parse/evaluate expressions similar to https://github.com/JSONPath-Plus/JSONPath/pull/185/files
- search the tree
- theming
- vim like keyboard shortcuts. i.e. composable keyboard shortcuts.

- [x] Create panels for
- tree visualization
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"dependencies": {
"@types/d3-org-chart": "^3.1.2",
"base16": "^1.0.0",
"d3": "^7.9.0",
"d3-org-chart": "^3.1.1",
"events": "^3.3.0",
Expand All @@ -20,6 +21,7 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/base16": "^1.0.5",
"@types/d3": "^7.4.3",
"@types/events": "^3.0.3",
"@types/react": "^18.2.64",
Expand Down
14 changes: 14 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,20 @@
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
filter: drop-shadow(0 0 2em var(--base0E, #646cffaa));
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
filter: drop-shadow(0 0 2em var(--base0D, #61dafbaa));
}

.flex {
display: flex;
}
.gap-8 {
gap: 8px;
}
.f-wrap {
flex-wrap: wrap;
}

@keyframes logo-spin {
Expand Down
31 changes: 13 additions & 18 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { useCallback, useEffect, useRef, useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";
import { PlanViz, planRaw1, sample1 } from "./lib/planViz";
import { planRaw1, sample1 } from "./lib/planViz";
import { TreeView } from "./components/TreeView";
import lzString from "lz-string";
import ThemeSelect from "./components/ThemeSelect";

function updateUrl(doc) {
doc = {
Expand All @@ -16,11 +15,10 @@ function updateUrl(doc) {
"q",
lzString.compressToEncodedURIComponent(JSON.stringify(doc)),
);
window.location.href = url.toString();
window.history.pushState(undefined, "", url);
}

function App() {
const [count, setCount] = useState(0);
const [text, _setText] = useState(planRaw1);
const [code, _output] = sample1(text);
const debounceRef = useRef<number | null>(null);
Expand Down Expand Up @@ -74,7 +72,6 @@ function App() {
}
} catch (err) {}
}, []);
// const output = JSON.stringify(_output, undefined, 2);
return (
<>
<div
Expand All @@ -86,19 +83,17 @@ function App() {
}}
>
<div className="w100pct-on-focus">
<code>{document.title}</code>
<textarea
style={{ whiteSpace: "pre" }}
rows={100}
value={text}
onChange={(e) => setText(e.target.value)}
></textarea>
<div style={{ display: "flex", flexDirection: "column" }}>
<ThemeSelect />
<code>{document.title}</code>
<textarea
style={{ whiteSpace: "pre" }}
rows={100}
value={text}
onChange={(e) => setText(e.target.value)}
></textarea>
</div>
</div>
{/* <textarea */}
{/* style={{ width: "100%" }} */}
{/* rows={100} */}
{/* value={output} */}
{/* ></textarea> */}
<div style={{ width: "100%" }}>
<TreeView data={_output} />
</div>
Expand Down
103 changes: 103 additions & 0 deletions src/components/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {
Dispatch,
SetStateAction,
createContext,
useEffect,
useState,
} from "react";

import * as base16Themes from "base16";

type IThemes = Exclude<keyof typeof base16Themes, "default">;

const THEMES_MAP = Object.fromEntries(
Object.entries(base16Themes).filter(([k]) => k !== "default"),
) as Pick<typeof base16Themes, IThemes>;
const THEMES = Object.keys(THEMES_MAP) as IThemes[];

const THEME_PROPERTIES = [
"base00",
"base01",
"base02",
"base03",
"base04",
"base05",
"base06",
"base07",
"base08",
"base09",
"base0A",
"base0B",
"base0C",
"base0D",
"base0E",
"base0F",
] as const;

type IMode = "light" | "dark";
const applyTheme = (name: keyof typeof THEMES_MAP, mode: IMode = "light") => {
const theme = THEMES_MAP[name];
const light = mode === "light";
for (let i = 0; i < 16; i++) {
document.documentElement.style.setProperty(
`--${THEME_PROPERTIES[i]}`,
theme[THEME_PROPERTIES[i < 8 && !light ? 7 - i : i]],
);
}
document.documentElement.style.setProperty("--scheme", mode);
document.documentElement.style.setProperty("--theme", theme["scheme"]);
localStorage.setItem("theme", JSON.stringify([name, mode]));
};

interface IThemeState {
name: IThemes;
mode: IMode;
}
interface IThemeContextValue {
theme: IThemeState;
THEMES: typeof THEMES;
setTheme: Dispatch<SetStateAction<IThemeState>>;
}
const defaultTheme: IThemeState = {
name: "solarized",
mode: "dark",
};
function loadThemeFromLocalStorage(): IThemeState | null {
const themeStr = localStorage.getItem("theme");
if (!themeStr) return null;
try {
const _theme = JSON.parse(themeStr);
if (_theme[0] in THEMES_MAP && ["light", "dark"].includes(_theme[1])) {
return {
name: _theme[0],
mode: _theme[1],
};
}
} catch (err) {
if (err instanceof SyntaxError) {
console.error("Invalid Theme stored: ", themeStr);
}
}
return null;
}
const initialTheme: IThemeState = loadThemeFromLocalStorage() || defaultTheme;
export const ThemeContext = createContext<IThemeContextValue>({
theme: initialTheme,
THEMES,
setTheme: () => {},
});

function ThemeProvider(props: React.PropsWithChildren<{}>) {
const { children } = props;
const [theme, setTheme] = useState<IThemeState>(initialTheme);
useEffect(() => {
applyTheme(theme.name, theme.mode);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme: setTheme, THEMES }}>
{children}
</ThemeContext.Provider>
);
}

export default ThemeProvider;
43 changes: 43 additions & 0 deletions src/components/ThemeSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useCallback, useContext } from "react";
import { ThemeContext } from "./ThemeProvider";

const ThemeSelect = () => {
const themeContext = useContext(ThemeContext);
const handleThemeChange = useCallback((e) => {
const name = e.target.form["theme-select"].selectedOptions[0].value;
const mode = e.target.form["dark-mode-input"].checked ? "dark" : "light";
themeContext.setTheme({ name, mode });
console.log(e);
}, []);
return (
<>
<form>
<label>
Theme:{" "}
<select name="theme-select" onChange={handleThemeChange}>
{themeContext.THEMES.map((t) => (
<option
key={t}
value={t}
selected={t === themeContext.theme.name}
>
{t}
</option>
))}
</select>
</label>
<label>
Dark
<input
name="dark-mode-input"
onChange={handleThemeChange}
type="checkbox"
checked={themeContext.theme.mode === "dark"}
/>
</label>
</form>
</>
);
};

export default ThemeSelect;
19 changes: 19 additions & 0 deletions src/components/TreeView.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
svg.progress {
width: 100%;
height: 1em;
}
svg.progress text {
color: var(--base07);
font-size: 1em;
}
div.node-card {
background-color: var(--base07);
border: 1px solid var(--base04);
padding: 4px;
}
/* @media (prefers-color-scheme: light) { */
/* div.node-card { */
/* background-color: #efefef; */
/* border-color: #101010; */
/* } */
/* } */
Loading

0 comments on commit 9b13eee

Please sign in to comment.