improve preview & responsive ness

This commit is contained in:
hsjobeki 2022-12-11 21:22:45 +01:00
parent 521fb6e03f
commit ae81eb869b
8 changed files with 367 additions and 309 deletions

View File

@ -20,7 +20,7 @@ import FormControlLabel from "@mui/material/FormControlLabel";
import FormControl from "@mui/material/FormControl";
import FormLabel from "@mui/material/FormLabel";
import ClearIcon from "@mui/icons-material/Clear";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import { NixType, nixTypes } from "../../types/nix";
export type BasicListItem = {
@ -28,76 +28,11 @@ export type BasicListItem = {
key: string;
};
export type BasicListProps = BasicDataViewProps & {
handleFilter: (t: NixType, mode: "from" | "to") => void;
preview: React.ReactNode;
handleFilter: (filter: { from: NixType; to: NixType }) => void;
selected?: string | null;
itemsPerPage: number;
};
interface SelectOptionProps {
label: string;
handleChange: (value: string) => void;
options: {
value: string;
label: string;
}[];
}
const SelectOption = (props: SelectOptionProps) => {
const { label, handleChange, options } = props;
const [value, setValue] = React.useState<NixType>("any");
const _handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newVal = (event.target as HTMLInputElement).value as NixType;
setValue(newVal);
handleChange(newVal);
};
const handleClear = () => {
setValue("any");
handleChange("any");
};
return (
<FormControl
sx={{
// pl: 1.5,
flexDirection: "row",
}}
>
<FormLabel sx={{ width: "11rem", wordWrap: "unset" }}>
<Typography>
<IconButton aria-label="clear-button" onClick={handleClear}>
<ClearIcon />
</IconButton>
{label}
</Typography>
</FormLabel>
<RadioGroup
sx={{
// pl: 1.5,
width: "100%",
"&.MuiFormGroup-root": {
flexDirection: "row",
},
}}
value={value}
onChange={_handleChange}
>
{options.map(({ value, label }) => (
<FormControlLabel
key={value}
value={value}
control={<Radio />}
label={label}
/>
))}
</RadioGroup>
</FormControl>
);
};
export function BasicList(props: BasicListProps) {
const {
items,
@ -105,7 +40,6 @@ export function BasicList(props: BasicListProps) {
itemsPerPage,
handleSearch,
handleFilter,
preview,
selected = "",
} = props;
// const [from, setFrom] = useState<NixType>("any");
@ -128,8 +62,8 @@ export function BasicList(props: BasicListProps) {
setPage(value);
};
const _handleFilter = (t: NixType, mode: "from" | "to") => {
handleFilter(t, mode);
const _handleFilter = (filter: { from: NixType; to: NixType }) => {
handleFilter(filter);
setPage(1);
};
@ -142,76 +76,15 @@ export function BasicList(props: BasicListProps) {
return (
<Stack>
<SearchInput
handleFilter={_handleFilter}
placeholder="search nix functions"
handleSearch={_handleSearch}
clearSearch={() => _handleSearch("")}
/>
<Box>
{/* <Stack direction="row"> */}
<Grid container>
<Grid item xs={12} md={5}>
<SelectOption
label="from type"
handleChange={(value) => {
_handleFilter(value as NixType, "from");
}}
options={nixTypes.map((v) => ({ value: v, label: v }))}
/>
</Grid>
<Grid
item
md={2}
sx={{
display: {
md: "flex",
xs: "none",
},
justifyContent: "center",
alignItems: "center",
}}
>
<Typography
// sx={{
// width: "100%",
// display: "flex",
// justifyContent: "center",
// alignItems: "center",
// // flexDirection: "column",
// }}
>
<ChevronRightIcon />
</Typography>
</Grid>
<Grid item xs={12} md={5}>
<SelectOption
label="to type"
handleChange={(value) => {
_handleFilter(value as NixType, "to");
}}
options={nixTypes.map((v) => ({ value: v, label: v }))}
/>
</Grid>
</Grid>
{/* </Stack> */}
</Box>
<List aria-label="basic-list" sx={{ pt: 0 }}>
{pageItems.map(({ item, key }, idx) => (
<Box key={`${key}-${idx}`}>
<Slide
direction="up"
in={key === selected}
mountOnEnter
unmountOnExit
>
<ListItem
key={`${key}-preview`}
aria-label={`item-${key}`}
sx={{ px: 0 }}
>
{preview}
</ListItem>
{/* )} */}
</Slide>
<ListItem sx={{ px: 0 }} key={key} aria-label={`item-${key}`}>
{item}
</ListItem>

View File

@ -1,39 +1,74 @@
import { ListItemText, Paper, Stack, Typography } from "@mui/material";
import { useMemo } from "react";
import { DocItem } from "../../types/nix";
import { Preview } from "../preview/preview";
interface FunctionItemProps {
selected: boolean;
name: String;
docItem: DocItem;
handleClose: () => void;
}
export default function FunctionItem(props: FunctionItemProps) {
const { name, docItem, selected } = props;
const { fn_type, category } = docItem;
const { name, docItem, selected, handleClose } = props;
const { fn_type, category, description } = docItem;
const descriptionPreview = useMemo(() => {
const getFirstWords = (s: string) => {
const indexOfDot = s.indexOf(".");
if (indexOfDot) {
return s.slice(0, indexOfDot + 1);
}
return s.split(" ").filter(Boolean).slice(0, 10).join(" ");
};
if (typeof description === "object") {
const singleString = description.join("");
return getFirstWords(singleString);
} else if (description) {
return getFirstWords(description);
} else {
return "";
}
}, [description]);
return (
<Paper
elevation={0}
sx={{
cursor: "pointer",
cursor: !selected ? "pointer" : "default",
display: "flex",
justifyContent: "left",
px: 2,
py: 1,
color: selected ? "primary.main" : undefined,
borderColor: selected ? "action.selected" : "none",
borderColor: selected ? "primary.light" : "none",
borderWidth: 1,
borderStyle: selected ? "solid" : "none",
"&:hover": {
backgroundColor: "action.hover",
},
"&:hover": !selected
? {
backgroundColor: "action.hover",
}
: {},
}}
>
<Stack>
<ListItemText primary={name} secondary={category} />
<Typography
sx={{
color: !fn_type ? "text.secondary" : "text.primary",
}}
>{`${fn_type || "No type yet provided"} `}</Typography>
<Stack sx={{ width: "100%" }}>
{!selected && (
<>
<ListItemText
primary={`${
category.includes(".nix") ? "lib" : "builtins"
}.${name}`}
secondary={category}
/>
<ListItemText secondary={descriptionPreview} />
<Typography
sx={{
color: !fn_type ? "text.secondary" : "text.primary",
}}
>
{`${fn_type || "No type yet provided"} `}
</Typography>
</>
)}
{selected && <Preview docItem={docItem} handleClose={handleClose} />}
</Stack>
</Paper>
);

View File

@ -107,6 +107,8 @@ export function Layout(props: LayoutProps) {
marginTop: "6em",
maxHeight: "calc(100vh - 8em)",
overflowY: "scroll",
overflowX: "hidden",
width: "100vw",
}}
>
<Container sx={{ pt: 0, px: { xs: 0, md: 2 } }} maxWidth="xl">

View File

@ -0,0 +1,4 @@
.hljs {
padding: 0 !important;
}

View File

@ -20,7 +20,7 @@ import { DocItem } from "../../types/nix";
import CodeIcon from "@mui/icons-material/Code";
import ReactMarkdown from "react-markdown";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import styles from "./preview.module.css";
// import hljs from "highlight.js";
// needed for nextjs to import the classes of github theme
@ -66,33 +66,45 @@ export const Preview = (props: PreviewProps) => {
<Box
sx={{
p: 1,
pt: 2,
mt: 2,
mb: -2,
width: "100%",
borderTop: "solid 1px",
borderTopColor: "primary.main",
overflow: "none",
}}
>
{/* <Box sx={{ display: "flex" }}> */}
<Typography variant="h2">{`${prefix}.${name}`}</Typography>
{/* </Box> */}
<Box
sx={{
display: { md: "flex", xs: "flex" },
flexDirection: { md: "row", xs: "column-reverse" },
width: "100%",
}}
>
<Typography
variant="h4"
sx={{ wordWrap: "normal", lineBreak: "anywhere" }}
>{`${prefix}.${name}`}</Typography>
<Tooltip title="close details">
<IconButton
sx={{
mx: { xs: "auto", md: 1 },
left: { lg: "calc(50% - 2rem)", xs: "unset" },
position: { lg: "absolute", xs: "relative" },
}}
size="small"
onClick={() => handleClose()}
>
<ExpandLessIcon fontSize="large" />
</IconButton>
</Tooltip>
</Box>
<List sx={{ width: "100%" }}>
<ListItem>
<Tooltip title="close details">
<IconButton onClick={() => handleClose()}>
<ExpandLessIcon />
</IconButton>
</Tooltip>
</ListItem>
<ListItem>
<ListItem sx={{ flexDirection: { xs: "column", sm: "row" } }}>
<ListItemIcon>
<LocalLibraryIcon />
<LocalLibraryIcon sx={{ m: "auto" }} />
</ListItemIcon>
<ListItemText
sx={{
overflow: "hidden",
textOverflow: "ellipsis",
alignSelf: "flex-start",
}}
primaryTypographyProps={{
color: "text.secondary",
@ -105,6 +117,7 @@ export const Preview = (props: PreviewProps) => {
primary={"nixpkgs/" + category.replace("./", "")}
secondary={
<Container
component={"div"}
sx={{ ml: "0 !important", pl: "0 !important" }}
maxWidth="lg"
>
@ -117,14 +130,15 @@ export const Preview = (props: PreviewProps) => {
}
/>
</ListItem>
<ListItem>
<ListItem sx={{ flexDirection: { xs: "column", sm: "row" } }}>
<ListItemIcon>
<InputIcon />
<InputIcon sx={{ m: "auto" }} />
</ListItemIcon>
<ListItemText
sx={{
overflow: "hidden",
textOverflow: "ellipsis",
alignSelf: "flex-start",
}}
primaryTypographyProps={{
color: "text.secondary",
@ -141,15 +155,17 @@ export const Preview = (props: PreviewProps) => {
<ListItem
sx={{
backgroundColor: "background.paper",
flexDirection: { xs: "column", sm: "row" },
}}
>
<ListItemIcon>
<CodeIcon />
<CodeIcon sx={{ m: "auto" }} />
</ListItemIcon>
<ListItemText
sx={{
overflow: "hidden",
textOverflow: "ellipsis",
alignSelf: "flex-start",
}}
disableTypography
primary={
@ -161,8 +177,10 @@ export const Preview = (props: PreviewProps) => {
}
secondary={
finalExample ? (
<Box sx={{ mt: -2, pl: 1.5 }}>
<Highlight className="nix">{finalExample}</Highlight>
<Box sx={{ mt: -2 }}>
<Highlight className={`nix ${styles.hljs}`}>
{finalExample}
</Highlight>
</Box>
) : (
<Typography

View File

@ -4,18 +4,92 @@ import InputBase from "@mui/material/InputBase";
import IconButton from "@mui/material/IconButton";
import SearchIcon from "@mui/icons-material/Search";
import ClearIcon from "@mui/icons-material/Clear";
import { debounce } from "@mui/material";
import {
Box,
debounce,
FormControl,
FormControlLabel,
FormLabel,
Grid,
Radio,
RadioGroup,
Typography,
} from "@mui/material";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import { NixType, nixTypes } from "../../types/nix";
interface SelectOptionProps {
label: string;
handleChange: (value: string) => void;
value: string;
options: {
value: string;
label: string;
}[];
}
const SelectOption = (props: SelectOptionProps) => {
const { label, handleChange, options, value } = props;
const _handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newVal = (event.target as HTMLInputElement).value as NixType;
handleChange(newVal);
};
return (
<FormControl
sx={{
flexDirection: "row",
}}
>
<FormLabel
sx={{
width: "7rem",
wordWrap: "unset",
margin: "auto",
padding: 1,
}}
>
<Typography sx={{ minWidth: "max-content" }}>{label}</Typography>
</FormLabel>
<RadioGroup
sx={{
width: "100%",
"&.MuiFormGroup-root": {
flexDirection: "row",
},
}}
value={value}
onChange={_handleChange}
>
{options.map(({ value, label }) => (
<FormControlLabel
key={value}
value={value}
control={<Radio />}
label={label}
/>
))}
</RadioGroup>
</FormControl>
);
};
export interface SearchInputProps {
handleSearch: (term: string) => void;
handleFilter: (filter: { to: NixType; from: NixType }) => void;
clearSearch: () => void;
placeholder: string;
}
export function SearchInput(props: SearchInputProps) {
const { handleSearch, clearSearch, placeholder } = props;
const { handleSearch, clearSearch, placeholder, handleFilter } = props;
const [term, setTerm] = useState("");
const [to, setTo] = useState<NixType>("any");
const [from, setFrom] = useState<NixType>("any");
const handleSubmit = React.useRef((input: string) => {
handleSearch(input);
@ -25,8 +99,21 @@ export function SearchInput(props: SearchInputProps) {
[handleSubmit]
);
const _handleFilter = (t: NixType, mode: "from" | "to") => {
console.log({ t, mode });
if (mode === "to") {
setTo(t);
handleFilter({ to: t, from });
} else {
setFrom(t);
handleFilter({ to, from: t });
}
};
const handleClear = () => {
setTerm("");
setFrom("any");
setTo("any");
clearSearch();
};
const handleChange = (
@ -37,53 +124,95 @@ export function SearchInput(props: SearchInputProps) {
};
return (
<Paper
component="form"
elevation={0}
sx={{
width: "100%",
p: 1,
my: 2,
display: "flex",
alignItems: "center",
}}
onSubmit={(e: React.FormEvent) => {
e.preventDefault();
handleSubmit(term);
}}
>
<IconButton aria-label="clear-button" onClick={handleClear}>
<ClearIcon />
</IconButton>
<InputBase
autoFocus
<>
<Paper
component="form"
elevation={0}
sx={{
ml: 1,
flex: 1,
backgroundColor: "paper.main",
width: "100%",
p: 1,
my: 2,
display: "flex",
alignItems: "center",
}}
placeholder={placeholder}
inputProps={{ "aria-label": "search-input" }}
value={term}
onChange={(e) => handleChange(e)}
/>
<IconButton
type="submit"
onClick={() => handleSubmit(term)}
sx={{
p: 1,
bgcolor: "primary.dark",
color: "common.white",
"&:hover": {
backgroundColor: "primary.main",
opacity: [0.9, 0.8, 0.7],
},
onSubmit={(e: React.FormEvent) => {
e.preventDefault();
handleSubmit(term);
}}
aria-label="search-button"
>
<SearchIcon />
</IconButton>
</Paper>
<IconButton aria-label="clear-button" onClick={handleClear}>
<ClearIcon />
</IconButton>
<InputBase
autoFocus
sx={{
ml: 1,
flex: 1,
backgroundColor: "paper.main",
p: 1,
}}
placeholder={placeholder}
inputProps={{ "aria-label": "search-input" }}
value={term}
onChange={(e) => handleChange(e)}
/>
<IconButton
type="submit"
onClick={() => handleSubmit(term)}
sx={{
p: 1,
bgcolor: "primary.dark",
color: "common.white",
"&:hover": {
backgroundColor: "primary.main",
opacity: [0.9, 0.8, 0.7],
},
}}
aria-label="search-button"
>
<SearchIcon />
</IconButton>
</Paper>
<Box>
<Grid container>
<Grid item xs={12} md={5}>
<SelectOption
value={from}
label="from type"
handleChange={(value) => {
_handleFilter(value as NixType, "from");
}}
options={nixTypes.map((v) => ({ value: v, label: v }))}
/>
</Grid>
<Grid
item
md={2}
sx={{
display: {
md: "flex",
xs: "none",
},
justifyContent: "center",
alignItems: "center",
}}
>
<Typography>
<ChevronRightIcon />
</Typography>
</Grid>
<Grid item xs={12} md={5}>
<SelectOption
value={to}
label="to type"
handleChange={(value) => {
_handleFilter(value as NixType, "to");
}}
options={nixTypes.map((v) => ({ value: v, label: v }))}
/>
</Grid>
</Grid>
</Box>
</>
);
}

View File

@ -3,7 +3,6 @@ import { useState, useMemo } from "react";
import { Box } from "@mui/material";
import FunctionItem from "../components/functionItem/functionItem";
import { NixType, nixTypes, MetaData, DocItem } from "../types/nix";
import { Preview } from "../components/preview/preview";
import nixLibs from "../models/lib.json";
import nixBuiltins from "../models/builtins.json";
@ -36,69 +35,81 @@ const search =
});
};
const preProcess = (a: string | undefined) => {
if (a) {
let b = a;
if (a.includes("::")) {
b = a.split("::")[1];
function getTypes(
fnName: string,
fnType: string | undefined
): { args: NixType[]; types: NixType[] } {
if (fnType) {
let cleanType = fnType.replace(/ /g, "").replace(`${fnName}::`, "");
const tokens = cleanType
.split(/(::|->|\[|\]|\{|\}|\(|\))/gm)
.filter(Boolean);
const lastArrowIdx = tokens.lastIndexOf("->");
if (lastArrowIdx) {
// Function has at least on return value
const interpretToken = (token: string) => {
if (token === "(" || token === ")") {
return "function" as NixType;
} else if (token === "[" || token === "]") {
return "list" as NixType;
} else if (token === "{" || token === "}") {
return "attrset" as NixType;
} else if (nixTypes.includes(token.toLowerCase() as NixType)) {
return token.toLowerCase() as NixType;
} else if (
token.length === 1 &&
["a", "b", "c", "d", "e"].includes(token)
) {
return "any" as NixType;
} else {
return undefined;
}
};
const returnValueTokens = tokens.slice(lastArrowIdx + 1);
const types = returnValueTokens
.map(interpretToken)
.filter(Boolean)
.filter((e, i, s) => s.indexOf(e) === i);
const args = tokens
.slice(0, lastArrowIdx)
.map(interpretToken)
.filter(Boolean)
.filter((e, i, s) => s.indexOf(e) === i);
return { args, types } as { args: NixType[]; types: NixType[] };
}
const cleaned = b?.replace("(", "").replace(")", "").trim();
let typ = cleaned;
if (cleaned.match(/\[(.*)\]/)) {
typ = "list";
}
if (
cleaned.toLowerCase().includes("attrset") ||
cleaned.trim().startsWith("{")
) {
typ = "attrset";
}
if (cleaned.length === 1 && ["a", "b", "c", "d", "e"].includes(cleaned)) {
typ = "any";
}
return typ;
}
return a;
};
return { args: ["any"], types: ["any"] };
}
const filterByType =
(to: NixType[], from: NixType[]) =>
({ to, from }: { to: NixType; from: NixType }) =>
(data: MetaData): MetaData => {
//if user wants any data show all
if (to.includes("any") && from.includes("any")) {
if (to === "any" && from === "any") {
return data;
} else {
return data.filter(
// TODO: Implement proper type matching
({ name, fn_type }) => {
if (fn_type) {
const parsedType = getTypes(name, fn_type);
return (
parsedType.args.includes(from) && parsedType.types.includes(to)
);
} else {
return to === "any" && from === "any";
}
}
);
}
return data.filter(
// TODO: Implement proper type matching
({ name, fn_type }) => {
if (fn_type) {
const cleanType = fn_type.replace(/ /g, "").replace(`${name}::`, "");
const args = cleanType.split("->");
const front = args.slice(0, -1);
const parsedInpTypes = front.map(preProcess);
const fn_to = args.at(-1);
const parsedOutType = preProcess(fn_to);
return (
from.some((f) => parsedInpTypes.join(" ").includes(f)) &&
to.some((t) => parsedOutType?.includes(t))
);
}
//function type could not be detected only show those without filters
if (fn_type === null) {
return to.includes("any") && from.includes("any");
}
return false;
}
);
};
const initialTypes = nixTypes;
export default function FunctionsPage() {
const [selected, setSelected] = useState<string | null>(null);
const [term, setTerm] = useState<string>("");
const [to, setTo] = useState<NixType[]>(initialTypes);
const [from, setFrom] = useState<NixType[]>(initialTypes);
const [filter, setFilter] = useState<{ to: NixType; from: NixType }>({
to: "any",
from: "any",
});
const handleSelect = (key: string) => {
setSelected((curr: string | null) => {
@ -111,55 +122,42 @@ export default function FunctionsPage() {
};
const filteredData = useMemo(
() => pipe(filterByType(to, from), search(term))(data),
[to, from, term]
() => pipe(filterByType(filter), search(term))(data),
[filter, term]
);
const handleSearch = (term: string) => {
setTerm(term);
};
const handleFilter = (t: NixType, mode: "to" | "from") => {
let filterBy;
if (t === "any") {
filterBy = nixTypes;
} else {
filterBy = [t];
}
if (mode === "from") {
setFrom(filterBy);
}
if (mode === "to") {
setTo(filterBy);
}
const handleFilter = (filter: { from: NixType; to: NixType }) => {
setFilter(filter);
};
const getKey = (item: DocItem) => `${item.category}/${item.name}`;
const preRenderedItems: BasicListItem[] = filteredData.map(
(docItem: DocItem) => ({
item: (
<Box
sx={{
width: "100%",
height: "100%",
}}
onClick={() => handleSelect(getKey(docItem))}
>
<FunctionItem
name={docItem.name}
docItem={docItem}
selected={selected === getKey(docItem)}
/>
</Box>
),
key: getKey(docItem),
})
);
const preview = (
<Preview
docItem={data.find((f) => getKey(f) === selected) || data[0]}
handleClose={() => setSelected(null)}
/>
(docItem: DocItem) => {
const key = getKey(docItem);
return {
item: (
<Box
sx={{
width: "100%",
height: "100%",
}}
onClick={!(selected === key) ? () => handleSelect(key) : undefined}
>
<FunctionItem
name={docItem.name}
docItem={docItem}
selected={selected === key}
handleClose={() => setSelected(null)}
/>
</Box>
),
key,
};
}
);
return (
@ -170,7 +168,6 @@ export default function FunctionsPage() {
items={preRenderedItems}
handleSearch={handleSearch}
handleFilter={handleFilter}
preview={selected ? preview : null}
/>
</Box>
);

View File

@ -1,4 +1,4 @@
export type NixType = "attrset" | "list" | "string" | "int" | "bool" | "any";
export type NixType = "function" | "attrset" | "list" | "string" | "int" | "bool" | "any";
export const nixTypes: NixType[] = [
"any",
"attrset",