Skip to content

Commit ec0d818

Browse files
authored
Reference item and landing (#133)
* Corrected styles for code-embed * Styles preview butttons * Responsive styles for preview buttons * Ensure that properties exist in class previews * Fields and methods on class items in reference * Links and more styles for class items * Parenthesize where appropriate * Working filter by keyword * Keep category information when filtering reference results * Small fixes, test updates * Fix typing error that only appears in CI * Remove unecessary rename
1 parent 711bfb0 commit ec0d818

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+957
-161
lines changed

.eslintrc.cjs

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module.exports = {
2525
extends: ["plugin:@typescript-eslint/recommended"],
2626
rules: {
2727
"@typescript-eslint/no-explicit-any": "off",
28+
"react/no-danger": "off",
2829
},
2930
},
3031
],

src/components/CircleButton/index.tsx

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { JSX } from "preact";
2+
3+
type CircleButtonProps = {
4+
onClick?: (ev: MouseEvent) => void;
5+
ariaLabel: string;
6+
children: Element | JSX.Element;
7+
className?: string;
8+
};
9+
10+
export const CircleButton = ({
11+
onClick,
12+
ariaLabel,
13+
children,
14+
className = "",
15+
}: CircleButtonProps) => (
16+
<button
17+
onClick={onClick}
18+
aria-label={ariaLabel}
19+
className={`rounded-full bg-bg-white p-xs ${className}`}
20+
>
21+
{children}
22+
</button>
23+
);
24+
25+
export default CircleButton;

src/components/CodeEmbed/frame.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ interface CodeBundle {
1515
* Wraps the given code in a html document for display.
1616
* Single object argument, all properties optional:
1717
*/
18-
const wrapInMarkup = (code: CodeBundle) => `<!DOCTYPE html>
18+
const wrapInMarkup = (code: CodeBundle) =>
19+
`<!DOCTYPE html>
1920
<meta charset="utf8" />
2021
<style type='text/css'>
2122
html, body {
@@ -30,7 +31,7 @@ ${code.css || ""}
3031
<body>${code.htmlBody || ""}</body>
3132
<script id="code" type="text/javascript">${code.js || ""}</script>
3233
<script src="${p5LibraryUrl}"></script>
33-
`.replace(/\u00A0/g, ' ');
34+
`.replace(/\u00A0/g, " ");
3435

3536
export interface CodeFrameProps {
3637
jsCode: string;

src/components/CodeEmbed/index.jsx

+56-29
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import { useState, useEffect } from "preact/hooks";
2-
import CodeMirror from "@uiw/react-codemirror";
2+
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
33
import { javascript } from "@codemirror/lang-javascript";
44

55
import { CodeFrame } from "./frame";
6+
import { CopyCodeButton } from "../CopyCodeButton";
7+
import CircleButton from "../CircleButton";
8+
import { Icon } from "../Icon";
69
/*
710
* A more featured code embed component that uses CodeMirror
811
*
912
* Props: {
1013
* initialValue?: string;
1114
* editable: boolean;
1215
* previewable: boolean;
16+
* previewHeight?: number;
17+
* previewWidth?: number;
1318
* }
1419
*/
1520
export const CodeEmbed = (props) => {
@@ -19,7 +24,10 @@ export const CodeEmbed = (props) => {
1924
// instead of a normal one, but these break the code frame, so we replace them here.
2025
// We also replace them in CodeFrame, but replacing here too ensures people don't
2126
// accidentally copy-and-paste them out of the embedded editor.
22-
const [codeString, setCodeString] = useState(initialCode.replace(/\u00A0/g, ' '));
27+
const [codeString, setCodeString] = useState(
28+
initialCode.replace(/\u00A0/g, " "),
29+
);
30+
2331
const [previewCodeString, setPreviewCodeString] = useState(codeString);
2432

2533
useEffect(() => {
@@ -29,53 +37,72 @@ export const CodeEmbed = (props) => {
2937
if (!rendered) return <div className="code-placeholder" />;
3038

3139
return (
32-
<div className="mb-md flex w-full flex-col overflow-hidden md:flex-row">
40+
<div className="mb-md flex w-full flex-col overflow-hidden lg:flex-row">
3341
{props.previewable ? (
34-
<div>
35-
<CodeFrame jsCode={previewCodeString} width={150} height={200} />
36-
{/* TODO: Actual button styles */}
37-
<button
38-
className="bg-bg-gray-40 rounded-full p-xs"
39-
onClick={() => {
40-
console.log("updating code");
41-
setPreviewCodeString(codeString);
42-
}}
43-
>
44-
Run
45-
</button>
46-
<button
47-
className="bg-bg-gray-40 rounded-full p-xs"
48-
onClick={() => {
49-
console.log("resetting code");
50-
setCodeString(initialCode);
51-
setPreviewCodeString(initialCode);
52-
}}
53-
>
54-
Reset
55-
</button>
42+
<div className="flex lg:flex-col">
43+
<CodeFrame
44+
jsCode={previewCodeString}
45+
width={props.previewWidth}
46+
height={props.previewHeight}
47+
/>
48+
{/* Looks more visually balanced with a slight leftward nudge */}
49+
<div className="gap-xs lg:flex">
50+
<CircleButton
51+
className="!bg-bg-gray-40 !p-sm lg:ml-[-2px]"
52+
onClick={() => {
53+
setPreviewCodeString(codeString);
54+
}}
55+
>
56+
<Icon kind="play" />
57+
</CircleButton>
58+
<CircleButton
59+
className="!bg-bg-gray-40 !p-sm"
60+
onClick={() => {
61+
setPreviewCodeString("");
62+
}}
63+
>
64+
<Icon kind="stop" />
65+
</CircleButton>
66+
</div>
5667
</div>
5768
) : null}
58-
<div className="w-full md:w-[calc(100%-150px)]">
69+
<div className="relative w-full md:w-[calc(100%-150px)]">
5970
<CodeMirror
6071
value={codeString}
6172
theme="light"
6273
width="100%"
6374
minimalSetup={{
6475
highlightSpecialChars: false,
65-
history: true,
76+
history: false,
6677
drawSelection: true,
6778
syntaxHighlighting: true,
6879
defaultKeymap: true,
6980
historyKeymap: true,
7081
}}
71-
indentWithTab={false}
72-
extensions={[javascript()]}
82+
basicSetup={{
83+
lineNumbers: false,
84+
foldGutter: false,
85+
autocompletion: false,
86+
}}
87+
extensions={[javascript(), EditorView.lineWrapping]}
7388
onChange={(val) => setCodeString(val)}
7489
editable={props.editable}
7590
onCreateEditor={(editorView) =>
7691
(editorView.contentDOM.ariaLabel = "Code Editor")
7792
}
7893
/>
94+
<div className="absolute right-0 top-0 flex gap-xs p-xs">
95+
<CopyCodeButton textToCopy={codeString || initialCode} />
96+
<CircleButton
97+
onClick={() => {
98+
setCodeString(initialCode);
99+
setPreviewCodeString(initialCode);
100+
}}
101+
ariaLabel="Reset code to initial value"
102+
>
103+
<Icon kind="refresh" />
104+
</CircleButton>
105+
</div>
79106
</div>
80107
</div>
81108
);

src/components/CopyCodeButton/index.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import CircleButton from "../CircleButton";
2+
13
interface CopyCodeButtonProps {
24
textToCopy: string;
35
}
@@ -10,10 +12,9 @@ export const CopyCodeButton = ({ textToCopy }: CopyCodeButtonProps) => {
1012
};
1113

1214
return (
13-
<button
15+
<CircleButton
1416
onClick={copyTextToClipboard}
15-
aria-label="Copy Code"
16-
className="rounded-full bg-bg-white p-xs"
17+
ariaLabel="Copy code to clipboard"
1718
>
1819
<svg
1920
width="18"
@@ -37,6 +38,6 @@ export const CopyCodeButton = ({ textToCopy }: CopyCodeButtonProps) => {
3738
fill="black"
3839
/>
3940
</svg>
40-
</button>
41+
</CircleButton>
4142
);
4243
};

src/components/Icon/index.tsx

+44-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
interface IconProps {
2-
kind: "arrow" | "code-brackets";
2+
kind: "arrow" | "code-brackets" | "refresh" | "play" | "stop";
33
className?: string;
44
}
55
/**
@@ -49,6 +49,49 @@ export const Icon = (props: IconProps) => {
4949
/>
5050
</svg>
5151
);
52+
case "refresh":
53+
return (
54+
<svg
55+
width="18"
56+
height="24"
57+
viewBox="0 0 18 24"
58+
fill="none"
59+
xmlns="http://www.w3.org/2000/svg"
60+
>
61+
<path
62+
d="M14.3597 0.745963L15.9691 6.75275L9.96235 8.36232C9.69562 8.43379 9.42145 8.2755 9.34998 8.00877C9.2785 7.74204 9.43679 7.46787 9.70353 7.39639L14.0814 6.22329C12.7385 5.02054 10.9658 4.28984 9.02153 4.28986C4.83114 4.28989 1.43412 7.68694 1.43408 11.8774C1.43408 12.1535 1.21022 12.3774 0.934077 12.3774C0.657935 12.3774 0.43408 12.1535 0.434082 11.8774C0.434122 7.13467 4.27885 3.2899 9.02154 3.28986C11.1249 3.28984 13.0522 4.04653 14.545 5.30172L13.3937 1.00477C13.3223 0.738039 13.4806 0.463872 13.7473 0.392404C14.014 0.320936 14.2882 0.47923 14.3597 0.745963Z"
63+
fill="black"
64+
/>
65+
<path
66+
d="M3.3684 18.4804L4.52046 22.7801C4.59193 23.0468 4.43363 23.321 4.1669 23.3924C3.90017 23.4639 3.626 23.3056 3.55453 23.0389L1.94508 17.0321L7.95183 15.4225C8.21856 15.3511 8.49274 15.5093 8.56421 15.7761C8.63568 16.0428 8.47739 16.317 8.21066 16.3885L3.83466 17.5611C5.17757 18.7638 6.95034 19.4945 8.8946 19.4945C13.085 19.4945 16.482 16.0974 16.4821 11.907C16.4821 11.6308 16.7059 11.407 16.9821 11.407C17.2582 11.407 17.4821 11.6308 17.4821 11.907C17.482 16.6497 13.6373 20.4945 8.8946 20.4945C6.79 20.4945 4.86161 19.7369 3.3684 18.4804Z"
67+
fill="black"
68+
/>
69+
</svg>
70+
);
71+
case "play":
72+
return (
73+
<svg
74+
width="16"
75+
height="16"
76+
viewBox="0 0 16 16"
77+
fill="none"
78+
xmlns="http://www.w3.org/2000/svg"
79+
>
80+
<path d="M15.5 8L0.5 15.5L0.499999 0.500001L15.5 8Z" fill="black" />
81+
</svg>
82+
);
83+
case "stop":
84+
return (
85+
<svg
86+
width="16"
87+
height="16"
88+
viewBox="0 0 16 16"
89+
fill="none"
90+
xmlns="http://www.w3.org/2000/svg"
91+
>
92+
<rect x="0.5" y="0.5" width="15" height="15" fill="black" />
93+
</svg>
94+
);
5295
default:
5396
return null;
5497
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import type { ReferenceDocContentItem } from "@/src/content/types";
2+
import { useMemo, useState } from "preact/hooks";
3+
import type { JSX } from "preact";
4+
5+
type ReferenceDirectoryEntry = ReferenceDocContentItem & {
6+
data: {
7+
path: string;
8+
title: string;
9+
description: string;
10+
};
11+
};
12+
13+
type FilteredCategoryData = {
14+
name: string;
15+
subcats: {
16+
name: string;
17+
entries: ReferenceDirectoryEntry[];
18+
}[];
19+
};
20+
21+
type ReferenceDirectoryWithFilterProps = {
22+
categoryData: {
23+
name: string;
24+
subcats: {
25+
name: string;
26+
entry?: ReferenceDirectoryEntry;
27+
entries: ReferenceDirectoryEntry[];
28+
}[];
29+
}[];
30+
};
31+
32+
export const ReferenceDirectoryWithFilter = ({
33+
categoryData,
34+
}: ReferenceDirectoryWithFilterProps) => {
35+
const [searchKeyword, setSearchKeyword] = useState("");
36+
37+
const filteredEntries = useMemo(() => {
38+
if (!searchKeyword) return categoryData;
39+
40+
return categoryData.reduce((acc: FilteredCategoryData[], category) => {
41+
const filteredSubcats = category.subcats.reduce(
42+
(subAcc, subcat) => {
43+
const filteredEntries = subcat.entries.filter((entry) =>
44+
entry.data.title.includes(searchKeyword),
45+
);
46+
if (filteredEntries.length > 0) {
47+
subAcc.push({ ...subcat, entries: filteredEntries });
48+
}
49+
return subAcc;
50+
},
51+
[] as typeof category.subcats,
52+
);
53+
54+
if (filteredSubcats.length > 0) {
55+
acc.push({ ...category, subcats: filteredSubcats });
56+
}
57+
return acc;
58+
}, []);
59+
}, [categoryData, searchKeyword]);
60+
61+
const renderEntries = (entries: ReferenceDirectoryEntry[]) => (
62+
<div class="content-grid">
63+
{entries.map((entry) => (
64+
<div class="col-span-3 w-full overflow-hidden" key={entry.id}>
65+
<a href={`/reference/${entry.data.path}`} class="text-body-mono">
66+
<span dangerouslySetInnerHTML={{ __html: entry.data.title }} />
67+
</a>
68+
<p>{`${entry.data.description.replace(/<[^>]*>/g, "").split(/\.(\s|$)/, 1)[0]}.`}</p>
69+
</div>
70+
))}
71+
</div>
72+
);
73+
74+
const renderCategoryData = () =>
75+
filteredEntries.map((category) => (
76+
<div class="my-md border-b border-type-color pb-2xl" key={category.name}>
77+
<h2>
78+
{category.name}
79+
<a id={category.name} />
80+
</h2>
81+
{category.subcats.map((subcat) => (
82+
<div key={subcat.name}>
83+
{subcat.name && (
84+
<div class="my-lg">
85+
<h3>
86+
{subcat.name}
87+
<a id={subcat.name} />
88+
</h3>
89+
</div>
90+
)}
91+
{renderEntries(subcat.entries)}
92+
</div>
93+
))}
94+
</div>
95+
));
96+
97+
return (
98+
<>
99+
<div class="bg-accent-color px-lg pb-lg">
100+
<div class="max-w-screen-md">
101+
<input
102+
type="text"
103+
id="search"
104+
class="text-body w-full border-b border-accent-type-color bg-transparent py-xs placeholder:text-accent-type-color"
105+
placeholder="Filter by keyword"
106+
onKeyUp={(e: JSX.TargetedKeyboardEvent<HTMLInputElement>) => {
107+
const target = e.target as HTMLInputElement;
108+
setSearchKeyword(target?.value);
109+
}}
110+
/>
111+
</div>
112+
</div>
113+
<div class="mx-lg">{renderCategoryData()}</div>
114+
</>
115+
);
116+
};

src/components/RootPageHeader/index.astro

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
const { title, subtitle } = Astro.props;
33
---
44

5-
<div class="bg-accent-color text-accent-type-color p-md pt-2xl">
5+
<div class="bg-accent-color text-accent-type-color p-md px-lg pt-2xl">
66
<h1 class="whitespace-pre-line">{title}</h1>
77
<h2>{subtitle}</h2>
88
</div>

0 commit comments

Comments
 (0)