Back
diff --git a/app/(docs)/layout.tsx b/app/(docs)/layout.tsx
index c59435578..530830b84 100644
--- a/app/(docs)/layout.tsx
+++ b/app/(docs)/layout.tsx
@@ -7,7 +7,7 @@ import { Body, NavChildren, SidebarBanner } from "./layout.client";
import { Statuspage } from "statuspage.io";
const statuspage = new Statuspage("3111l89394q4");
-console.log({ status: await statuspage.api.getStatus() });
+// console.log({ status: await statuspage.api.getStatus() });
export const layoutOptions: Omit
= {
tree: utils.pageTree,
@@ -21,10 +21,7 @@ export const layoutOptions: Omit = {
href: "https://platform.hiro.so/",
icon: (
-
- Hiro Platform
-
-
+
Hiro Platform
),
external: true,
@@ -36,10 +33,10 @@ export const layoutOptions: Omit = {
text: "Guides",
url: "/guides",
},
- // {
- // text: "Cookbook",
- // url: "/cookbook",
- // },
+ {
+ text: "Cookbook",
+ url: "/cookbook",
+ },
],
sidebar: {
defaultOpenLevel: 0,
@@ -59,10 +56,7 @@ export const homeLayoutOptions: Omit = {
href: "https://platform.hiro.so/",
icon: (
-
- Hiro Platform
-
-
+
Hiro Platform
),
external: true,
@@ -74,10 +68,10 @@ export const homeLayoutOptions: Omit = {
text: "Guides",
url: "/guides",
},
- // {
- // text: "Cookbook",
- // url: "/cookbook",
- // },
+ {
+ text: "Cookbook",
+ url: "/cookbook",
+ },
],
};
diff --git a/app/cookbook/[id]/page.tsx b/app/cookbook/[id]/page.tsx
new file mode 100644
index 000000000..b19bd4616
--- /dev/null
+++ b/app/cookbook/[id]/page.tsx
@@ -0,0 +1,109 @@
+import { Code } from "@/components/docskit/code";
+import { loadRecipes } from "@/utils/loader";
+import { Badge } from "@/components/ui/badge";
+import { HoverProvider } from "@/context/hover";
+import { HoverLink } from "@/components/docskit/annotations/hover";
+import { Terminal } from "@/components/docskit/terminal";
+import { InlineCode } from "@/components/docskit/inline-code";
+import { WithNotes } from "@/components/docskit/notes";
+import { SnippetResult } from "../components/snippet-result";
+import Link from "next/link";
+import { RecipeCarousel } from "@/components/recipe-carousel";
+import { MoveLeft } from "lucide-react";
+
+interface Param {
+ id: string;
+}
+
+export const dynamicParams = false;
+
+export default async function Page({
+ params,
+}: {
+ params: Param;
+}): Promise {
+ const { id } = params;
+ const recipes = await loadRecipes();
+ const recipe = recipes.find((r) => r.id === id);
+
+ if (!recipe) {
+ return Recipe not found
;
+ }
+
+ // Dynamically import MDX content based on recipe id
+ const Content = await import(`@/content/_recipes/guides/${id}.mdx`).catch(
+ () => {
+ console.error(`Failed to load MDX content for recipe: ${id}`);
+ return { default: () => Content not found
};
+ }
+ );
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {recipe.categories.map((category) => (
+
+ {category}
+
+ ))}
+
+
+
+
+
+
+
+ {/* Sticky sidebar */}
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/app/cookbook/components/code-result.tsx b/app/cookbook/components/code-result.tsx
new file mode 100644
index 000000000..38805699d
--- /dev/null
+++ b/app/cookbook/components/code-result.tsx
@@ -0,0 +1,19 @@
+import { Code } from "@/components/docskit/code";
+
+interface CodeResultProps {
+ result: string;
+}
+
+export function CodeResult({ result }: CodeResultProps) {
+ return (
+
+ );
+}
diff --git a/app/cookbook/components/cookbook-ui.tsx b/app/cookbook/components/cookbook-ui.tsx
new file mode 100644
index 000000000..d268d0f01
--- /dev/null
+++ b/app/cookbook/components/cookbook-ui.tsx
@@ -0,0 +1,327 @@
+"use client";
+
+import { useState, useMemo, Suspense, useEffect, useRef } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { Recipe } from "@/types/recipes";
+import { cn } from "@/lib/utils";
+import { Input } from "@/components/ui/input";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { LayoutGrid, List, Search } from "lucide-react";
+import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
+import { FilterPopover } from "@/components/filter-popover";
+
+// Internal components
+function ViewToggle({
+ view,
+ onChange,
+}: {
+ view: "grid" | "list";
+ onChange: (view: "grid" | "list") => void;
+}) {
+ return (
+
+
+
+
+ );
+}
+
+function RecipeFilters({
+ search,
+ onSearchChange,
+ selectedCategories,
+ onCategoriesChange,
+}: {
+ search: string;
+ onSearchChange: (value: string) => void;
+ selectedCategories: string[];
+ onCategoriesChange: (categories: string[]) => void;
+}) {
+ return (
+
+
+
+ onSearchChange(e.target.value)}
+ />
+
+
+
+ );
+}
+
+interface CookbookProps {
+ initialRecipes: Recipe[];
+ recipeCards: React.ReactNode[];
+}
+
+function CookbookContent({ initialRecipes, recipeCards }: CookbookProps) {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const ITEMS_PER_PAGE = 10;
+ const [currentPage, _] = useState(1);
+
+ const [view, setView] = useState<"grid" | "list">(() => {
+ return (searchParams.get("view") as "grid" | "list") || "list";
+ });
+ const [search, setSearch] = useState("");
+ const [selectedCategories, setSelectedCategories] = useState(() => {
+ const categories = searchParams.get("categories");
+ return categories ? categories.split(",") : [];
+ });
+
+ const updateURL = (newView?: "grid" | "list", newCategories?: string[]) => {
+ const params = new URLSearchParams();
+
+ if (newView === "list") {
+ params.set("view", newView);
+ }
+
+ if (newCategories && newCategories.length > 0) {
+ params.set("categories", newCategories.join(","));
+ }
+
+ const newURL = params.toString()
+ ? `?${params.toString()}`
+ : window.location.pathname;
+
+ router.push(newURL, { scroll: false });
+ };
+
+ const handleViewChange = (newView: "grid" | "list") => {
+ setView(newView);
+ updateURL(newView, selectedCategories);
+ };
+
+ const handleCategoriesChange = (categories: string[]) => {
+ setSelectedCategories(categories);
+ updateURL(view, categories);
+ };
+
+ const handleSearchChange = (value: string) => {
+ setSearch(value);
+ };
+
+ // Create a map of recipe IDs to their corresponding rendered cards
+ const recipeCardMap = useMemo(() => {
+ return initialRecipes.reduce(
+ (map, recipe, index) => {
+ map[recipe.id] = recipeCards[index];
+ return map;
+ },
+ {} as Record
+ );
+ }, [initialRecipes, recipeCards]);
+
+ const filteredRecipeCards = useMemo(() => {
+ // First sort by date
+ const sortedRecipes = [...initialRecipes].sort((a, b) => {
+ return new Date(b.date).getTime() - new Date(a.date).getTime();
+ });
+
+ // Then apply filters
+ const filteredRecipes = sortedRecipes.filter((recipe) => {
+ const searchText = search.toLowerCase();
+ const matchesSearch =
+ recipe.title.toLowerCase().includes(searchText) ||
+ recipe.description.toLowerCase().includes(searchText) ||
+ recipe.categories.some((category) =>
+ category.toLowerCase().includes(searchText)
+ ) ||
+ recipe.tags.some((tag) => tag.toLowerCase().includes(searchText));
+
+ const matchesCategories =
+ selectedCategories.length === 0 ||
+ recipe.categories.some((category) =>
+ selectedCategories.includes(category.toLowerCase())
+ );
+
+ return matchesSearch && matchesCategories;
+ });
+
+ const startIndex = 0;
+ const endIndex = currentPage * ITEMS_PER_PAGE;
+
+ return filteredRecipes
+ .slice(startIndex, endIndex)
+ .map((recipe) => recipeCardMap[recipe.id]);
+ }, [search, selectedCategories, initialRecipes, recipeCardMap, currentPage]);
+
+ // Add total pages calculation
+ const totalPages = useMemo(() => {
+ const filteredLength = initialRecipes.filter((recipe) => {
+ const searchText = search.toLowerCase();
+ const matchesSearch =
+ recipe.title.toLowerCase().includes(searchText) ||
+ recipe.description.toLowerCase().includes(searchText) ||
+ recipe.categories.some((category) =>
+ category.toLowerCase().includes(searchText)
+ ) ||
+ recipe.tags.some((tag) => tag.toLowerCase().includes(searchText));
+
+ const matchesCategories =
+ selectedCategories.length === 0 ||
+ recipe.categories.some((category) =>
+ selectedCategories.includes(category.toLowerCase())
+ );
+
+ return matchesSearch && matchesCategories;
+ }).length;
+
+ return Math.ceil(filteredLength / ITEMS_PER_PAGE);
+ }, [initialRecipes, search, selectedCategories]);
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ const lastItemRef = useRef(null);
+
+ useEffect(() => {
+ const observer = new IntersectionObserver(
+ (entries) => {
+ const lastEntry = entries[0];
+ if (lastEntry.isIntersecting && !isLoading) {
+ // Check if we have more pages to load
+ if (currentPage < totalPages) {
+ setIsLoading(true);
+ }
+ }
+ },
+ { threshold: 0.1 }
+ );
+
+ const currentRef = lastItemRef.current;
+ if (currentRef) {
+ observer.observe(currentRef);
+ }
+
+ return () => {
+ if (currentRef) {
+ observer.unobserve(currentRef);
+ }
+ };
+ }, [currentPage, totalPages, isLoading]);
+
+ return (
+
+
+
+
+
Cookbook
+
+ Explore ready-to-use code recipes for building applications on
+ Stacks.
+
+
+
+
+
+
+
+
+ {view === "list" ? (
+
+
+ {filteredRecipeCards.map((recipeCard, index) => {
+ const recipe = initialRecipes[index];
+ const isLastItem = index === filteredRecipeCards.length - 1;
+
+ return (
+ router.push(`/cookbook/${recipe.id}`)}
+ ref={isLastItem ? lastItemRef : null}
+ >
+
+
+ {recipe.title}
+
+
+
+
+ {recipe.categories.map((category) => (
+
+ {category.toUpperCase()}
+
+ ))}
+
+
+
+ );
+ })}
+
+
+ ) : (
+
+ {filteredRecipeCards.map((card, index) => (
+
+ {card}
+
+ ))}
+
+ )}
+
+
+
+ {isLoading && (
+
+ Loading more recipes...
+
+ )}
+
+ );
+}
+
+export function CookbookUI({ initialRecipes, recipeCards }: CookbookProps) {
+ return (
+ Loading... }>
+
+
+ );
+}
diff --git a/app/cookbook/components/snippet-result.tsx b/app/cookbook/components/snippet-result.tsx
new file mode 100644
index 000000000..717d8d53a
--- /dev/null
+++ b/app/cookbook/components/snippet-result.tsx
@@ -0,0 +1,297 @@
+"use client";
+
+import React from "react";
+import Link from "next/link";
+import { Play, Terminal } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { ArrowUpRight } from "lucide-react";
+import { Code } from "@/components/docskit/code";
+import { initSimnet, type Simnet } from "@hirosystems/clarinet-sdk-browser";
+import { Cl } from "@stacks/transactions";
+import { loadSandpackClient } from "@codesandbox/sandpack-client";
+import type { SandboxSetup } from "@codesandbox/sandpack-client";
+
+import type { Recipe } from "@/types/recipes";
+
+interface SnippetResultProps {
+ recipe: Recipe;
+ code: string;
+ type: string;
+ dependencies: {
+ [key: string]: string;
+ };
+}
+
+type OutputItem = {
+ type: "command" | "success" | "error" | "log";
+ content: string;
+};
+
+export function SnippetResult({
+ recipe,
+ code,
+ type,
+ dependencies,
+}: SnippetResultProps) {
+ const [result, setResult] = React.useState
(null);
+ const [isLoading, setIsLoading] = React.useState(false);
+ const [isConsoleOpen, setIsConsoleOpen] = React.useState(false);
+ const [input, setInput] = React.useState("");
+ const [output, setOutput] = React.useState([]);
+ const [simnetInstance, setSimnetInstance] = React.useState(
+ null
+ );
+ const [codeHistory, setCodeHistory] = React.useState("");
+ // Add these new states near your other state declarations
+ const [commandHistory, setCommandHistory] = React.useState([]);
+ const [historyIndex, setHistoryIndex] = React.useState(-1);
+
+ const inputRef = React.useRef(null);
+ const outputRef = React.useRef(null);
+ const iframeRef = React.useRef(null);
+
+ React.useEffect(() => {
+ if (outputRef.current) {
+ outputRef.current.scrollTop = outputRef.current.scrollHeight;
+ }
+ }, [output]);
+
+ React.useEffect(() => {
+ if (isConsoleOpen && inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, [isConsoleOpen]);
+
+ async function runCode() {
+ if (type === "clarity") {
+ if (isConsoleOpen) {
+ setIsConsoleOpen(false);
+ return;
+ }
+ setIsLoading(true);
+ setResult(null);
+
+ try {
+ const simnet = await initSimnet();
+ await simnet.initEmptySession();
+ simnet.deployer = "ST000000000000000000002AMW42H";
+ const deployer = simnet.deployer;
+ console.log("deployer", deployer);
+ simnet.setEpoch("3.0");
+
+ // Store the initialized simnet instance
+ setSimnetInstance(simnet);
+ // Store the initial code in history
+ setCodeHistory(code);
+
+ const contract = simnet.deployContract(
+ recipe.files[0].name.split(".")[0],
+ code,
+ { clarityVersion: 3 },
+ deployer
+ );
+ const result = contract.result;
+ const prettyResult = Cl.prettyPrint(result, 2);
+ // console.log("before :", simnet.execute("stacks-block-height"));
+ // simnet.executeCommand("::advance_chain_tip 2");
+ // console.log("after: ", simnet.execute("stacks-block-height"));
+
+ // Add a 1-second delay before updating the result
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ setResult(prettyResult);
+ setIsConsoleOpen(true);
+ } catch (error) {
+ console.error("Error running code snippet:", error);
+ setResult("An error occurred while running the code snippet.");
+ } finally {
+ setIsLoading(false);
+ }
+ } else {
+ const content = {
+ files: {
+ "/package.json": {
+ code: JSON.stringify({
+ main: "index.js",
+ dependencies: dependencies || {},
+ }),
+ },
+ "/index.js": {
+ code: code, // This is the content from your recipe file
+ },
+ },
+ environment: "vanilla",
+ };
+
+ const client = await loadSandpackClient(iframeRef.current!, content);
+ console.log(client);
+ }
+ }
+
+ // Add this function to handle keyboard events
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "ArrowUp") {
+ e.preventDefault();
+ if (commandHistory.length > 0) {
+ const newIndex = historyIndex + 1;
+ if (newIndex < commandHistory.length) {
+ setHistoryIndex(newIndex);
+ setInput(commandHistory[commandHistory.length - 1 - newIndex]);
+ }
+ }
+ } else if (e.key === "ArrowDown") {
+ e.preventDefault();
+ if (historyIndex > 0) {
+ const newIndex = historyIndex - 1;
+ setHistoryIndex(newIndex);
+ setInput(commandHistory[commandHistory.length - 1 - newIndex]);
+ } else if (historyIndex === 0) {
+ setHistoryIndex(-1);
+ setInput("");
+ }
+ }
+ };
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ if (input.trim()) {
+ // Add command to history
+ setCommandHistory((prev) => [...prev, input.trim()]);
+ setHistoryIndex(-1); // Reset history index
+ setOutput([...output, { type: "command", content: input }]);
+ try {
+ if (!simnetInstance) {
+ throw new Error("Please run the code snippet first");
+ }
+
+ // Check if input is a command (starts with "::")
+ if (input.startsWith("::")) {
+ const commandResult = simnetInstance.executeCommand(input);
+
+ setOutput((prev) => [
+ ...prev,
+ {
+ type: "success",
+ content: commandResult,
+ },
+ ]);
+ } else {
+ // Regular Clarity code execution
+ const fullCode = `${codeHistory}\n${input}`;
+ setCodeHistory(fullCode);
+ const codeExecution = simnetInstance.execute(fullCode);
+ const result = codeExecution.result;
+ const prettyResult = Cl.prettyPrint(result, 2);
+
+ setOutput((prev) => [
+ ...prev,
+ { type: "success", content: prettyResult },
+ ]);
+ }
+ } catch (error) {
+ setOutput((prev) => [
+ ...prev,
+ { type: "error", content: String(error) },
+ ]);
+ }
+
+ setInput("");
+ }
+ }
+
+ const getButtonText = () => {
+ if (type === "clarity") {
+ if (isLoading) return "Loading...";
+ if (isConsoleOpen) return "Close terminal";
+ return "Open in terminal";
+ }
+ return "Run code snippet";
+ };
+
+ return (
+
+
+
+ {/*
*/}
+ {type === "clarity" && (
+
+ )}
+
+ {result && type !== "clarity" && (
+
+ )}
+ {result && isConsoleOpen && (
+
+
+ {output.map((item, index) => (
+
+ {item.type === "command" ? (
+
+ $
+ {item.content}
+
+ ) : (
+
{item.content}
+ )}
+
+ ))}
+
+
+
+ )}
+
+ );
+}
diff --git a/app/cookbook/layout.tsx b/app/cookbook/layout.tsx
new file mode 100644
index 000000000..51be6614b
--- /dev/null
+++ b/app/cookbook/layout.tsx
@@ -0,0 +1,19 @@
+import { Layout } from "fumadocs-ui/layout";
+import type { ReactNode } from "react";
+import { homeLayoutOptions } from "../(docs)/layout";
+
+export default function CookbookLayout({
+ children,
+}: {
+ children: ReactNode;
+}): JSX.Element {
+ return (
+
+ );
+}
diff --git a/app/cookbook/page.tsx b/app/cookbook/page.tsx
new file mode 100644
index 000000000..da53a11f6
--- /dev/null
+++ b/app/cookbook/page.tsx
@@ -0,0 +1,73 @@
+import { loadRecipes } from "@/utils/loader";
+import { CookbookUI } from "./components/cookbook-ui";
+import { Code } from "@/components/docskit/code";
+import { Recipe } from "@/types/recipes";
+import Link from "next/link";
+import { Badge } from "@/components/ui/badge";
+import { CopyButton } from "@/components/docskit/copy-button";
+
+function RecipeCard({
+ recipe,
+ codeElement,
+}: {
+ recipe: Recipe;
+ codeElement: React.ReactNode;
+}) {
+ return (
+
+
+
+
+
+ {recipe.title}
+
+ {/*
+ {recipe.description}
+
*/}
+
+
+
+
+ {recipe.categories.map((category) => (
+
+ {category.toUpperCase()}
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
+
+export default async function Page() {
+ // Pre-render the recipe cards with Code components on the server
+ const recipes = await loadRecipes();
+ const recipeCards = await Promise.all(
+ recipes.map(async (recipe) => {
+ const codeElement = await Code({
+ codeblocks: [
+ {
+ lang: recipe.files[0].type,
+ value: recipe.files[0].content,
+ meta: "",
+ },
+ ],
+ });
+
+ return (
+
+ );
+ })
+ );
+ return ;
+}
diff --git a/app/global.css b/app/global.css
index c1d73e335..c53b87456 100644
--- a/app/global.css
+++ b/app/global.css
@@ -22,7 +22,7 @@
--destructive-foreground: 0 0% 98%;
--border: 32 13.8% 78.6%;
--input: 240 5.9% 90%;
- --ring: 346.8 77.2% 49.8%;
+ --ring: 240 5.9% 90%;
--radius: 0.5rem;
--hiro: 21 100% 67.5%;
--icon: #383432;
@@ -67,7 +67,7 @@
--destructive-foreground: 0 85.7% 97.3%;
--border: 17 6.5% 21%;
--input: 240 3.7% 15.9%;
- --ring: 346.8 77.2% 49.8%;
+ --ring: 17 6.5% 21%;
--hiro: 24 100% 51.4%;
--card-hover: 15 5% 16%;
--icon: #ffffff;
@@ -115,6 +115,30 @@ body {
/* Override CSS */
+.recipe-preview > div:first-child {
+ margin: 0;
+ height: 200px;
+}
+
+.recipe-preview > div:first-child pre {
+ overflow-x: hidden;
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* IE and Edge */
+}
+
+.recipe-preview > div:first-child pre::-webkit-scrollbar {
+ display: none; /* Chrome, Safari and Opera */
+}
+
+.recipe > div:first-child {
+ margin: 0;
+}
+
+.recipe > div:first-child pre {
+ max-height: 425px;
+ height: auto;
+}
+
.sticky.top-24 {
font-family: var(--font-aeonikFono), sans-serif;
background: transparent;
@@ -537,3 +561,24 @@ div.divide-y.divide-border.overflow-hidden.rounded-lg.border.bg-card {
}
/* END Docskit theme */
+
+/* Replace the nested selectors with flat ones */
+.light::selection {
+ background-color: #f4d4a3;
+ color: #bc812e;
+}
+
+.light::-moz-selection {
+ background-color: #f4d4a3;
+ color: #bc812e;
+}
+
+.dark::selection {
+ background-color: #ffe6f2;
+ color: #ff9ecf;
+}
+
+.dark::-moz-selection {
+ background-color: #ffe6f2;
+ color: #ff9ecf;
+}
diff --git a/components/code/clarinet-sdk.tsx b/components/code/clarinet-sdk.tsx
index 9e847f0bd..21abc0f81 100644
--- a/components/code/clarinet-sdk.tsx
+++ b/components/code/clarinet-sdk.tsx
@@ -3,107 +3,30 @@
import React from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
-import * as Base from "fumadocs-ui/components/codeblock";
+import { Code } from "../docskit/code";
import { initSimnet } from "@hirosystems/clarinet-sdk-browser";
import { Cl } from "@stacks/transactions";
-import { getHighlighter } from "shiki";
-
-// TODO: WIP: testing out new Clarinet JS SDK browser lib
export const ClarinetSDK: React.FC = () => {
- const [simnet, setSimnet] = React.useState();
- const [html, setHtml] = React.useState();
- const [evaluatedResponse, setEvaluatedResponse] = React.useState();
-
- async function showMe() {
- const simnet = await initSimnet();
- await simnet.initEmtpySession();
-
- simnet.setEpoch("2.5");
- const result =
- simnet.runSnippet(`(define-map Users uint {address: principal})
- (map-insert Users u1 { address: tx-sender })
- (map-get? Users u1)
- `) as any;
-
- const highlighter = await getHighlighter({
- langs: ["bash", "ts", "tsx", "clarity"],
- themes: ["github-light", "github-dark"],
- });
- const res = highlighter.codeToHtml(Cl.prettyPrint(result, 2), {
- lang: "clarity",
- defaultColor: false,
- themes: {
- light: "github-light",
- dark: "github-dark",
- },
- transformers: [
- {
- name: "remove-pre",
- root: (root) => {
- if (root.children[0].type !== "element") return;
+ const [evaluatedResponse, setEvaluatedResponse] = React.useState();
- return {
- type: "root",
- children: root.children[0].children,
- };
- },
- },
- ],
- });
- setEvaluatedResponse(res);
- }
+ // Clarity code to be executed
+ const clarityCode = `(define-map Users uint {address: principal})
+(map-insert Users u1 { address: tx-sender })
+(map-get? Users u1)`;
- async function run() {
+ async function runCode() {
const simnet = await initSimnet();
- await simnet.initEmtpySession();
-
+ await simnet.initEmptySession();
simnet.setEpoch("2.5");
- const result =
- simnet.runSnippet(`(define-map Users uint {address: principal})
- (map-insert Users u1 { address: tx-sender })
- (map-get? Users u1)
- `) as any;
- console.log(Cl.prettyPrint(result, 2));
- setSimnet(simnet);
-
- const codeResponse = await fetch("/scripts/hello-world.clar");
- const code = await codeResponse.text();
-
- const highlighter = await getHighlighter({
- langs: ["bash", "ts", "tsx", "clarity"],
- themes: ["github-light", "github-dark"],
- });
-
- const html = highlighter.codeToHtml(code, {
- lang: "clarity",
- defaultColor: false,
- themes: {
- light: "github-light",
- dark: "github-dark",
- },
- transformers: [
- {
- name: "remove-pre",
- root: (root) => {
- if (root.children[0].type !== "element") return;
- return {
- type: "root",
- children: root.children[0].children,
- };
- },
- },
- ],
- });
- setHtml(html);
+ const result = simnet.runSnippet(clarityCode) as any;
+ const deserializedResult = Cl.deserialize(result);
+ console.log(deserializedResult);
+ setEvaluatedResponse(Cl.prettyPrint(deserializedResult, 2));
}
- React.useEffect(() => {
- run();
- }, []);
-
return (
<>
-
-
-
- {evaluatedResponse ? (
-
-
-
- ) : null}
+
+
+
+ {evaluatedResponse && (
+
+ )}
>
);
};
diff --git a/components/docskit/annotations/hover-line.client.tsx b/components/docskit/annotations/hover-line.client.tsx
new file mode 100644
index 000000000..c3555d8e4
--- /dev/null
+++ b/components/docskit/annotations/hover-line.client.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+import { InnerLine } from "codehike/code";
+import { CustomLineProps } from "codehike/code/types";
+import { useHover } from "@/context/hover";
+
+export function HoverLineClient({ annotation, ...props }: CustomLineProps) {
+ const lineRef = useRef(null);
+ try {
+ const { hoveredId } = useHover();
+ const isHovered = !hoveredId || annotation?.query === hoveredId;
+
+ useEffect(() => {
+ // Add scrollable effect to the line when hovered
+ if (
+ hoveredId &&
+ annotation?.query &&
+ annotation.query === hoveredId &&
+ lineRef.current
+ ) {
+ const recipeContainer = lineRef.current.closest(".recipe");
+
+ if (recipeContainer) {
+ // Find the first scrollable child
+ const scrollableContainers = [
+ ...recipeContainer.querySelectorAll("*"),
+ ].filter((el) => {
+ const style = window.getComputedStyle(el);
+ return (
+ style.overflow === "auto" ||
+ style.overflow === "scroll" ||
+ style.overflowY === "auto" ||
+ style.overflowY === "scroll"
+ );
+ });
+
+ const codeContainer = scrollableContainers[0];
+
+ if (codeContainer) {
+ const offset = codeContainer.clientHeight / 3;
+ const lineRect = lineRef.current.getBoundingClientRect();
+ const containerRect = codeContainer.getBoundingClientRect();
+
+ // Calculate relative position considering current scroll
+ const relativeTop =
+ lineRect.top - containerRect.top + codeContainer.scrollTop;
+
+ console.log({
+ offset,
+ lineTop: lineRect.top,
+ containerTop: containerRect.top,
+ relativeTop,
+ currentScroll: codeContainer.scrollTop,
+ containerHeight: codeContainer.clientHeight,
+ });
+
+ codeContainer.scrollTo({
+ top: relativeTop - offset,
+ behavior: "smooth",
+ });
+ }
+ }
+ }
+ }, [hoveredId, annotation?.query]);
+
+ return (
+
+
+
+ );
+ } catch (error) {
+ console.warn("Hover context not ready:", error);
+ return ;
+ }
+}
diff --git a/components/docskit/annotations/hover.client.tsx b/components/docskit/annotations/hover.client.tsx
new file mode 100644
index 000000000..6a0465764
--- /dev/null
+++ b/components/docskit/annotations/hover.client.tsx
@@ -0,0 +1,22 @@
+"use client";
+
+import { useHover } from "@/context/hover";
+
+export function HoverLinkClient(props: {
+ href?: string;
+ children?: React.ReactNode;
+ className?: string;
+}) {
+ const { setHoveredId } = useHover();
+ const hoverId = props.href?.slice("hover:".length);
+
+ return (
+ setHoveredId(hoverId ?? null)}
+ onMouseLeave={() => setHoveredId(null)}
+ >
+ {props.children}
+
+ );
+}
diff --git a/components/docskit/annotations/hover.tsx b/components/docskit/annotations/hover.tsx
new file mode 100644
index 000000000..d62c99e29
--- /dev/null
+++ b/components/docskit/annotations/hover.tsx
@@ -0,0 +1,14 @@
+import { AnnotationHandler, InnerLine } from "codehike/code";
+import { HoverLinkClient } from "./hover.client";
+import { HoverLineClient } from "./hover-line.client";
+
+export const hover: AnnotationHandler = {
+ name: "hover",
+ onlyIfAnnotated: true,
+ Line: ({ annotation, ...props }) => {
+ // This needs to be a client component to access context
+ return ;
+ },
+};
+
+export { HoverLinkClient as HoverLink };
diff --git a/components/docskit/code-icon.tsx b/components/docskit/code-icon.tsx
index 297d0dc4a..d5086d9a8 100644
--- a/components/docskit/code-icon.tsx
+++ b/components/docskit/code-icon.tsx
@@ -1,4 +1,5 @@
import { themeIcons } from "seti-icons";
+import { Clarity } from "@/components/ui/icon";
export function CodeIcon({
title,
@@ -9,6 +10,18 @@ export function CodeIcon({
lang: string;
className?: string;
}) {
+ if (lang === "clarity") {
+ return (
+
+
+
+ );
+ }
+
let filename = title || "x";
if (!filename.includes(".")) {
filename += "." + lang;
diff --git a/components/docskit/code.tsx b/components/docskit/code.tsx
index 582379bc4..c94aac591 100644
--- a/components/docskit/code.tsx
+++ b/components/docskit/code.tsx
@@ -15,6 +15,7 @@ import { link } from "./annotations/link";
import { tokenTransitions } from "./annotations/token-transitions";
import { tooltip } from "./annotations/tooltip";
import { callout } from "./annotations/callout";
+import { hover } from "./annotations/hover";
import { CODEBLOCK, CodeGroup, flagsToOptions, TITLEBAR } from "./code-group";
export async function Code(props: {
@@ -121,6 +122,7 @@ function getHandlers(options: CodeGroup["options"]) {
...collapse,
options.wordWrap && wordWrap,
callout,
+ hover,
].filter(Boolean) as AnnotationHandler[];
}
diff --git a/components/docskit/copy-button.tsx b/components/docskit/copy-button.tsx
index 70533f9bd..77d8925fc 100644
--- a/components/docskit/copy-button.tsx
+++ b/components/docskit/copy-button.tsx
@@ -16,7 +16,8 @@ export function CopyButton({
return (