Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/systems-witch/llms.txt

Use this file to discover all available pages before exploring further.

The Articles page turns a writing index into a filesystem listing. Every article is a row in a ls -lh-style table with Unix permission strings, human-readable file sizes, short dates, and .md filenames. A $ grep input above the table filters rows in real time as the user types, completing the illusion that they are searching a live directory. The entire layout is font-mono and lives inside the standard page space-y-12 wrapper.

Data structure

Articles are defined in a const articles array above the component:
const articles = [
  { name: "architecting-the-void.md",    size: "14K",  date: "May 20", perms: "-rw-r--r--" },
  { name: "css-as-incantation.md",        size: "8.2K", date: "May 15", perms: "-rw-r--r--" },
  { name: "state-management-rituals.md",  size: "21K",  date: "Apr 30", perms: "-rw-r--r--" },
  { name: "the-death-of-clean-code.md",   size: "12K",  date: "Apr 12", perms: "-rw-r--r--" },
  { name: "webgl-sigils-tutorial.md",     size: "45K",  date: "Mar 28", perms: "-rw-r--r--" }
];
FilenameSizeDate
architecting-the-void.md14KMay 20
css-as-incantation.md8.2KMay 15
state-management-rituals.md21KApr 30
the-death-of-clean-code.md12KApr 12
webgl-sigils-tutorial.md45KMar 28
All five default entries use -rw-r--r-- permissions (owner read/write, group and other read-only). perms is a plain display string — no logic derives it from the other fields.

Grep filter

The search input is styled as a terminal command prefix:
<div className="flex items-center gap-2 text-acid">
  <span>$ grep</span>
  <input
    type="text"
    value={query}
    onChange={e => setQuery(e.target.value)}
    placeholder="pattern..."
    className="bg-transparent border-b border-graphite focus:border-acid outline-none
               text-bone px-2 py-1 w-64 placeholder:text-graphite text-sm"
  />
</div>
query is a useState("") string. The filtered list is computed at render time:
const filtered = articles.filter(a =>
  a.name.includes(query.toLowerCase())
);
The filter is case-insensitive (.toLowerCase()) and matches anywhere in the filename. The placeholder "pattern..." continues the Unix grep metaphor.
The filter only searches name — not size, date, or perms. If you want multi-field search, expand the filter predicate to check multiple properties.

Table layout

The <table> uses w-full text-left whitespace-nowrap inside an overflow-x-auto wrapper for mobile scrolling. Column headers are text-lilac font-normal (not bold) with border-b border-graphite/50 separator:
<thead>
  <tr className="text-lilac border-b border-graphite/50">
    <th className="font-normal pb-2 px-4">PERMS</th>
    <th className="font-normal pb-2 px-4">SIZE</th>
    <th className="font-normal pb-2 px-4">DATE</th>
    <th className="font-normal pb-2 px-4">NAME</th>
  </tr>
</thead>

Row styling

Each <tr> is hover:bg-graphite/30 group cursor-pointer transition-colors. On hover, the row background lifts to graphite/30 and column-specific styles activate via group:
ColumnDefaultOn row-hover
PERMStext-graphitetext-lilac
SIZEtext-lilac(unchanged)
DATEtext-lilac(unchanged)
NAMEtext-bonetext-acid + underline (decoration-acid/50, underline-offset-4)
The NAME column treatment makes filenames behave like hyperlinks on hover without being actual <a> tags — the cursor-pointer on the row handles the affordance signal.

Empty state

When the filtered array is empty, a full-width cell renders a “no results” message:
{filtered.length === 0 && (
  <tr>
    <td colSpan={4} className="py-4 px-4 text-graphite text-center">
      No matching files found.
    </td>
  </tr>
)}
The text-graphite colour renders the message almost invisible against the black background — a deliberate design choice that mimics the subdued “no results” state of a real terminal.

Customization

Add an article: Append an object to the articles array:
{ name: "your-new-post.md", size: "9.5K", date: "Jun 10", perms: "-rw-r--r--" }
There is no router integration in the default build — rows are not linked anywhere. To make rows navigate to actual article pages, wrap the filename cell in a React Router <Link to={/articles/$}> and add a corresponding route. Sort by date: The articles render in array order. Re-arrange the array manually, or add a .sort() call before the .filter():
const filtered = [...articles]
  .sort((a, b) => new Date(b.date) - new Date(a.date))
  .filter(a => a.name.includes(query.toLowerCase()));
The date values in the default data ("May 20", "Apr 30", etc.) omit the year. If you want reliable date sorting, switch to ISO 8601 strings like "2026-05-20" in the data array and display them with a toLocaleDateString() formatter in the <td>.
Change the filter field: To search by date or size instead of (or in addition to) name, update the filter predicate:
const filtered = articles.filter(a =>
  a.name.includes(query.toLowerCase()) ||
  a.date.toLowerCase().includes(query.toLowerCase())
);

Build docs developers (and LLMs) love