OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      4e4b8a4aaefe
DATE      2026-07-17
SUBJECT   web: tabs, breadcrumbs, and filed-path search
FILES     13 CHANGED
HASH      4e4b8a4aaefe
DATE      2026-07-17
SUBJECT   web: tabs, breadcrumbs, and filed-path
          search
FILES     13 CHANGED
 

diff --git a/ooknet-design/src/components/ArticleMeta/ArticleMeta.astro b/ooknet-d
esign/src/components/ArticleMeta/ArticleMeta.astro
index f5dcb9e..ac3c926 100644
--- a/ooknet-design/src/components/ArticleMeta/ArticleMeta.astro
+++ b/ooknet-design/src/components/ArticleMeta/ArticleMeta.astro
@@ -1,6 +1,6 @@
 ---
 import Frame from "../Frame/Frame.astro";
 import { buildMeta, type Props } from "./ArticleMeta";
-const { fm, path } = Astro.props as Props;
+const { fm } = Astro.props as Props;
 ---
-<Frame build={(w) => buildMeta(w, fm, path)} />
+<Frame build={(w) => buildMeta(w, fm)} />

diff --git a/ooknet-design/src/components/Ar
ticleMeta/ArticleMeta.astro b/ooknet-design/
src/components/ArticleMeta/ArticleMeta.astro
index f5dcb9e..ac3c926 100644
--- a/ooknet-design/src/components/ArticleMe
ta/ArticleMeta.astro
+++ b/ooknet-design/src/components/ArticleMe
ta/ArticleMeta.astro
@@ -1,6 +1,6 @@
 ---
 import Frame from "../Frame/Frame.astro";
 import { buildMeta, type Props } from "./Ar
ticleMeta";
-const { fm, path } = Astro.props as Props;
+const { fm } = Astro.props as Props;
 ---
-<Frame build={(w) => buildMeta(w, fm, path)
} />
+<Frame build={(w) => buildMeta(w, fm)} />
 

diff --git a/ooknet-design/src/components/ArticleMeta/ArticleMeta.ts b/ooknet-desi
gn/src/components/ArticleMeta/ArticleMeta.ts
index 8f60dc9..acfeef8 100644
--- a/ooknet-design/src/components/ArticleMeta/ArticleMeta.ts
+++ b/ooknet-design/src/components/ArticleMeta/ArticleMeta.ts
@@ -4,28 +4,14 @@ import type { NoteFrontmatter } from "../../lib/types";
 export interface Props {
   width?: number;
   fm: NoteFrontmatter;
-  path: string;
 }
 
-// The path can exceed the narrow frame; char-wrap it under the label.
-function filedUnder(path: string, width: number): string[] {
-  const label = "[FILED UNDER]: ";
-  const avail = Math.max(1, width - len(label));
-  const cps = [...`/${path}`];
-  const out = [label + cps.slice(0, avail).join("")];
-  for (let i = avail; i < cps.length; i += avail) {
-    out.push(" ".repeat(len(label)) + cps.slice(i, i + avail).join(""));
-  }
-  return out;
-}
-
-export function buildMeta(width: number, fm: NoteFrontmatter, path: string): stri
ng {
+export function buildMeta(width: number, fm: NoteFrontmatter): string {
   // Below ~60 cells the JSON-style META block can't fit (the tags array
   // alone is ~40 chars), so narrow frames drop it.
   const compact = width < 60;
 
   const lines: string[] = [
-    ...filedUnder(path, width),
     rule(width),
     "",
     kv("AUTHOR",    fm.authors.join(", ") || "UNATTRIBUTED", 12, width),

diff --git a/ooknet-design/src/components/Ar
ticleMeta/ArticleMeta.ts b/ooknet-design/src
/components/ArticleMeta/ArticleMeta.ts
index 8f60dc9..acfeef8 100644
--- a/ooknet-design/src/components/ArticleMe
ta/ArticleMeta.ts
+++ b/ooknet-design/src/components/ArticleMe
ta/ArticleMeta.ts
@@ -4,28 +4,14 @@ import type { NoteFrontmat
ter } from "../../lib/types";
 export interface Props {
   width?: number;
   fm: NoteFrontmatter;
-  path: string;
 }
 
-// The path can exceed the narrow frame; ch
ar-wrap it under the label.
-function filedUnder(path: string, width: nu
mber): string[] {
-  const label = "[FILED UNDER]: ";
-  const avail = Math.max(1, width - len(lab
el));
-  const cps = [...`/${path}`];
-  const out = [label + cps.slice(0, avail).
join("")];
-  for (let i = avail; i < cps.length; i += 
avail) {
-    out.push(" ".repeat(len(label)) + cps.s
lice(i, i + avail).join(""));
-  }
-  return out;
-}
-
-export function buildMeta(width: number, fm
: NoteFrontmatter, path: string): string {
+export function buildMeta(width: number, fm
: NoteFrontmatter): string {
   // Below ~60 cells the JSON-style META bl
ock can't fit (the tags array
   // alone is ~40 chars), so narrow frames 
drop it.
   const compact = width < 60;
 
   const lines: string[] = [
-    ...filedUnder(path, width),
     rule(width),
     "",
     kv("AUTHOR",    fm.authors.join(", ") |
| "UNATTRIBUTED", 12, width),
 

diff --git a/ooknet-design/src/components/Breadcrumbs/Breadcrumbs.astro b/ooknet-d
esign/src/components/Breadcrumbs/Breadcrumbs.astro
new file mode 100644
index 0000000..e79bc5d
--- /dev/null
+++ b/ooknet-design/src/components/Breadcrumbs/Breadcrumbs.astro
@@ -0,0 +1,27 @@
+---
+import Pre from "../Pre/Pre.astro";
+import { len } from "../../lib/ascii";
+import { FRAME_W, MOBILE_FRAME_W } from "../../lib/config";
+import { CRUMB_LABEL, crumbs, layoutCrumbs, type Crumb, type Props } from "./Brea
dcrumbs";
+import "./Breadcrumbs.scss";
+
+const { filed } = Astro.props as Props;
+const items = crumbs(filed);
+const indent = " ".repeat(len(CRUMB_LABEL));
+
+const variants: [string, Crumb[][]][] = [
+  ["frame-wide", layoutCrumbs(items, FRAME_W)],
+  ["frame-narrow", layoutCrumbs(items, MOBILE_FRAME_W)],
+];
+---
+{items.length > 0 && (
+  <nav class="crumbs" aria-label="Filed under">
+    {variants.map(([variant, rows]) =>
+      rows.map((row, i) => (
+        <Pre class={variant}>{i === 0 ? CRUMB_LABEL : indent}{row.map((c) => (
+          <Fragment>/<a class="crumb-link" href={c.href}>{c.label}</a></Fragment>
+        ))}</Pre>
+      ))
+    )}
+  </nav>
+)}

diff --git a/ooknet-design/src/components/Br
eadcrumbs/Breadcrumbs.astro b/ooknet-design/
src/components/Breadcrumbs/Breadcrumbs.astro
new file mode 100644
index 0000000..e79bc5d
--- /dev/null
+++ b/ooknet-design/src/components/Breadcrum
bs/Breadcrumbs.astro
@@ -0,0 +1,27 @@
+---
+import Pre from "../Pre/Pre.astro";
+import { len } from "../../lib/ascii";
+import { FRAME_W, MOBILE_FRAME_W } from "..
/../lib/config";
+import { CRUMB_LABEL, crumbs, layoutCrumbs,
 type Crumb, type Props } from "./Breadcrumb
s";
+import "./Breadcrumbs.scss";
+
+const { filed } = Astro.props as Props;
+const items = crumbs(filed);
+const indent = " ".repeat(len(CRUMB_LABEL))
;
+
+const variants: [string, Crumb[][]][] = [
+  ["frame-wide", layoutCrumbs(items, FRAME_
W)],
+  ["frame-narrow", layoutCrumbs(items, MOBI
LE_FRAME_W)],
+];
+---
+{items.length > 0 && (
+  <nav class="crumbs" aria-label="Filed und
er">
+    {variants.map(([variant, rows]) =>
+      rows.map((row, i) => (
+        <Pre class={variant}>{i === 0 ? CRU
MB_LABEL : indent}{row.map((c) => (
+          <Fragment>/<a class="crumb-link" 
href={c.href}>{c.label}</a></Fragment>
+        ))}</Pre>
+      ))
+    )}
+  </nav>
+)}
 

diff --git a/ooknet-design/src/components/Breadcrumbs/Breadcrumbs.scss b/ooknet-de
sign/src/components/Breadcrumbs/Breadcrumbs.scss
new file mode 100644
index 0000000..b76c1eb
--- /dev/null
+++ b/ooknet-design/src/components/Breadcrumbs/Breadcrumbs.scss
@@ -0,0 +1,18 @@
+.crumbs {
+  display: block;
+}
+
+.crumb-link {
+  color: var(--ink);
+  text-decoration: none;
+
+  &:hover {
+    text-decoration: underline;
+    text-underline-offset: 3px;
+  }
+
+  &:focus-visible {
+    outline: 1px dashed var(--ink);
+    outline-offset: 1px;
+  }
+}

diff --git a/ooknet-design/src/components/Br
eadcrumbs/Breadcrumbs.scss b/ooknet-design/s
rc/components/Breadcrumbs/Breadcrumbs.scss
new file mode 100644
index 0000000..b76c1eb
--- /dev/null
+++ b/ooknet-design/src/components/Breadcrum
bs/Breadcrumbs.scss
@@ -0,0 +1,18 @@
+.crumbs {
+  display: block;
+}
+
+.crumb-link {
+  color: var(--ink);
+  text-decoration: none;
+
+  &:hover {
+    text-decoration: underline;
+    text-underline-offset: 3px;
+  }
+
+  &:focus-visible {
+    outline: 1px dashed var(--ink);
+    outline-offset: 1px;
+  }
+}
 

diff --git a/ooknet-design/src/components/Breadcrumbs/Breadcrumbs.test.ts b/ooknet
-design/src/components/Breadcrumbs/Breadcrumbs.test.ts
new file mode 100644
index 0000000..151e2a3
--- /dev/null
+++ b/ooknet-design/src/components/Breadcrumbs/Breadcrumbs.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, it } from "vitest";
+import { CRUMB_LABEL, crumbs, layoutCrumbs } from "./Breadcrumbs";
+import { len } from "../../lib/ascii";
+
+describe("crumbs", () => {
+  it("splits the filed path into linked segments", () => {
+    expect(crumbs("/META/STYLE/SHEET")).toEqual([
+      { label: "META", href: "/?q=meta" },
+      { label: "STYLE", href: "/?q=style" },
+      { label: "SHEET", href: "/?q=sheet" },
+    ]);
+  });
+
+  it("ignores empty segments", () => {
+    expect(crumbs("//INFO/")).toHaveLength(1);
+  });
+});
+
+describe("layoutCrumbs", () => {
+  const rowLen = (row: { label: string }[]) =>
+    row.reduce((s, c) => s + 1 + len(c.label), 0);
+
+  it("keeps a short path on one row", () => {
+    expect(layoutCrumbs(crumbs("/META/STYLE/SHEET"), 48)).toHaveLength(1);
+  });
+
+  it("wraps at segment boundaries within the width", () => {
+    const items = crumbs("/INFO/GRAPH/ENTROPY/BOUND/SPARSE/WINDOWS/PERF");
+    const rows = layoutCrumbs(items, 48);
+    expect(rows.length).toBeGreaterThan(1);
+    for (const row of rows) {
+      expect(rowLen(row)).toBeLessThanOrEqual(48 - len(CRUMB_LABEL));
+    }
+    expect(rows.flat()).toEqual(items);
+  });
+});

diff --git a/ooknet-design/src/components/Br
eadcrumbs/Breadcrumbs.test.ts b/ooknet-desig
n/src/components/Breadcrumbs/Breadcrumbs.tes
t.ts
new file mode 100644
index 0000000..151e2a3
--- /dev/null
+++ b/ooknet-design/src/components/Breadcrum
bs/Breadcrumbs.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, it } from "vites
t";
+import { CRUMB_LABEL, crumbs, layoutCrumbs 
} from "./Breadcrumbs";
+import { len } from "../../lib/ascii";
+
+describe("crumbs", () => {
+  it("splits the filed path into linked seg
ments", () => {
+    expect(crumbs("/META/STYLE/SHEET")).toE
qual([
+      { label: "META", href: "/?q=meta" },
+      { label: "STYLE", href: "/?q=style" }
,
+      { label: "SHEET", href: "/?q=sheet" }
,
+    ]);
+  });
+
+  it("ignores empty segments", () => {
+    expect(crumbs("//INFO/")).toHaveLength(
1);
+  });
+});
+
+describe("layoutCrumbs", () => {
+  const rowLen = (row: { label: string }[])
 =>
+    row.reduce((s, c) => s + 1 + len(c.labe
l), 0);
+
+  it("keeps a short path on one row", () =>
 {
+    expect(layoutCrumbs(crumbs("/META/STYLE
/SHEET"), 48)).toHaveLength(1);
+  });
+
+  it("wraps at segment boundaries within th
e width", () => {
+    const items = crumbs("/INFO/GRAPH/ENTRO
PY/BOUND/SPARSE/WINDOWS/PERF");
+    const rows = layoutCrumbs(items, 48);
+    expect(rows.length).toBeGreaterThan(1);
+    for (const row of rows) {
+      expect(rowLen(row)).toBeLessThanOrEqu
al(48 - len(CRUMB_LABEL));
+    }
+    expect(rows.flat()).toEqual(items);
+  });
+});
 

diff --git a/ooknet-design/src/components/Breadcrumbs/Breadcrumbs.ts b/ooknet-desi
gn/src/components/Breadcrumbs/Breadcrumbs.ts
new file mode 100644
index 0000000..0356e89
--- /dev/null
+++ b/ooknet-design/src/components/Breadcrumbs/Breadcrumbs.ts
@@ -0,0 +1,43 @@
+// Linkified [FILED UNDER] taxonomy path. Each segment searches the
+// index (like tag badges); long paths wrap at segment boundaries.
+import { checkGrid, len } from "../../lib/ascii";
+
+export const CRUMB_LABEL = "[FILED UNDER]: ";
+
+export interface Crumb {
+  label: string;
+  href: string;
+}
+
+export interface Props {
+  filed: string;
+}
+
+export function crumbs(filed: string): Crumb[] {
+  return filed
+    .split("/")
+    .filter(Boolean)
+    .map((seg) => ({
+      label: seg,
+      href: `/?q=${encodeURIComponent(seg.toLowerCase())}`,
+    }));
+}
+
+/** Rows of crumbs fitting `width` after the label indent; every crumb
+ *  costs "/" + label. */
+export function layoutCrumbs(items: Crumb[], width: number): Crumb[][] {
+  const avail = Math.max(1, width - len(CRUMB_LABEL));
+  const rows: Crumb[][] = [[]];
+  let used = 0;
+  for (const c of items) {
+    checkGrid(`/${c.label}`, avail, "breadcrumb segment");
+    const w = 1 + len(c.label);
+    if (used + w > avail && rows[rows.length - 1].length) {
+      rows.push([]);
+      used = 0;
+    }
+    rows[rows.length - 1].push(c);
+    used += w;
+  }
+  return rows;
+}

diff --git a/ooknet-design/src/components/Br
eadcrumbs/Breadcrumbs.ts b/ooknet-design/src
/components/Breadcrumbs/Breadcrumbs.ts
new file mode 100644
index 0000000..0356e89
--- /dev/null
+++ b/ooknet-design/src/components/Breadcrum
bs/Breadcrumbs.ts
@@ -0,0 +1,43 @@
+// Linkified [FILED UNDER] taxonomy path. E
ach segment searches the
+// index (like tag badges); long paths wrap
 at segment boundaries.
+import { checkGrid, len } from "../../lib/a
scii";
+
+export const CRUMB_LABEL = "[FILED UNDER]: 
";
+
+export interface Crumb {
+  label: string;
+  href: string;
+}
+
+export interface Props {
+  filed: string;
+}
+
+export function crumbs(filed: string): Crum
b[] {
+  return filed
+    .split("/")
+    .filter(Boolean)
+    .map((seg) => ({
+      label: seg,
+      href: `/?q=${encodeURIComponent(seg.t
oLowerCase())}`,
+    }));
+}
+
+/** Rows of crumbs fitting `width` after th
e label indent; every crumb
+ *  costs "/" + label. */
+export function layoutCrumbs(items: Crumb[]
, width: number): Crumb[][] {
+  const avail = Math.max(1, width - len(CRU
MB_LABEL));
+  const rows: Crumb[][] = [[]];
+  let used = 0;
+  for (const c of items) {
+    checkGrid(`/${c.label}`, avail, "breadc
rumb segment");
+    const w = 1 + len(c.label);
+    if (used + w > avail && rows[rows.lengt
h - 1].length) {
+      rows.push([]);
+      used = 0;
+    }
+    rows[rows.length - 1].push(c);
+    used += w;
+  }
+  return rows;
+}
 

diff --git a/ooknet-design/src/components/Tabs/Tabs.astro b/ooknet-design/src/comp
onents/Tabs/Tabs.astro
new file mode 100644
index 0000000..a2b38e3
--- /dev/null
+++ b/ooknet-design/src/components/Tabs/Tabs.astro
@@ -0,0 +1,61 @@
+---
+// Tab bar over a full-width rule. The bar and panels render once (both
+// are width-independent; panel content dual-renders itself), so active
+// state lives in one place. Panels fill via named slots: tab-0, tab-1...
+import Pre from "../Pre/Pre.astro";
+import Frame from "../Frame/Frame.astro";
+import { rule } from "../../lib/ascii";
+import type { Props } from "./Tabs";
+import "./Tabs.scss";
+
+const { labels } = Astro.props as Props;
+// slot[name] must be static, so dynamic panel slots render here.
+const panels = await Promise.all(labels.map((_, i) => Astro.slots.render(`tab-${i
}`)));
+---
+<div class="tabs">
+  <Pre class="tabs-bar"><span role="tablist">{labels.map((l, i) => (
+    <Fragment>{i > 0 ? "  " : ""}<button type="button" role="tab" class="tab-btn"
 aria-selected={i === 0 ? "true" : "false"}>[ {l} ]</button></Fragment>
+  ))}</span></Pre>
+  <Frame build={(w) => rule(w)} />
+  {panels.map((html, i) => (
+    <div class="tabs-panel" role="tabpanel" hidden={i !== 0} set:html={html} />
+  ))}
+</div>
+
+<script>
+  function activate(tabs: HTMLElement, idx: number) {
+    const btns = Array.from(tabs.querySelectorAll<HTMLButtonElement>(":scope > .t
abs-bar .tab-btn"));
+    const panels = Array.from(tabs.querySelectorAll<HTMLElement>(":scope > .tabs-
panel"));
+    btns.forEach((b, i) => b.setAttribute("aria-selected", String(i === idx)));
+    panels.forEach((p, i) => { p.hidden = i !== idx; });
+  }
+
+  function ownIndex(btn: HTMLButtonElement): { tabs: HTMLElement; idx: number } |
 null {
+    const tabs = btn.closest<HTMLElement>(".tabs");
+    if (!tabs) return null;
+    const btns = Array.from(tabs.querySelectorAll(":scope > .tabs-bar .tab-btn"))
;
+    return { tabs, idx: btns.indexOf(btn) };
+  }
+
+  document.addEventListener("click", (e) => {
+    const btn = (e.target as HTMLElement).closest<HTMLButtonElement>(".tab-btn");
+    if (!btn) return;
+    const own = ownIndex(btn);
+    if (own) activate(own.tabs, own.idx);
+  });
+
+  document.addEventListener("keydown", (e) => {
+    if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
+    const btn = (e.target as HTMLElement).closest?.<HTMLButtonElement>(".tab-btn"
);
+    if (!btn) return;
+    const own = ownIndex(btn);
+    if (!own) return;
+    const btns = Array.from(own.tabs.querySelectorAll<HTMLButtonElement>(":scope 
> .tabs-bar .tab-btn"));
+    const next = e.key === "ArrowRight"
+      ? (own.idx + 1) % btns.length
+      : (own.idx - 1 + btns.length) % btns.length;
+    e.preventDefault();
+    btns[next].focus();
+    activate(own.tabs, next);
+  });
+</script>

diff --git a/ooknet-design/src/components/Ta
bs/Tabs.astro b/ooknet-design/src/components
/Tabs/Tabs.astro
new file mode 100644
index 0000000..a2b38e3
--- /dev/null
+++ b/ooknet-design/src/components/Tabs/Tabs
.astro
@@ -0,0 +1,61 @@
+---
+// Tab bar over a full-width rule. The bar 
and panels render once (both
+// are width-independent; panel content dua
l-renders itself), so active
+// state lives in one place. Panels fill vi
a named slots: tab-0, tab-1...
+import Pre from "../Pre/Pre.astro";
+import Frame from "../Frame/Frame.astro";
+import { rule } from "../../lib/ascii";
+import type { Props } from "./Tabs";
+import "./Tabs.scss";
+
+const { labels } = Astro.props as Props;
+// slot[name] must be static, so dynamic pa
nel slots render here.
+const panels = await Promise.all(labels.map
((_, i) => Astro.slots.render(`tab-${i}`)));
+---
+<div class="tabs">
+  <Pre class="tabs-bar"><span role="tablist
">{labels.map((l, i) => (
+    <Fragment>{i > 0 ? "  " : ""}<button ty
pe="button" role="tab" class="tab-btn" aria-
selected={i === 0 ? "true" : "false"}>[ {l} 
]</button></Fragment>
+  ))}</span></Pre>
+  <Frame build={(w) => rule(w)} />
+  {panels.map((html, i) => (
+    <div class="tabs-panel" role="tabpanel"
 hidden={i !== 0} set:html={html} />
+  ))}
+</div>
+
+<script>
+  function activate(tabs: HTMLElement, idx:
 number) {
+    const btns = Array.from(tabs.querySelec
torAll<HTMLButtonElement>(":scope > .tabs-ba
r .tab-btn"));
+    const panels = Array.from(tabs.querySel
ectorAll<HTMLElement>(":scope > .tabs-panel"
));
+    btns.forEach((b, i) => b.setAttribute("
aria-selected", String(i === idx)));
+    panels.forEach((p, i) => { p.hidden = i
 !== idx; });
+  }
+
+  function ownIndex(btn: HTMLButtonElement)
: { tabs: HTMLElement; idx: number } | null 
{
+    const tabs = btn.closest<HTMLElement>("
.tabs");
+    if (!tabs) return null;
+    const btns = Array.from(tabs.querySelec
torAll(":scope > .tabs-bar .tab-btn"));
+    return { tabs, idx: btns.indexOf(btn) }
;
+  }
+
+  document.addEventListener("click", (e) =>
 {
+    const btn = (e.target as HTMLElement).c
losest<HTMLButtonElement>(".tab-btn");
+    if (!btn) return;
+    const own = ownIndex(btn);
+    if (own) activate(own.tabs, own.idx);
+  });
+
+  document.addEventListener("keydown", (e) 
=> {
+    if (e.key !== "ArrowLeft" && e.key !== 
"ArrowRight") return;
+    const btn = (e.target as HTMLElement).c
losest?.<HTMLButtonElement>(".tab-btn");
+    if (!btn) return;
+    const own = ownIndex(btn);
+    if (!own) return;
+    const btns = Array.from(own.tabs.queryS
electorAll<HTMLButtonElement>(":scope > .tab
s-bar .tab-btn"));
+    const next = e.key === "ArrowRight"
+      ? (own.idx + 1) % btns.length
+      : (own.idx - 1 + btns.length) % btns.
length;
+    e.preventDefault();
+    btns[next].focus();
+    activate(own.tabs, next);
+  });
+</script>
 

diff --git a/ooknet-design/src/components/Tabs/Tabs.scss b/ooknet-design/src/compo
nents/Tabs/Tabs.scss
new file mode 100644
index 0000000..5b6e4c5
--- /dev/null
+++ b/ooknet-design/src/components/Tabs/Tabs.scss
@@ -0,0 +1,36 @@
+.tabs {
+  .tab-btn {
+    font: inherit;
+    color: var(--ink);
+    background: none;
+    border: 0;
+    padding: 0;
+    margin: 0;
+    cursor: pointer;
+    white-space: pre;
+
+    &:hover:not([aria-selected="true"]) {
+      text-decoration: underline;
+      text-underline-offset: 3px;
+    }
+
+    &:focus-visible {
+      outline: 1px dashed var(--ink);
+      outline-offset: 1px;
+    }
+
+    &[aria-selected="true"] {
+      font-weight: 700;
+      background: var(--ink);
+      color: var(--paper);
+    }
+  }
+
+  .tabs-panel {
+    margin: 0.6em 0;
+
+    &[hidden] {
+      display: none;
+    }
+  }
+}

diff --git a/ooknet-design/src/components/Ta
bs/Tabs.scss b/ooknet-design/src/components/
Tabs/Tabs.scss
new file mode 100644
index 0000000..5b6e4c5
--- /dev/null
+++ b/ooknet-design/src/components/Tabs/Tabs
.scss
@@ -0,0 +1,36 @@
+.tabs {
+  .tab-btn {
+    font: inherit;
+    color: var(--ink);
+    background: none;
+    border: 0;
+    padding: 0;
+    margin: 0;
+    cursor: pointer;
+    white-space: pre;
+
+    &:hover:not([aria-selected="true"]) {
+      text-decoration: underline;
+      text-underline-offset: 3px;
+    }
+
+    &:focus-visible {
+      outline: 1px dashed var(--ink);
+      outline-offset: 1px;
+    }
+
+    &[aria-selected="true"] {
+      font-weight: 700;
+      background: var(--ink);
+      color: var(--paper);
+    }
+  }
+
+  .tabs-panel {
+    margin: 0.6em 0;
+
+    &[hidden] {
+      display: none;
+    }
+  }
+}
 

diff --git a/ooknet-design/src/components/Tabs/Tabs.ts b/ooknet-design/src/compone
nts/Tabs/Tabs.ts
new file mode 100644
index 0000000..09f42c8
--- /dev/null
+++ b/ooknet-design/src/components/Tabs/Tabs.ts
@@ -0,0 +1,3 @@
+export interface Props {
+  labels: string[];
+}

diff --git a/ooknet-design/src/components/Ta
bs/Tabs.ts b/ooknet-design/src/components/Ta
bs/Tabs.ts
new file mode 100644
index 0000000..09f42c8
--- /dev/null
+++ b/ooknet-design/src/components/Tabs/Tabs
.ts
@@ -0,0 +1,3 @@
+export interface Props {
+  labels: string[];
+}
 

diff --git a/ooknet-design/src/components/TopHeader/TopHeader.astro b/ooknet-desig
n/src/components/TopHeader/TopHeader.astro
index 1d7ecc2..0a19b5d 100644
--- a/ooknet-design/src/components/TopHeader/TopHeader.astro
+++ b/ooknet-design/src/components/TopHeader/TopHeader.astro
@@ -41,7 +41,7 @@ const PLACEHOLDER_NARROW = "_".repeat(18);
 <script>
   import Fuse from "fuse.js";
 
-  type Note = { id: string; note: string; title: string; category: string; tags: 
string[] };
+  type Note = { id: string; note: string; title: string; category: string; tags: 
string[]; filed: string };
   let fuse: Fuse<Note> | null = null;
 
   async function ensureLoaded() {
@@ -54,6 +54,7 @@ const PLACEHOLDER_NARROW = "_".repeat(18);
         { name: "note", weight: 1.5 },
         { name: "category", weight: 1 },
         { name: "tags", weight: 1 },
+        { name: "filed", weight: 1 },
       ],
       threshold: 0.4,
       ignoreLocation: true,

diff --git a/ooknet-design/src/components/To
pHeader/TopHeader.astro b/ooknet-design/src/
components/TopHeader/TopHeader.astro
index 1d7ecc2..0a19b5d 100644
--- a/ooknet-design/src/components/TopHeader
/TopHeader.astro
+++ b/ooknet-design/src/components/TopHeader
/TopHeader.astro
@@ -41,7 +41,7 @@ const PLACEHOLDER_NARROW =
 "_".repeat(18);
 <script>
   import Fuse from "fuse.js";
 
-  type Note = { id: string; note: string; t
itle: string; category: string; tags: string
[] };
+  type Note = { id: string; note: string; t
itle: string; category: string; tags: string
[]; filed: string };
   let fuse: Fuse<Note> | null = null;
 
   async function ensureLoaded() {
@@ -54,6 +54,7 @@ const PLACEHOLDER_NARROW =
 "_".repeat(18);
         { name: "note", weight: 1.5 },
         { name: "category", weight: 1 },
         { name: "tags", weight: 1 },
+        { name: "filed", weight: 1 },
       ],
       threshold: 0.4,
       ignoreLocation: true,
 

diff --git a/ooknet-design/src/pages/components.astro b/ooknet-design/src/pages/co
mponents.astro
index 6f54486..f2d83e2 100644
--- a/ooknet-design/src/pages/components.astro
+++ b/ooknet-design/src/pages/components.astro
@@ -17,6 +17,8 @@ import Spinner from "../components/Spinner/Spinner.astro";
 import Status from "../components/Status/Status.astro";
 import TableOfContents from "../components/TableOfContents/TableOfContents.astro"
;
 import Accordion from "../components/Accordion/Accordion.astro";
+import Tabs from "../components/Tabs/Tabs.astro";
+import Breadcrumbs from "../components/Breadcrumbs/Breadcrumbs.astro";
 import Timeline from "../components/Timeline/Timeline.astro";
 import type { TimelineEntry } from "../components/Timeline/Timeline";
 import { rule, ruleD, row2, pad } from "../lib/ascii";
@@ -156,6 +158,28 @@ const FILED_TREE: TreeNode[] = [
   <Timeline entries={HISTORY} />
   <Pre>{" "}</Pre>
 
+  <Frame build={section("TABS", "Tabs.astro")} />
+  <Pre>{" "}</Pre>
+  <Tabs labels={["NIX", "NPM", "MANUAL"]}>
+    <Fragment slot="tab-0">
+      <Pre>{"$ nix develop"}</Pre>
+      <Pre>{"$ npm run dev"}</Pre>
+    </Fragment>
+    <Fragment slot="tab-1">
+      <Pre>{"$ npm install"}</Pre>
+      <Pre>{"$ npm run dev"}</Pre>
+    </Fragment>
+    <Fragment slot="tab-2">
+      <Pre>{"clone the repo and bring your own node >= 22.12"}</Pre>
+    </Fragment>
+  </Tabs>
+  <Pre>{" "}</Pre>
+
+  <Frame build={section("BREADCRUMBS", "Breadcrumbs.astro")} />
+  <Pre>{" "}</Pre>
+  <Breadcrumbs filed="/INFO/GRAPH/ENTROPY/BOUND" />
+  <Pre>{" "}</Pre>
+
   <Frame build={section("TOAST", "Toast.astro")} />
   <Pre>{" "}</Pre>
   <Pre><span id="toast-ok"><Button label="QUEUE" variant="primary" /></span>{"  "
}<span id="toast-error"><Button label="JAM" /></span>{"  "}<span id="toast-pending
"><Button label="HOLD" /></span></Pre>

diff --git a/ooknet-design/src/pages/compone
nts.astro b/ooknet-design/src/pages/componen
ts.astro
index 6f54486..f2d83e2 100644
--- a/ooknet-design/src/pages/components.ast
ro
+++ b/ooknet-design/src/pages/components.ast
ro
@@ -17,6 +17,8 @@ import Spinner from "../co
mponents/Spinner/Spinner.astro";
 import Status from "../components/Status/St
atus.astro";
 import TableOfContents from "../components/
TableOfContents/TableOfContents.astro";
 import Accordion from "../components/Accord
ion/Accordion.astro";
+import Tabs from "../components/Tabs/Tabs.a
stro";
+import Breadcrumbs from "../components/Brea
dcrumbs/Breadcrumbs.astro";
 import Timeline from "../components/Timelin
e/Timeline.astro";
 import type { TimelineEntry } from "../comp
onents/Timeline/Timeline";
 import { rule, ruleD, row2, pad } from "../
lib/ascii";
@@ -156,6 +158,28 @@ const FILED_TREE: TreeN
ode[] = [
   <Timeline entries={HISTORY} />
   <Pre>{" "}</Pre>
 
+  <Frame build={section("TABS", "Tabs.astro
")} />
+  <Pre>{" "}</Pre>
+  <Tabs labels={["NIX", "NPM", "MANUAL"]}>
+    <Fragment slot="tab-0">
+      <Pre>{"$ nix develop"}</Pre>
+      <Pre>{"$ npm run dev"}</Pre>
+    </Fragment>
+    <Fragment slot="tab-1">
+      <Pre>{"$ npm install"}</Pre>
+      <Pre>{"$ npm run dev"}</Pre>
+    </Fragment>
+    <Fragment slot="tab-2">
+      <Pre>{"clone the repo and bring your 
own node >= 22.12"}</Pre>
+    </Fragment>
+  </Tabs>
+  <Pre>{" "}</Pre>
+
+  <Frame build={section("BREADCRUMBS", "Bre
adcrumbs.astro")} />
+  <Pre>{" "}</Pre>
+  <Breadcrumbs filed="/INFO/GRAPH/ENTROPY/B
OUND" />
+  <Pre>{" "}</Pre>
+
   <Frame build={section("TOAST", "Toast.ast
ro")} />
   <Pre>{" "}</Pre>
   <Pre><span id="toast-ok"><Button label="Q
UEUE" variant="primary" /></span>{"  "}<span
 id="toast-error"><Button label="JAM" /></sp
an>{"  "}<span id="toast-pending"><Button la
bel="HOLD" /></span></Pre>
 

diff --git a/ooknet-design/src/pages/notes/[slug].astro b/ooknet-design/src/pages/
notes/[slug].astro
index f039af8..b7c1968 100644
--- a/ooknet-design/src/pages/notes/[slug].astro
+++ b/ooknet-design/src/pages/notes/[slug].astro
@@ -6,6 +6,7 @@ import ArticleHeader from "../../components/ArticleHeader/ArticleH
eader.astro";
 import DocumentTitle from "../../components/DocumentTitle/DocumentTitle.astro";
 import ArticleMeta from "../../components/ArticleMeta/ArticleMeta.astro";
 import TableOfContents from "../../components/TableOfContents/TableOfContents.ast
ro";
+import Breadcrumbs from "../../components/Breadcrumbs/Breadcrumbs.astro";
 import ArticleSeeAlso from "../../components/ArticleSeeAlso/ArticleSeeAlso.astro"
;
 import ArticleTags from "../../components/ArticleTags/ArticleTags.astro";
 import ArticlePagination from "../../components/ArticlePagination/ArticlePaginati
on.astro";
@@ -37,7 +38,8 @@ const { Content, headings } = await render(note);
   <TopHeader />
   <ArticleHeader fm={note.data} />
   <DocumentTitle fm={note.data} />
-  <ArticleMeta fm={note.data} path={`notes/${note.id}`} />
+  <Breadcrumbs filed={note.data.filed} />
+  <ArticleMeta fm={note.data} />
   {headings.length >= 3 && <TableOfContents headings={headings} />}
   <Prose><Content /></Prose>
   <ArticleSeeAlso fm={note.data} />

diff --git a/ooknet-design/src/pages/notes/[
slug].astro b/ooknet-design/src/pages/notes/
[slug].astro
index f039af8..b7c1968 100644
--- a/ooknet-design/src/pages/notes/[slug].a
stro
+++ b/ooknet-design/src/pages/notes/[slug].a
stro
@@ -6,6 +6,7 @@ import ArticleHeader from ".
./../components/ArticleHeader/ArticleHeader.
astro";
 import DocumentTitle from "../../components
/DocumentTitle/DocumentTitle.astro";
 import ArticleMeta from "../../components/A
rticleMeta/ArticleMeta.astro";
 import TableOfContents from "../../componen
ts/TableOfContents/TableOfContents.astro";
+import Breadcrumbs from "../../components/B
readcrumbs/Breadcrumbs.astro";
 import ArticleSeeAlso from "../../component
s/ArticleSeeAlso/ArticleSeeAlso.astro";
 import ArticleTags from "../../components/A
rticleTags/ArticleTags.astro";
 import ArticlePagination from "../../compon
ents/ArticlePagination/ArticlePagination.ast
ro";
@@ -37,7 +38,8 @@ const { Content, headings 
} = await render(note);
   <TopHeader />
   <ArticleHeader fm={note.data} />
   <DocumentTitle fm={note.data} />
-  <ArticleMeta fm={note.data} path={`notes/
${note.id}`} />
+  <Breadcrumbs filed={note.data.filed} />
+  <ArticleMeta fm={note.data} />
   {headings.length >= 3 && <TableOfContents
 headings={headings} />}
   <Prose><Content /></Prose>
   <ArticleSeeAlso fm={note.data} />
 

diff --git a/ooknet-design/src/pages/search.json.ts b/ooknet-design/src/pages/sear
ch.json.ts
index 5805975..c4bec1c 100644
--- a/ooknet-design/src/pages/search.json.ts
+++ b/ooknet-design/src/pages/search.json.ts
@@ -11,6 +11,7 @@ export const GET: APIRoute = async () => {
     title: n.data.title,
     category: n.data.meta.category,
     tags: n.data.meta.tags,
+    filed: n.data.filed,
   }));
   return new Response(JSON.stringify(data), {
     headers: { "Content-Type": "application/json" },

diff --git a/ooknet-design/src/pages/search.
json.ts b/ooknet-design/src/pages/search.jso
n.ts
index 5805975..c4bec1c 100644
--- a/ooknet-design/src/pages/search.json.ts
+++ b/ooknet-design/src/pages/search.json.ts
@@ -11,6 +11,7 @@ export const GET: APIRoute
 = async () => {
     title: n.data.title,
     category: n.data.meta.category,
     tags: n.data.meta.tags,
+    filed: n.data.filed,
   }));
   return new Response(JSON.stringify(data),
 {
     headers: { "Content-Type": "application
/json" },
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET