Skip to content

Commit 5580d04

Browse files
committed
Make tab links an explicit action with custom markdown directive
Resolves #88
1 parent 5a44e0d commit 5580d04

File tree

10 files changed

+218
-46
lines changed

10 files changed

+218
-46
lines changed

components/interface/client/package-lock.json

Lines changed: 57 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/interface/client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"rehype-github-alerts": "^4.1.1",
2626
"rehype-mermaid": "^3.0.0",
2727
"rehype-raw": "^7.0.0",
28+
"remark-directive": "^4.0.0",
2829
"remark-gfm": "^4.0.1",
2930
"sass": "^1.89.2"
3031
},

components/interface/client/src/TabContext.jsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createContext, use, useCallback, useContext, useState } from "react";
1+
import { createContext, useCallback, useContext, useState } from "react";
22

33
const TabContext = createContext([]);
44

@@ -7,16 +7,17 @@ export function TabContextProvider({ children }) {
77
const [activeTab, setActiveTab] = useState(null);
88

99
const addTab = useCallback(
10-
(url) => {
11-
setTabs((prevTabs) => [...prevTabs, url]);
10+
(url, title) => {
11+
if (!title) title = url;
12+
setTabs((prevTabs) => [...prevTabs, { url, title }]);
1213
setActiveTab(url);
1314
},
1415
[setTabs, setActiveTab],
1516
);
1617

1718
const removeTab = useCallback(
1819
(url) => {
19-
setTabs((prevTabs) => prevTabs.filter((tab) => tab !== url));
20+
setTabs((prevTabs) => prevTabs.filter((tab) => tab.url !== url));
2021
setActiveTab((prevActiveTab) =>
2122
prevActiveTab === url ? null : prevActiveTab,
2223
);
@@ -25,10 +26,12 @@ export function TabContextProvider({ children }) {
2526
);
2627

2728
const displayLink = useCallback(
28-
(url) => {
29+
(url, title) => {
30+
if (!title) title = url;
31+
2932
setTabs((prevTabs) => {
30-
if (!prevTabs.includes(url)) {
31-
return [...prevTabs, url];
33+
if (!prevTabs.find((tab) => tab.url === url)) {
34+
return [...prevTabs, { url, title }];
3235
}
3336
return prevTabs;
3437
});

components/interface/client/src/components/ExternalContentPanel/ExternalContentPanel.jsx

Lines changed: 17 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,33 @@
11
import { IdePlaceholder } from "./IdePlaceholder";
22
import { useTabs } from "../../TabContext";
3-
import Nav from "react-bootstrap/Nav";
4-
import Button from "react-bootstrap/Button";
53
import "./ExternalContentPanel.scss";
6-
7-
function getTabTitle(url) {
8-
if (url === "http://localhost:8085") return "VS Code";
9-
return url;
10-
}
4+
import { ExternalTabs } from "./ExternalTabs";
5+
import { useRef } from "react";
116

127
export function ExternalContentPanel() {
138
const { tabs, setActiveTab, activeTab, addTab, removeTab } = useTabs();
9+
const iframeRef = useRef();
1410

1511
return (
1612
<div className="d-flex flex-fill flex-column">
1713
<div className="p-3 pt-2 pb-0 bg-secondary-subtle">
18-
<Nav
19-
variant="tabs"
20-
activeKey={activeTab}
21-
onSelect={(selectedKey) => setActiveTab(selectedKey)}
22-
id="external-content-tabs"
23-
>
24-
{tabs.map((tab) => (
25-
<Nav.Item key={tab} className="me-2 ms-2">
26-
<Nav.Link eventKey={tab} className="p-1 ps-3 pe-1">
27-
<span className="me-3">{getTabTitle(tab)}</span>
28-
29-
{!tab.endsWith("localhost:8085") && (
30-
<Button
31-
size="sm"
32-
variant="default"
33-
className="rounded-circle p-1 pt-0 pb-0"
34-
onClick={(e) => {
35-
e.stopPropagation();
36-
removeTab(tab);
37-
}}
38-
>
39-
&times;
40-
</Button>
41-
)}
42-
</Nav.Link>
43-
</Nav.Item>
44-
))}
45-
</Nav>
14+
<ExternalTabs
15+
activeTab={activeTab}
16+
setActiveTab={setActiveTab}
17+
tabs={tabs}
18+
onTabRemoval={removeTab}
19+
/>
4620
</div>
4721
{activeTab ? (
48-
<iframe style={{ flex: 1, border: "none" }} src={activeTab} />
22+
<iframe
23+
ref={iframeRef}
24+
style={{ flex: 1, border: "none" }}
25+
src={activeTab}
26+
/>
4927
) : (
50-
<IdePlaceholder onLaunch={() => addTab("http://localhost:8085")} />
28+
<IdePlaceholder
29+
onLaunch={() => addTab("http://localhost:8085", "Workspace")}
30+
/>
5131
)}
5232
</div>
5333
);

components/interface/client/src/components/ExternalContentPanel/ExternalContentPanel.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,10 @@
99
background-color: var(--bs-secondary);
1010
color: var(--bs-white);
1111
}
12+
13+
span {
14+
position: relative;
15+
top: 1px;
16+
}
1217
}
1318
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import Nav from "react-bootstrap/Nav";
2+
import Button from "react-bootstrap/Button";
3+
4+
export function ExternalTabs({
5+
activeTab,
6+
setActiveTab,
7+
tabs,
8+
onTabRemoval,
9+
onRefreshClick,
10+
}) {
11+
return (
12+
<Nav
13+
variant="tabs"
14+
activeKey={activeTab}
15+
onSelect={(selectedKey) => setActiveTab(selectedKey)}
16+
id="external-content-tabs"
17+
>
18+
{tabs.map((tab) => (
19+
<Nav.Item key={tab.url} className="me-2 ms-2">
20+
<Nav.Link
21+
eventKey={tab.url}
22+
href={tab.url}
23+
className="p-1 ps-3 pe-1"
24+
onClick={(e) => {
25+
e.preventDefault();
26+
}}
27+
>
28+
<span className="me-3">{tab.title}</span>
29+
30+
{!tab.title !== "Workspace" && (
31+
<Button
32+
size="sm"
33+
variant="default"
34+
className="rounded-circle p-1 pt-0 pb-0"
35+
onClick={(e) => {
36+
e.stopPropagation();
37+
e.preventDefault();
38+
onTabRemoval(tab.url);
39+
}}
40+
>
41+
<span className="material-symbols-outlined">close</span>
42+
</Button>
43+
)}
44+
</Nav.Link>
45+
</Nav.Item>
46+
))}
47+
</Nav>
48+
);
49+
}

components/interface/client/src/components/WorkshopPanel/markdown/MarkdownRenderer.jsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,32 @@ import { MarkdownHooks } from "react-markdown";
22
import remarkGfm from "remark-gfm";
33
import rehypeRaw from "rehype-raw";
44
import rehypeMermaid from "rehype-mermaid";
5+
import remarkDirective from "remark-directive";
56
import { rehypeGithubAlerts } from "rehype-github-alerts";
67
import { CodeBlock } from "./CodeBlock";
78
import { remarkCodeIndexer } from "./codeIndexer";
89
import { ExternalLink } from "./ExternalLink";
910
import { RenderedImage } from "./RenderedImage";
1011
import { RenderedSvg } from "./RenderedSvg";
12+
import { tabDirective } from "./reactDirective";
13+
import { TabLink } from "./TabLink";
1114

1215
export function MarkdownRenderer({ children }) {
1316
return (
1417
<MarkdownHooks
15-
remarkPlugins={[remarkGfm, remarkCodeIndexer]}
18+
remarkPlugins={[
19+
remarkGfm,
20+
remarkCodeIndexer,
21+
remarkDirective,
22+
tabDirective,
23+
]}
1624
rehypePlugins={[rehypeRaw, rehypeMermaid, rehypeGithubAlerts]}
1725
components={{
1826
code: CodeBlock,
1927
a: ExternalLink,
2028
img: RenderedImage,
2129
svg: RenderedSvg,
30+
tablink: TabLink,
2231
}}
2332
>
2433
{children}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { useTabs } from "../../../TabContext";
2+
3+
export function TabLink({ href, title, children }) {
4+
const { displayLink } = useTabs();
5+
6+
return (
7+
<a
8+
href={href}
9+
title={title}
10+
onClick={(e) => {
11+
e.preventDefault();
12+
displayLink(href, title);
13+
}}
14+
>
15+
{children}
16+
</a>
17+
);
18+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// import {h} from 'hastscript'
2+
import { visit } from "unist-util-visit";
3+
4+
/**
5+
* Works with the remark-directive plugin to transform custom directives into
6+
* components that can be used by react-markdown.
7+
*
8+
* To support a custom directive, simply define the React component and add it
9+
* to the components prop of MarkdownRenderer. The name of the component must
10+
* match the name of the directive.
11+
*
12+
* @returns
13+
*/
14+
export function tabDirective() {
15+
/**
16+
* @param {Root} tree
17+
* Tree.
18+
* @returns {undefined}
19+
* Nothing.
20+
*/
21+
return (tree) => {
22+
visit(
23+
tree,
24+
["textDirective", "leafDirective", "containerDirective"],
25+
(node) => {
26+
const data = node.data || (node.data = {});
27+
28+
// This is what's supposed to work. But "h" was not a function?
29+
// const { properties } = h(node.name, node.attributes);
30+
31+
data.hName = node.name;
32+
data.hProperties = {
33+
...node.attributes,
34+
};
35+
},
36+
);
37+
};
38+
}

docs/markdown-options.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,16 @@ To add a "Save file" button, use the `save-as=path/to/file.txt` metadata.
7474
If a user clicks this "Save file" button, a `compose.yaml` file will be created with the following content.
7575

7676
- If the file already exists, the contents will be replaced with what is provided in the code block.
77-
- All required directories leading up to the file will automatically be created
77+
- All required directories leading up to the file will automatically be created
78+
79+
80+
81+
## Links
82+
83+
By default, all links are configured to open new browser tabs when clicked.
84+
85+
If you want to add another tab to the right-hand panel, you can use the following directive:
86+
87+
::tabLink[Link text]{href="http://localhost:3000" title="Tab title"}
88+
89+
This will render a link with the visible text of "Link text" pointing to "http://localhost:3000". When clicked, a new tab will be created with the title of "Tab title".

0 commit comments

Comments
 (0)