Skip to content

Commit 4e54b4d

Browse files
authored
feat(instructions): add anchors (#148)
1 parent 7f2fa09 commit 4e54b4d

File tree

8 files changed

+267
-44
lines changed

8 files changed

+267
-44
lines changed

src/pages/InstructionsPage/InstructionsPage.module.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,22 @@
191191
flex-wrap: wrap;
192192
}
193193

194+
.anchorIndicator {
195+
display: flex;
196+
align-items: center;
197+
gap: var(--spacing-md);
198+
padding: var(--spacing-xs) var(--spacing-sm);
199+
background-color: var(--color-background-primary);
200+
border-radius: var(--border-radius-sm);
201+
font-size: var(--font-size-xsm);
202+
color: var(--color-text-secondary);
203+
margin-top: var(--spacing-sm);
204+
}
205+
206+
.anchorIndicator span {
207+
font-weight: var(--font-weight-medium);
208+
}
209+
194210
@media (width <= 768px) {
195211
.subCategoryAndDocsContainer {
196212
flex-direction: column;

src/pages/InstructionsPage/InstructionsPage.tsx

Lines changed: 91 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ function appendFiftInstructions(
5959
function InstructionsPage() {
6060
const [spec, setSpec] = useState<TvmSpec | null>(null)
6161
const [expandedRows, setExpandedRows] = useState<Record<string, boolean>>({})
62+
const [anchorInstruction, setAnchorInstruction] = useState<string | null>(null)
6263

6364
const stored = loadStoredSettings()
6465

@@ -76,10 +77,33 @@ function InstructionsPage() {
7677
setSpec(tvmSpecData as unknown as TvmSpec)
7778
}, [])
7879

80+
useEffect(() => {
81+
const handleHashChange = () => {
82+
const hash = window.location.hash.slice(1) // Remove the '#'
83+
if (hash && hash !== anchorInstruction) {
84+
setAnchorInstruction(hash)
85+
setExpandedRows(prev => ({
86+
...prev,
87+
[hash]: true,
88+
}))
89+
}
90+
}
91+
92+
handleHashChange()
93+
window.addEventListener("hashchange", handleHashChange)
94+
return () => window.removeEventListener("hashchange", handleHashChange)
95+
}, [anchorInstruction])
96+
7997
const toggleColumn = (key: InstructionColumnKey) => {
8098
setSearchColumns(prev => (prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key]))
8199
}
82100

101+
const resetAnchor = () => {
102+
setAnchorInstruction(null)
103+
window.history.replaceState(null, "", window.location.pathname)
104+
setExpandedRows({})
105+
}
106+
83107
const instructions = useMemo(() => {
84108
return spec?.instructions ?? ({} as TvmSpec["instructions"])
85109
}, [spec?.instructions])
@@ -162,6 +186,22 @@ function InstructionsPage() {
162186

163187
const filteredInstructions = useMemo(() => {
164188
const q = query.trim().toLowerCase()
189+
190+
if (anchorInstruction) {
191+
const allInstructions = spec?.instructions ?? {}
192+
if (allInstructions[anchorInstruction]) {
193+
return {[anchorInstruction]: allInstructions[anchorInstruction]}
194+
} else {
195+
// Clear anchor if the instruction is not found
196+
setTimeout(() => {
197+
setAnchorInstruction(null)
198+
window.history.replaceState(null, "", window.location.pathname)
199+
setExpandedRows({})
200+
}, 0)
201+
return filteredByCategory
202+
}
203+
}
204+
165205
if (!q) return filteredByCategory
166206

167207
const entries = Object.entries(filteredByCategory)
@@ -183,7 +223,7 @@ function InstructionsPage() {
183223
if (match) next[name] = instruction
184224
}
185225
return next
186-
}, [query, filteredByCategory, searchColumns])
226+
}, [query, filteredByCategory, searchColumns, anchorInstruction, spec?.instructions])
187227

188228
const sortedInstructions = useMemo(() => {
189229
const entries = Object.entries(filteredInstructions)
@@ -252,6 +292,9 @@ function InstructionsPage() {
252292
}
253293

254294
const handleRowClick = (instructionName: string) => {
295+
// don't allow collapsing the instruction in anchor mode
296+
if (anchorInstruction) return
297+
255298
setExpandedRows(prev => ({
256299
...prev,
257300
[instructionName]: !prev[instructionName],
@@ -266,45 +309,57 @@ function InstructionsPage() {
266309

267310
<main className={styles.appContainer} role="main" aria-label="TVM Instructions">
268311
<div className={styles.mainContent}>
269-
<div className={styles.toolbar} role="search" aria-label="Toolbar">
270-
<SortSelector value={sortMode} onChange={setSortMode} />
271-
272-
<div className={styles.searchToolbar}>
273-
<SearchColumnsSelector value={searchColumns} onToggle={toggleColumn} />
274-
<div className={styles.searchInputContainer}>
275-
<SearchInput
276-
value={query}
277-
onChange={setQuery}
278-
onSubmit={() => {}}
279-
placeholder="Search instructions"
280-
compact={true}
281-
buttonLabel="Search"
282-
autoFocus={true}
283-
/>
312+
{!anchorInstruction && (
313+
<>
314+
<div className={styles.toolbar} role="search" aria-label="Toolbar">
315+
<SortSelector value={sortMode} onChange={setSortMode} />
316+
317+
<div className={styles.searchToolbar}>
318+
<SearchColumnsSelector value={searchColumns} onToggle={toggleColumn} />
319+
<div className={styles.searchInputContainer}>
320+
<SearchInput
321+
value={query}
322+
onChange={setQuery}
323+
onSubmit={() => {}}
324+
placeholder="Search instructions"
325+
compact={true}
326+
buttonLabel="Search"
327+
autoFocus={true}
328+
/>
329+
</div>
330+
</div>
284331
</div>
285-
</div>
286-
</div>
287-
<div>
288-
<CategoryTabs
289-
categories={categories}
290-
selected={selectedCategory}
291-
onSelect={cat => {
292-
setSelectedCategory(cat)
293-
setSelectedSubCategory("All")
294-
}}
295-
/>
296-
{subCategories.length > 0 && (
297-
<div className={styles.subCategoryAndDocsContainer}>
332+
<div>
298333
<CategoryTabs
299-
categories={subCategories}
300-
selected={selectedSubCategory}
301-
onSelect={setSelectedSubCategory}
302-
label="Subcategory:"
334+
categories={categories}
335+
selected={selectedCategory}
336+
onSelect={cat => {
337+
setSelectedCategory(cat)
338+
setSelectedSubCategory("All")
339+
}}
303340
/>
304-
{selectedCategory === "continuation" && <ContinuationsDocsBanner />}
341+
{subCategories.length > 0 && (
342+
<div className={styles.subCategoryAndDocsContainer}>
343+
<CategoryTabs
344+
categories={subCategories}
345+
selected={selectedSubCategory}
346+
onSelect={setSelectedSubCategory}
347+
label="Subcategory:"
348+
/>
349+
{selectedCategory === "continuation" && <ContinuationsDocsBanner />}
350+
</div>
351+
)}
305352
</div>
306-
)}
307-
</div>
353+
</>
354+
)}
355+
{anchorInstruction && (
356+
<div className={styles.anchorIndicator}>
357+
<span>Showing: {anchorInstruction}</span>
358+
<Button variant="outline" size="sm" onClick={resetAnchor}>
359+
Back to table
360+
</Button>
361+
</div>
362+
)}
308363
<InstructionTable
309364
instructions={sortedInstructions}
310365
expandedRows={expandedRows}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
.anchorButton {
2+
display: inline-flex;
3+
align-items: center;
4+
justify-content: center;
5+
margin-left: var(--spacing-xs);
6+
padding: var(--spacing-xs);
7+
border: none;
8+
background-color: transparent;
9+
cursor: pointer;
10+
opacity: 0;
11+
border-radius: var(--border-radius-sm);
12+
color: var(--color-text-secondary);
13+
transition:
14+
opacity 0.3s ease,
15+
background-color 0.15s ease;
16+
}
17+
18+
.anchorButton:hover {
19+
opacity: 1;
20+
color: var(--color-text-primary);
21+
}
22+
23+
.anchorButton svg {
24+
width: var(--font-size-xsm);
25+
height: var(--font-size-xsm);
26+
display: block;
27+
stroke: currentcolor;
28+
}
29+
30+
.anchorButton.copied {
31+
opacity: 1;
32+
background-color: transparent;
33+
}
34+
35+
.anchorButton.copied svg {
36+
stroke: currentcolor;
37+
animation: popIn 0.2s ease-out;
38+
}
39+
40+
@keyframes popIn {
41+
0% {
42+
transform: scale(0.7);
43+
opacity: 0.5;
44+
}
45+
46+
100% {
47+
transform: scale(1);
48+
opacity: 1;
49+
}
50+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React, {useCallback, useEffect, useState} from "react"
2+
3+
import Icon from "@shared/ui/Icon"
4+
5+
import styles from "./AnchorButton.module.css"
6+
7+
export const AnchorButton: React.FC<{
8+
readonly value: string
9+
readonly title: string
10+
readonly className?: string
11+
}> = ({value, title, className}) => {
12+
const [isCopied, setIsCopied] = useState(false)
13+
14+
const handleCopy = useCallback(() => {
15+
const url = `${window.location.origin}${window.location.pathname}#${value}`
16+
17+
navigator.clipboard
18+
.writeText(url)
19+
.then(() => {
20+
setIsCopied(true)
21+
})
22+
.catch(err => {
23+
console.error("Failed to copy URL:", err)
24+
})
25+
}, [value])
26+
27+
useEffect(() => {
28+
if (isCopied) {
29+
const timer = setTimeout(() => {
30+
setIsCopied(false)
31+
}, 1500)
32+
return () => clearTimeout(timer)
33+
}
34+
}, [isCopied])
35+
36+
const linkIconSvg = (
37+
<svg
38+
xmlns="http://www.w3.org/2000/svg"
39+
viewBox="0 0 24 24"
40+
fill="none"
41+
stroke="currentColor"
42+
strokeWidth="2"
43+
strokeLinecap="round"
44+
strokeLinejoin="round"
45+
aria-hidden="true"
46+
>
47+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
48+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
49+
</svg>
50+
)
51+
52+
const checkIconSvg = (
53+
<svg
54+
xmlns="http://www.w3.org/2000/svg"
55+
viewBox="0 0 24 24"
56+
fill="none"
57+
stroke="currentColor"
58+
strokeWidth="2"
59+
strokeLinecap="round"
60+
strokeLinejoin="round"
61+
aria-hidden="true"
62+
>
63+
<polyline points="20 6 9 17 4 12"></polyline>
64+
</svg>
65+
)
66+
67+
return (
68+
<>
69+
<button
70+
onClick={e => {
71+
e.stopPropagation()
72+
handleCopy()
73+
}}
74+
className={`${styles.anchorButton} ${isCopied ? styles.copied : ""} ${className ?? ""}`}
75+
title={isCopied ? "Copied!" : title}
76+
aria-label={isCopied ? "Copied to clipboard" : `Copy ${title.toLowerCase()}`}
77+
disabled={isCopied}
78+
type="button"
79+
>
80+
<Icon svg={isCopied ? checkIconSvg : linkIconSvg} />
81+
</button>
82+
83+
<div className="sr-only" aria-live="polite" aria-atomic="true">
84+
{isCopied && "Copied to clipboard"}
85+
</div>
86+
</>
87+
)
88+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {AnchorButton} from "./AnchorButton.tsx"

src/pages/InstructionsPage/components/InstructionTable/InstructionTable.module.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@
5555
background-color: var(--color-background-secondary);
5656
}
5757

58+
.divTr.tableRow:hover :global(.anchorButton) {
59+
opacity: 0.65;
60+
}
61+
5862
.divTr.tableRow:last-child:hover .divTd:first-child {
5963
border-radius: 0 0 0 var(--border-radius-md);
6064
}

0 commit comments

Comments
 (0)