Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
f7c81bf
cleanup from base-stack
abrulic Jul 14, 2025
6e8dfcf
sidebar initial working version
abrulic Jul 14, 2025
58084d7
functional table of content component
abrulic Jul 19, 2025
dfbeff7
small change in the knip.json
abrulic Jul 19, 2025
8dede72
custom script to validate content folder positions inside frontmatter…
abrulic Jul 20, 2025
8d06e3e
updated readme.md file
abrulic Jul 20, 2025
d0ab191
updated readme.md file
abrulic Jul 20, 2025
03dcf30
small refactoring and added documnetation for some components
abrulic Jul 20, 2025
6d23125
footer component
abrulic Jul 20, 2025
9fb1e8c
initial theme switcher, changes on UI, additional components
abrulic Jul 22, 2025
b8d8d87
fonts, some fixes
abrulic Jul 23, 2025
4b4bf90
small change
abrulic Jul 23, 2025
67d4cb7
convention xx-file-name.mdx update - still updates needed
abrulic Jul 24, 2025
0fd0ec2
refactoring
abrulic Jul 24, 2025
3029d09
small changes and updated package.json
abrulic Jul 25, 2025
d7286cc
ts fix?
abrulic Jul 25, 2025
c9d553c
ts fix?
abrulic Jul 25, 2025
2c04dc6
ts fix?
abrulic Jul 25, 2025
e70c006
ts fix?
abrulic Jul 25, 2025
06780d2
ts fix?
abrulic Jul 25, 2025
de6cfdd
sidebar fix
abrulic Jul 27, 2025
eb3f746
added content folder and some fixes and improvements in UI
abrulic Jul 28, 2025
0e29737
removed _index route, added index.mdx file for hopemage, reorganized …
abrulic Jul 29, 2025
d6b73ed
refactoring
abrulic Aug 1, 2025
892c14c
small update in update-frontmatter logic
abrulic Aug 1, 2025
7d2842e
small refactoring
abrulic Aug 3, 2025
5eaed66
small ui improvements
abrulic Aug 3, 2025
1d3e062
small ui improvements
abrulic Aug 3, 2025
82a39a9
refactoring
abrulic Aug 4, 2025
8bb2b5a
command palette component - initial version
abrulic Aug 4, 2025
a4e605c
refactoring
abrulic Aug 7, 2025
c960702
theme toggle fix
abrulic Aug 7, 2025
02369ea
refactoring ad vitest tests for some helper functions
abrulic Aug 8, 2025
6e2bfde
small refactoring
abrulic Aug 8, 2025
d17c3be
fix in breadcrumbs building
abrulic Aug 8, 2025
8f460c3
fix in breadcrumbs building
abrulic Aug 8, 2025
45158bb
unit tests and small improvements
abrulic Aug 10, 2025
9e5044c
comments and tests
abrulic Aug 10, 2025
55fcdcb
Merge branch 'initial-setup-and-layout' into command-palette
abrulic Aug 10, 2025
b7057d6
refactoring
abrulic Aug 11, 2025
6191bc9
initial changes
abrulic Aug 12, 2025
7d53709
small fixes
abrulic Aug 12, 2025
05f4738
fixed so it doesnt contains v1.0.1 now
abrulic Aug 13, 2025
5045de0
fixes
abrulic Aug 13, 2025
b9f2941
small update
abrulic Aug 13, 2025
ffc1fb7
small fixes
abrulic Aug 13, 2025
22ce1df
small change
abrulic Aug 13, 2025
936f956
Merge branch 'initial-setup-and-layout' into command-palette
abrulic Aug 13, 2025
35c41c6
small update
abrulic Aug 14, 2025
d472088
Merge branch 'initial-setup-and-layout' into generate-documentation
abrulic Aug 14, 2025
fe80fa0
dropdown versions and fixed build script
abrulic Aug 17, 2025
536f9c7
check-with no verify
abrulic Aug 17, 2025
45bab85
small fix in sidebar
abrulic Aug 19, 2025
fbbf8c2
working version
abrulic Aug 20, 2025
87b24cc
for now in url for documentation page it will be shown every verion, …
abrulic Aug 20, 2025
f395428
dropdown fix
abrulic Aug 20, 2025
fc1c8fb
load content collections fix
abrulic Aug 20, 2025
d967db8
sorting tags in versions.ts
abrulic Aug 20, 2025
679a95a
refactoring
abrulic Aug 20, 2025
f3a6b68
improvements
abrulic Aug 21, 2025
43ce350
Merge branch 'initial-setup-and-layout' into generate-documentation
abrulic Aug 21, 2025
53b94af
update
abrulic Aug 21, 2025
57f2ab2
Merge branch 'main' into generate-documentation
abrulic Aug 22, 2025
bca9ef6
updated docs.build.ts
abrulic Aug 22, 2025
737337d
updates with docs.build.ts
abrulic Aug 22, 2025
ae7c793
small update
abrulic Aug 22, 2025
2eac1d9
updates
abrulic Aug 26, 2025
6afc641
Merge branch 'initial-setup-and-layout' into command-palette
abrulic Aug 27, 2025
9bfaabb
updates
abrulic Aug 27, 2025
09954a2
changes
abrulic Aug 28, 2025
5b85ad1
updates
abrulic Sep 1, 2025
826779a
small changes
abrulic Sep 1, 2025
5d8bc22
Merge branch 'generate-documentation' into command-palette
abrulic Sep 1, 2025
1610336
small fix in breadcrumbs tests
abrulic Sep 1, 2025
98aa2e6
small update
abrulic Sep 1, 2025
9ab0f76
removed DEFAULT_BRANCH env from the yml and passed using cli args in …
abrulic Sep 1, 2025
2fc69cc
Merge branch 'generate-documentation' into command-palette
abrulic Sep 2, 2025
a43c129
small update
abrulic Sep 2, 2025
b8a065d
Merge branch 'main' into command-palette
abrulic Sep 4, 2025
41f73bc
reorganized and refactored command k
abrulic Sep 4, 2025
4f6555e
fix to work with multiple versions
abrulic Sep 4, 2025
da597cf
refactoring
abrulic Sep 4, 2025
6469713
fix with versions and search history
abrulic Sep 4, 2025
0f2c3ea
refactoring
abrulic Sep 4, 2025
40121ff
removed unused icons
abrulic Sep 4, 2025
d25a06b
small refactoring
abrulic Sep 4, 2025
df056ca
fixes to handle search on server side
abrulic Sep 10, 2025
ca9c0ff
refactoring
abrulic Sep 10, 2025
a5938f9
update so search Index is created on the app startup
abrulic Sep 10, 2025
8f2bdf7
small fix
abrulic Sep 10, 2025
86367c0
small fix
abrulic Sep 10, 2025
eee638c
small updates
abrulic Sep 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app/components/backdrop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { cn } from "~/utils/css"

export const Backdrop = ({ onClose }: { onClose: () => void }) => (
// biome-ignore lint/a11y/useKeyWithClickEvents: We don't need keyboard events for backdrop
<div
className={cn("fixed inset-0 bg-[var(--color-modal-backdrop)] backdrop-blur-sm transition-opacity duration-200")}
onClick={(e) => {
if (e.target === e.currentTarget) {
onClose()
}
}}
/>
)
135 changes: 135 additions & 0 deletions app/components/command-k/components/command-k.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router"
import { Modal } from "~/components/modal"
import type { Version } from "~/utils/version-resolvers"
import { useKeyboardNavigation } from "../hooks/use-keyboard-navigation"
import { useModalState } from "../hooks/use-modal-state"
import { useSearch } from "../hooks/use-search"
import { useSearchHistory } from "../hooks/use-search-history"
import type { HistoryItem, MatchType, SearchResult } from "../search-types"
import { EmptyState } from "./empty-state"
import { ResultsFooter } from "./results-footer"
import { SearchHistory } from "./search-history"
import { SearchInput } from "./search-input"
import { SearchResultRow } from "./search-result"
import { TriggerButton } from "./trigger-button"

interface CommandPaletteProps {
placeholder?: string
version: Version
}

export const CommandK = ({ placeholder, version }: CommandPaletteProps) => {
const { t } = useTranslation()
const navigate = useNavigate()
const inputRef = useRef<HTMLInputElement>(null)
const [query, setQuery] = useState("")
const { isOpen, openModal, closeModal } = useModalState()
const { history, addToHistory, clearHistory, removeFromHistory } = useSearchHistory(version)
const { results, search } = useSearch({ version })

const hasQuery = !!query.trim()
const hasResults = !!results.length
const hasHistory = !!history.length
const searchPlaceholder = placeholder ?? t("placeholders.search_documentation")

const handleClose = () => {
closeModal()
setQuery("")
search("")
}

const navigateToPage = (id: string) => {
const path = [version, id]
.filter(Boolean)
.map((s) => s.replace(/^\/+|\/+$/g, ""))
.join("/")

navigate(`/${path}`)
}

const handleResultSelect = (result: SearchResult) => {
if (!isOpen) return
const rowItem = result.item
const matchType: MatchType = result.refIndex === 0 ? "heading" : "paragraph"
const historyItem = {
...rowItem,
type: matchType,
highlightedText: result.highlightedText,
}

addToHistory(historyItem)
navigateToPage(rowItem.id)
handleClose()
}

const handleHistorySelect = (item: HistoryItem) => {
navigateToPage(item.id)
handleClose()
}

const handleToggle = () => {
isOpen ? handleClose() : openModal()
}

const { selectedIndex } = useKeyboardNavigation({
isOpen,
results,
onSelect: handleResultSelect,
onClose: handleClose,
onToggle: handleToggle,
})

if (!isOpen) {
return <TriggerButton onOpen={openModal} placeholder={searchPlaceholder} />
}

const renderBody = () => {
if (hasQuery) {
if (!hasResults) return <EmptyState query={query} />

return results.map((result, index) => (
<SearchResultRow
key={`${result.item.id}-${result.refIndex}`}
item={result.item}
highlightedText={result.highlightedText}
isSelected={index === selectedIndex}
onClick={() => handleResultSelect(result)}
matchType={result.refIndex === 0 ? "heading" : "paragraph"}
/>
))
}

if (hasHistory) {
return (
<SearchHistory
history={history}
onSelect={handleHistorySelect}
onRemove={removeFromHistory}
onClear={clearHistory}
/>
)
}

return <EmptyState />
}

return (
<Modal isOpen={isOpen} onClose={handleClose} getInitialFocus={() => inputRef.current} ariaLabel={searchPlaceholder}>
<SearchInput
ref={inputRef}
value={query}
onChange={(val) => {
setQuery(val)
search(val.trim())
}}
placeholder={searchPlaceholder}
/>
<div className="max-h-96 overflow-y-auto overscroll-contain" aria-label={searchPlaceholder}>
{renderBody()}
</div>
<ResultsFooter resultsCount={results.length} query={query} />
</Modal>
)
}
29 changes: 29 additions & 0 deletions app/components/command-k/components/empty-state.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useTranslation } from "react-i18next"
import { KeyboardHint } from "./keyboard-hint"
import { ResultsFooterNote } from "./results-footer-note"

export const EmptyState = ({ query }: { query?: string }) => {
const { t } = useTranslation()
if (query) {
return (
<div className="px-4 py-8 text-center">
<p className="font-medium text-[var(--color-empty-text)]">
{t("text.no_results_for")} "{query}"
</p>
<p className="mt-1 text-[var(--color-empty-text-muted)] text-sm">{t("text.adjust_search")}</p>
</div>
)
}

return (
<div className="space-y-6 px-4 py-8 text-center">
<p className="mb-4 font-medium text-[var(--color-empty-text)]">{t("text.start_typing_to_search")}</p>
<div className="flex items-center justify-center gap-6 text-[var(--color-empty-text-muted)] text-xs">
<KeyboardHint keys={["↑", "↓"]} label={t("controls.navigate")} />
<KeyboardHint keys="↵" label={t("controls.select")} />
<KeyboardHint keys="⇥" label={t("controls.cycle")} />
</div>
<ResultsFooterNote />
</div>
)
}
21 changes: 21 additions & 0 deletions app/components/command-k/components/keyboard-hint.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Kbd } from "~/ui/kbd"
import { cn } from "~/utils/css"

interface KeyboardHintProps {
keys: string | string[]
label: string
className?: string
}

export const KeyboardHint = ({ keys, label, className }: KeyboardHintProps) => {
const keyArray = Array.isArray(keys) ? keys : [keys]

return (
<div className={cn("flex items-center gap-1", className)}>
{keyArray.map((key) => (
<Kbd key={key}>{key}</Kbd>
))}
<span>{label}</span>
</div>
)
}
15 changes: 15 additions & 0 deletions app/components/command-k/components/results-footer-note.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useTranslation } from "react-i18next"

export const ResultsFooterNote = () => {
const { t } = useTranslation()
return (
<span className="text-[var(--color-footer-text)] text-xs opacity-70">
{t("p.search_by")}{" "}
<span className="font-semibold">
<a href="https://www.forge42.dev/" target="_blank" rel="noopener noreferrer">
Forge 42
</a>
</span>
</span>
)
}
28 changes: 28 additions & 0 deletions app/components/command-k/components/results-footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useTranslation } from "react-i18next"
import { cn } from "~/utils/css"
import { KeyboardHint } from "./keyboard-hint"
import { ResultsFooterNote } from "./results-footer-note"

export const ResultsFooter = ({
resultsCount,
query,
}: {
resultsCount: number
query: string
}) => {
const { t } = useTranslation()
if (!query || resultsCount === 0) return null

return (
<div className={cn("border-[var(--color-footer-border)] border-t bg-[var(--color-footer-bg)] px-4 py-3")}>
<div className="flex items-center justify-between text-xs">
<span className="font-medium text-[var(--color-footer-text)]">{t("text.result", { count: resultsCount })}</span>
<div className="flex items-center gap-4 text-[var(--color-footer-text)]">
<KeyboardHint keys={["↑", "↓"]} label={t("controls.navigate")} />
<KeyboardHint keys="↵" label={t("controls.select")} />
<ResultsFooterNote />
</div>
</div>
</div>
)
}
122 changes: 122 additions & 0 deletions app/components/command-k/components/search-history.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { useTranslation } from "react-i18next"
import { Icon } from "~/ui/icon/icon"
import { cn } from "~/utils/css"
import type { HistoryItem } from "../search-types"
import { SearchResultRow } from "./search-result"
interface SearchHistoryProps {
history: HistoryItem[]
onSelect: (item: HistoryItem) => void
onRemove: (id: string) => void
onClear: () => void
}

const SearchHistoryHeader = ({ onClear }: Pick<SearchHistoryProps, "onClear">) => {
const { t } = useTranslation()
return (
<div
className={cn(
"flex items-center justify-between border-[var(--color-history-header-border)] border-b",
"bg-[var(--color-history-header-bg)] px-4 py-3"
)}
>
<div className="flex items-center gap-2">
<Icon name="Clock" className="size-4 text-[var(--color-result-meta)]" />
<span className="font-medium text-[var(--color-history-header-text)] text-sm">{t("text.recent_searches")}</span>
</div>
<ClearHistoryButton onClear={onClear} />
</div>
)
}

const ClearHistoryButton = ({ onClear }: Pick<SearchHistoryProps, "onClear">) => {
const { t } = useTranslation()
return (
<button
type="button"
onClick={onClear}
className={cn(
"flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors",
"text-[var(--color-result-meta)] hover:bg-[var(--color-history-clear-hover-bg)] hover:text-[var(--color-history-clear-hover-text)]"
)}
title="Clear history"
aria-label="Clear search history"
>
<Icon name="Trash2" className="size-3" />
<span className="hidden sm:inline">{t("buttons.clear")}</span>
</button>
)
}

const RemoveItemButton = ({
onRemove,
id,
}: {
onRemove: Pick<SearchHistoryProps, "onRemove">["onRemove"]
id: string
}) => (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
onRemove(id)
}}
className={cn(
"-translate-y-1/2 absolute top-1/2 right-2 flex h-6 w-6 items-center justify-center rounded-full border opacity-0 transition-all duration-150 group-hover:opacity-100",
"border-[var(--color-history-remove-border)] bg-[var(--color-history-remove-bg)] text-[var(--color-history-remove-text)]",
"hover:border-[var(--color-history-remove-hover-border)] hover:text-[var(--color-history-remove-hover-text)]"
)}
title="Remove from history"
aria-label={"Remove from history"}
>
<Icon name="X" className="size-3" />
</button>
)

const HistoryItemRow = ({
item,
index,
onSelect,
onRemove,
}: {
item: HistoryItem
index: number
onSelect: Pick<SearchHistoryProps, "onSelect">["onSelect"]
onRemove: Pick<SearchHistoryProps, "onRemove">["onRemove"]
}) => (
<div key={`${item.id}-${index}`} className="group relative">
<SearchResultRow
item={item}
highlightedText={item.highlightedText ?? item.title}
isSelected={false}
onClick={() => onSelect(item)}
matchType={item.type ?? "heading"}
/>
<RemoveItemButton onRemove={onRemove} id={item.id} />
</div>
)

const HistoryItemsList = ({
history,
onSelect,
onRemove,
}: {
history: HistoryItem[]
onSelect: Pick<SearchHistoryProps, "onSelect">["onSelect"]
onRemove: Pick<SearchHistoryProps, "onRemove">["onRemove"]
}) => (
<div className="max-h-64 overflow-y-auto">
{history.map((item, index) => (
<HistoryItemRow key={`${item.id}-${index}`} item={item} index={index} onSelect={onSelect} onRemove={onRemove} />
))}
</div>
)

export const SearchHistory = ({ history, onSelect, onRemove, onClear }: SearchHistoryProps) => {
if (history.length === 0) return null
return (
<div>
<SearchHistoryHeader onClear={onClear} />
<HistoryItemsList history={history} onSelect={onSelect} onRemove={onRemove} />
</div>
)
}
Loading