diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 59db220..39e3e2d 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -129,6 +129,10 @@ {"id":"docs-z6h.5","title":"Add small mobile breakpoint (\u003c 480px) and pagination stacking","description":"## Files\n- documentation/styles/globals.css (modify)\n\n## What to do\nAdd a small mobile breakpoint for very narrow screens (iPhone SE at 375px, older Android at 360px). Also stack pagination cards vertically on mobile.\n\nAdd a new media query AFTER the existing mobile block (after line 1034):\n\n```css\n/* Small mobile: extra narrow screens */\n@media (max-width: 480px) {\n .sidebar {\n width: 85vw;\n min-width: 85vw;\n }\n\n .layout-header-inner {\n padding: 0 var(--space-3);\n }\n\n .layout-content {\n padding: var(--space-4) var(--space-3);\n }\n}\n```\n\nAlso add pagination stacking inside the existing `@media (max-width: 768px)` block (before line 1034):\n\n```css\n/* Stack pagination on mobile */\n.pagination {\n flex-direction: column;\n}\n\n.pagination-link {\n width: 100%;\n}\n\n.pagination-link-next {\n text-align: left;\n}\n```\n\nContext:\n- The sidebar is fixed at 250px width (line 108). On a 375px screen, that leaves only 125px visible content — too cramped. Using 85vw gives ~319px sidebar with some visible background.\n- Header inner padding is `0 var(--space-8)` (32px, line 195). On tiny screens, reduce to 12px.\n- Pagination uses `display: flex` with `justify-content: space-between` (lines 527-534). On mobile, prev/next cards should stack vertically.\n\n## Test\n```bash\ncd documentation \u0026\u0026 node -e \"\nconst fs = require(\\\"fs\\\");\nconst css = fs.readFileSync(\\\"styles/globals.css\\\", \\\"utf8\\\");\nconst hasSmallMobile = css.includes(\\\"max-width: 480px\\\");\nconst has85vw = /\\\\.sidebar[^}]*width:\\\\s*85vw/.test(css);\nconst mobileBlock = css.split(\\\"@media (max-width: 768px)\\\").slice(1).join(\\\"\\\");\nconst hasPaginationStack = /\\\\.pagination[^}]*flex-direction:\\\\s*column/.test(mobileBlock);\nif (!hasSmallMobile) { console.error(\\\"FAIL: no 480px breakpoint\\\"); process.exit(1); }\nif (!has85vw) { console.error(\\\"FAIL: no 85vw sidebar\\\"); process.exit(1); }\nif (!hasPaginationStack) { console.error(\\\"FAIL: no pagination stacking\\\"); process.exit(1); }\nconsole.log(\\\"PASS\\\");\n\"\n```\n\n## Dont\n- Do not modify the existing breakpoints\n- Do not change desktop styles\n- Do not modify any JS files\n- Do not change the sidebar width CSS variable — override the width directly on .sidebar","status":"closed","priority":2,"issue_type":"task","assignee":"sharfy-test.climateai.org","owner":"sharfy-test.climateai.org","created_at":"2026-02-16T23:34:17.12088+13:00","created_by":"sharfy-test.climateai.org","updated_at":"2026-02-16T23:39:44.212351+13:00","closed_at":"2026-02-16T23:39:44.212351+13:00","close_reason":"b2ca35e Add small mobile breakpoint (\u003c480px) and pagination stacking","labels":["scope:small"],"dependencies":[{"issue_id":"docs-z6h.5","depends_on_id":"docs-z6h","type":"parent-child","created_at":"2026-02-16T23:34:17.122179+13:00","created_by":"sharfy-test.climateai.org"}]} {"id":"hypercerts-atproto-documentation-5sb","title":"Epic: Fix last-updated date rendering bug — DOM injection → React component","description":"The LastUpdated component uses DOM injection (useEffect + appendChild) which races with Markdoc content rendering, causing the date to appear above the h1 on some pages. Fix: rewrite as a proper React component rendered inside \u003carticle\u003e after {children}. Changes already written on branch fix/last-updated-bug — needs commit, push, and PR. Success: last-updated date always appears at the bottom of the article content, never above the h1.","status":"open","priority":1,"issue_type":"epic","owner":"sharfy-test.climateai.org","created_at":"2026-03-06T18:09:25.532372+08:00","created_by":"sharfy-test.climateai.org","updated_at":"2026-03-06T18:09:25.532372+08:00","labels":["scope:trivial"]} {"id":"hypercerts-atproto-documentation-5sb.1","title":"Commit, push, and PR the last-updated bug fix (branch fix/last-updated-bug)","description":"## Files\n- components/LastUpdated.js (modified — already on branch)\n- components/Layout.js (modified — already on branch)\n\n## What to do\nThe changes are already written on branch fix/last-updated-bug. Just:\n1. Verify npm run build succeeds\n2. git add components/LastUpdated.js components/Layout.js\n3. git commit -m 'fix: rewrite LastUpdated as React component to fix rendering position'\n4. git push -u origin fix/last-updated-bug\n5. gh pr create targeting main\n\nThe changes: LastUpdated.js was rewritten from DOM injection (useEffect + appendChild) to a proper React component that returns \u003cp className='last-updated'\u003e. Layout.js moved \u003cLastUpdated /\u003e from before \u003carticle\u003e to inside \u003carticle\u003e after {children}.\n\n## Don't\n- Modify the changes — they're already correct\n- Touch lib/lastUpdated.json (it has regenerated dates, that's expected)","acceptance_criteria":"1. PR created targeting main. 2. npm run build succeeds. 3. LastUpdated.js does not import useEffect. 4. Layout.js renders \u003cLastUpdated /\u003e inside \u003carticle\u003e after {children}.","status":"open","priority":1,"issue_type":"task","owner":"sharfy-test.climateai.org","estimated_minutes":10,"created_at":"2026-03-06T18:09:35.388648+08:00","created_by":"sharfy-test.climateai.org","updated_at":"2026-03-06T18:09:35.388648+08:00","labels":["scope:trivial"],"dependencies":[{"issue_id":"hypercerts-atproto-documentation-5sb.1","depends_on_id":"hypercerts-atproto-documentation-5sb","type":"parent-child","created_at":"2026-03-06T18:09:35.389834+08:00","created_by":"sharfy-test.climateai.org"}]} +{"id":"hypercerts-atproto-documentation-7fu","title":"Epic: Full-text search — index page content and search by keyword","description":"## Problem\nThe search bar (Cmd+K) only matches against page titles from the hardcoded navigation tree. It cannot find keywords in page body content, headings, or descriptions. For example, searching 'OAuth' returns nothing because no page is titled 'OAuth'.\n\n## Goals\n1. Build a search index at build time that includes page titles, descriptions, headings (h2/h3), and body text from all .md files\n2. Use a lightweight client-side search library (FlexSearch) for fast full-text keyword search\n3. Show search results with matched context snippets so users can see where the keyword appears\n4. Keep the existing UX (Cmd+K modal, keyboard navigation, quick links) intact\n\n## Key files\n- components/SearchDialog.js — current search UI with fuzzy title matching\n- lib/navigation.js — current data source (title + path only)\n- styles/globals.css — search dialog styles (lines 1537-1665)\n- pages/**/*.md — content to be indexed\n\n## Architecture\n- Build script generates public/search-index.json from all .md files\n- SearchDialog loads index on first open, initializes FlexSearch\n- Results show title, section, and a snippet of matched body text\n- Field boosting: title matches rank higher than body matches\n\n## Scope\nSearch dialog replacement + build script. No changes to page rendering or navigation.","status":"open","priority":1,"issue_type":"epic","owner":"sharfy-test.climateai.org","created_at":"2026-03-09T16:50:30.512727+08:00","created_by":"sharfy-test.climateai.org","updated_at":"2026-03-09T16:50:30.512727+08:00","labels":["scope:medium"]} +{"id":"hypercerts-atproto-documentation-7fu.1","title":"Build-time search index generator — extract titles, headings, descriptions, and body text from all .md files","description":"## Files\n- lib/generate-search-index.js (create)\n- package.json (modify — add to build/dev scripts)\n\n## What to do\nCreate a Node.js build script that reads all .md files under pages/, extracts searchable content, and writes a JSON index to public/search-index.json.\n\n### Script behavior:\n1. Walk pages/ recursively, find all .md files (same pattern as lib/generate-last-updated.js)\n2. For each file, extract:\n - `path`: route path (e.g. \"/getting-started/quickstart\") — same logic as generate-last-updated.js\n - `title`: from YAML frontmatter `title` field (between `---` delimiters at top of file)\n - `description`: from YAML frontmatter `description` field\n - `headings`: array of h2/h3 text (lines starting with `## ` or `### `)\n - `body`: full plain text of the markdown with frontmatter, code blocks, Markdoc tags (`{% ... %}`), markdown syntax (`#`, `*`, `[`, etc.), and HTML tags stripped out. Collapse multiple whitespace/newlines into single spaces. Trim to max 5000 chars per page.\n3. Look up the section for each path from the navigation tree. Import `flattenNavigation` from lib/navigation.js — but since this is a CommonJS script and navigation.js uses ESM exports, instead parse the navigation structure manually or use a simple path-to-section mapping. The simplest approach: hardcode a function that maps path prefixes to sections:\n - /getting-started/* → 'Get Started'\n - /core-concepts/* → 'Core Concepts'\n - /tools/* → 'Tools'\n - /architecture/* → 'Architecture'\n - /lexicons/* → 'Reference'\n - /reference/* → 'Reference'\n - /ecosystem/* → 'Ecosystem \u0026 Vision'\n - /roadmap → 'Reference'\n - / → 'Get Started'\n4. Write output as JSON array to public/search-index.json:\n```json\n[\n {\n \"path\": \"/getting-started/quickstart\",\n \"title\": \"Quickstart\",\n \"description\": \"Create your first hypercert...\",\n \"section\": \"Get Started\",\n \"headings\": [\"Install dependencies\", \"Authenticate\", ...],\n \"body\": \"This guide walks through creating...\"\n },\n ...\n]\n```\n\n### Frontmatter parsing:\nSimple regex — no YAML library needed:\n```js\nconst fmMatch = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n// then extract title: and description: lines\n```\n\n### Markdoc/markdown stripping:\nRemove these patterns from body text:\n- Frontmatter block (`---...---`)\n- Code blocks (`\\`\\`\\`....\\`\\`\\``)\n- Markdoc tags (`{% callout ... %}`, `{% /callout %}`, `{% table %}`, etc.)\n- Heading markers (`# `, `## `, `### `)\n- Markdown links: `[text](url)` → `text`\n- Markdown bold/italic: `**text**` → `text`, `*text*` → `text`\n- Inline code backticks\n- HTML tags\n- Collapse whitespace\n\n### package.json changes:\nUpdate both scripts to run the index generator before next:\n```json\n\"dev\": \"node lib/generate-search-index.js \u0026\u0026 node lib/generate-last-updated.js \u0026\u0026 next dev --webpack\",\n\"build\": \"node lib/generate-search-index.js \u0026\u0026 node lib/generate-last-updated.js \u0026\u0026 next build --webpack\"\n```\n\n## Don't\n- Use any npm dependencies — this is pure Node.js (fs, path, regex)\n- Include files outside pages/ (no AGENTS.md, no .beads/, no README)\n- Include the pages/index.md home page body (it's mostly card markup) — include title only\n- Modify any existing files except package.json","acceptance_criteria":"1. Running `node lib/generate-search-index.js` creates public/search-index.json\n2. The JSON file is a valid JSON array with one entry per .md page (~43 entries)\n3. Each entry has path, title, section, headings (array), body (string), and description (string or empty)\n4. Body text does not contain markdown syntax, Markdoc tags, code blocks, or frontmatter\n5. Body text is max 5000 chars per page\n6. `pnpm build` succeeds and includes the search index generation\n7. The search index file is under 500KB total","status":"closed","priority":1,"issue_type":"task","assignee":"sharfy-test.climateai.org","owner":"sharfy-test.climateai.org","estimated_minutes":45,"created_at":"2026-03-09T16:51:18.334027+08:00","created_by":"sharfy-test.climateai.org","updated_at":"2026-03-09T17:05:01.575079+08:00","closed_at":"2026-03-09T17:05:01.575079+08:00","close_reason":"6417d68 Build-time search index generator implemented - extracts titles, headings, descriptions, and body text from all .md files","labels":["scope:small"],"dependencies":[{"issue_id":"hypercerts-atproto-documentation-7fu.1","depends_on_id":"hypercerts-atproto-documentation-7fu","type":"parent-child","created_at":"2026-03-09T16:51:18.336538+08:00","created_by":"sharfy-test.climateai.org"}]} +{"id":"hypercerts-atproto-documentation-7fu.2","title":"Replace SearchDialog with FlexSearch-powered full-text search","description":"## Files\n- components/SearchDialog.js (modify — major rewrite)\n- package.json (modify — add flexsearch dependency)\n\n## What to do\nReplace the current title-only fuzzy search with FlexSearch-powered full-text search that queries the search index generated by task 7fu.1.\n\n### 1. Add FlexSearch dependency\n```bash\npnpm add flexsearch\n```\n\n### 2. Rewrite SearchDialog.js\n\n#### Index loading:\n- On first dialog open, fetch `/search-index.json` (it's in public/)\n- Store the raw data in a module-level variable (not state) so it persists across opens\n- Create a FlexSearch Document index with these fields:\n```js\nimport { Document } from 'flexsearch';\n\nconst index = new Document({\n document: {\n id: 'path',\n index: ['title', 'description', 'headings', 'body'],\n },\n tokenize: 'forward',\n});\n```\n- Add all entries from the JSON to the index\n- Show a loading state ('Loading...') while fetching on first open\n\n#### Search:\n- On query change (debounce 150ms), search the index:\n```js\nconst results = index.search(query, { limit: 20, enrich: true });\n```\n- FlexSearch Document returns results grouped by field. Merge and deduplicate by path, prioritizing: title matches first, then description, then headings, then body.\n- For each result, look up the full entry from the raw data to get title, path, section, description, body.\n\n#### Snippet generation:\n- For body matches, generate a context snippet: find the first occurrence of the query in the body text, extract ~120 chars around it, and wrap the matched term in a `\u003cmark\u003e` tag.\n- Helper function:\n```js\nfunction getSnippet(body, query, contextChars = 60) {\n const lower = body.toLowerCase();\n const idx = lower.indexOf(query.toLowerCase());\n if (idx === -1) return '';\n const start = Math.max(0, idx - contextChars);\n const end = Math.min(body.length, idx + query.length + contextChars);\n let snippet = '';\n if (start \u003e 0) snippet += '...';\n snippet += body.slice(start, end);\n if (end \u003c body.length) snippet += '...';\n return snippet;\n}\n```\n\n#### Result display:\nEach result item should show:\n- `.search-result-title` — page title (existing class)\n- `.search-result-snippet` — the context snippet with matched term highlighted (NEW class, see CSS task)\n- `.search-result-path` — the URL path (existing class)\n\nKeep the existing structure:\n- Empty state: Quick Links (unchanged)\n- Results: grouped by section (unchanged grouping logic)\n- No results: 'No results' message + Quick Links (unchanged)\n- Keyboard navigation: Arrow Up/Down, Enter, Escape (unchanged)\n\n#### Keep from current implementation:\n- `QUICK_LINK_PATHS` array and quick links logic\n- Keyboard navigation (arrow keys, enter, escape)\n- `useRouter` for navigation\n- Focus management (focus input on open)\n- Overlay click-to-close\n- All existing class names for styling compatibility\n\n### 3. Remove old code:\n- Remove the `fuzzyMatch` function\n- Remove the `flattenNavigation` import (no longer needed)\n\n## Don't\n- Change the search dialog's visual layout or structure — only the data source and matching logic\n- Remove keyboard navigation\n- Remove quick links\n- Change any CSS class names (add new ones, don't rename existing)\n- Import from lib/navigation.js — the search index JSON replaces it for search","acceptance_criteria":"1. Searching 'OAuth' returns results (pages that mention OAuth in their body/headings)\n2. Searching 'createRecord' returns results (appears in code examples in body text)\n3. Searching 'PDS' returns multiple results with context snippets showing where PDS appears\n4. Title matches appear before body-only matches in results\n5. Results are grouped by section (Get Started, Core Concepts, etc.)\n6. Keyboard navigation (arrow keys, enter, escape) still works\n7. Quick Links still appear when search is empty\n8. 'No results' message appears for nonsense queries\n9. pnpm build succeeds\n10. No console errors when opening/using search","status":"closed","priority":1,"issue_type":"task","assignee":"sharfy-test.climateai.org","owner":"sharfy-test.climateai.org","estimated_minutes":60,"created_at":"2026-03-09T16:51:46.000565+08:00","created_by":"sharfy-test.climateai.org","updated_at":"2026-03-09T17:09:51.234016+08:00","closed_at":"2026-03-09T17:09:51.234016+08:00","close_reason":"7cb8d0e Replace SearchDialog with FlexSearch-powered full-text search","labels":["scope:small"],"dependencies":[{"issue_id":"hypercerts-atproto-documentation-7fu.2","depends_on_id":"hypercerts-atproto-documentation-7fu","type":"parent-child","created_at":"2026-03-09T16:51:46.001904+08:00","created_by":"sharfy-test.climateai.org"},{"issue_id":"hypercerts-atproto-documentation-7fu.2","depends_on_id":"hypercerts-atproto-documentation-7fu.1","type":"blocks","created_at":"2026-03-09T16:51:46.003258+08:00","created_by":"sharfy-test.climateai.org"}]} +{"id":"hypercerts-atproto-documentation-7fu.3","title":"Add CSS for search result snippets and mark highlighting","description":"## Files\n- styles/globals.css (modify — add rules after line ~1665, the end of the SearchDialog section)\n\n## What to do\nAdd CSS rules for the new search result snippet element and the `\u003cmark\u003e` highlight used for matched terms.\n\n### Add these rules:\n```css\n.search-result-snippet {\n font-size: 12px;\n color: var(--color-text-secondary);\n line-height: 1.5;\n margin-top: 2px;\n overflow: hidden;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n\n.search-result-snippet mark {\n background: oklch(0.85 0.15 85);\n color: inherit;\n border-radius: 2px;\n padding: 0 2px;\n}\n\nhtml.dark .search-result-snippet mark {\n background: oklch(0.45 0.12 85);\n color: var(--color-text-primary);\n}\n```\n\n### Context:\n- `.search-result-snippet` sits between `.search-result-title` and `.search-result-path` in each result item\n- The `\u003cmark\u003e` tag wraps the matched search term within the snippet\n- The snippet is clamped to 2 lines to keep results compact\n- Light mode uses a warm yellow highlight; dark mode uses a muted amber\n\n## Don't\n- Modify any existing CSS rules\n- Change existing class names\n- Add rules outside the SearchDialog CSS section","acceptance_criteria":"1. .search-result-snippet rule exists in globals.css\n2. .search-result-snippet mark rule exists with background highlight color\n3. html.dark .search-result-snippet mark rule exists for dark mode\n4. Snippet text is clamped to 2 lines via -webkit-line-clamp\n5. pnpm build succeeds","status":"closed","priority":1,"issue_type":"task","assignee":"sharfy-test.climateai.org","owner":"sharfy-test.climateai.org","estimated_minutes":15,"created_at":"2026-03-09T16:51:58.988935+08:00","created_by":"sharfy-test.climateai.org","updated_at":"2026-03-09T17:11:30.040681+08:00","closed_at":"2026-03-09T17:11:30.040681+08:00","close_reason":"9f49190 Add CSS for search result snippets and mark highlighting","labels":["scope:trivial"],"dependencies":[{"issue_id":"hypercerts-atproto-documentation-7fu.3","depends_on_id":"hypercerts-atproto-documentation-7fu","type":"parent-child","created_at":"2026-03-09T16:51:58.990183+08:00","created_by":"sharfy-test.climateai.org"},{"issue_id":"hypercerts-atproto-documentation-7fu.3","depends_on_id":"hypercerts-atproto-documentation-7fu.2","type":"blocks","created_at":"2026-03-09T16:51:58.991534+08:00","created_by":"sharfy-test.climateai.org"}]} {"id":"hypercerts-atproto-documentation-buj","title":"Epic: Fix remaining factual errors — old NSIDs, PDS validation claim, certified.ink URL","description":"17 instances of old NSIDs (org.hypercerts.claim.attachment/evaluation/measurement should be org.hypercerts.context.*) across 5 pages, plus 1 incorrect PDS validation claim in data-flow-and-lifecycle.md line 38, plus 1 certified.ink → certified.app in cel-work-scopes.md line 196. Success: zero instances of old context-namespace NSIDs in non-lexicon pages, no claims that PDS validates against schemas, certified.app used everywhere.","status":"open","priority":1,"issue_type":"epic","owner":"sharfy-test.climateai.org","created_at":"2026-03-06T18:07:54.799315+08:00","created_by":"sharfy-test.climateai.org","updated_at":"2026-03-06T18:07:54.799315+08:00","labels":["scope:medium"]} {"id":"hypercerts-atproto-documentation-buj.1","title":"Fix old NSIDs in data-flow-and-lifecycle.md (3 instances) and PDS validation error","description":"## Files\n- pages/architecture/data-flow-and-lifecycle.md (modify)\n\n## What to do\nReplace 3 old NSIDs with correct context-namespace NSIDs:\n- Line 63: `org.hypercerts.claim.attachment` → `org.hypercerts.context.attachment`\n- Line 67: `org.hypercerts.claim.measurement` → `org.hypercerts.context.measurement`\n- Line 101: `org.hypercerts.claim.evaluation` → `org.hypercerts.context.evaluation`\n\nFix PDS validation error:\n- Line 38: Change 'The PDS validates the record against the lexicon schema.' to 'The PDS stores the record in the contributor'\\''s repository.' (ATProto PDS instances are schema-agnostic — they do NOT validate records against lexicon schemas. Validation happens at the indexer/app view layer.)\n\n## Don't\n- Change any other content on this page\n- Add explanations about why PDS doesn't validate — just fix the sentence\n- Touch the ASCII diagrams","acceptance_criteria":"1. Zero instances of org.hypercerts.claim.attachment, org.hypercerts.claim.measurement, or org.hypercerts.claim.evaluation in the file. 2. Line 38 no longer claims PDS validates against lexicon schemas. 3. The three correct NSIDs (org.hypercerts.context.attachment, org.hypercerts.context.measurement, org.hypercerts.context.evaluation) appear in the file. 4. npm run build succeeds.","status":"open","priority":1,"issue_type":"task","owner":"sharfy-test.climateai.org","estimated_minutes":15,"created_at":"2026-03-06T18:08:07.273568+08:00","created_by":"sharfy-test.climateai.org","updated_at":"2026-03-06T18:08:07.273568+08:00","labels":["scope:trivial"],"dependencies":[{"issue_id":"hypercerts-atproto-documentation-buj.1","depends_on_id":"hypercerts-atproto-documentation-buj","type":"parent-child","created_at":"2026-03-06T18:08:07.274562+08:00","created_by":"sharfy-test.climateai.org"}]} {"id":"hypercerts-atproto-documentation-buj.2","title":"Fix old NSIDs in hypercerts-core-data-model.md (3 instances)","description":"## Files\n- pages/core-concepts/hypercerts-core-data-model.md (modify)\n\n## What to do\nReplace 3 old NSIDs in the table at lines 52-54:\n- Line 52: `org.hypercerts.claim.attachment` → `org.hypercerts.context.attachment`\n- Line 53: `org.hypercerts.claim.measurement` → `org.hypercerts.context.measurement`\n- Line 54: `org.hypercerts.claim.evaluation` → `org.hypercerts.context.evaluation`\n\nThese are in the 'Records that attach to a hypercert' table, in the Lexicon column.\n\n## Don't\n- Change any other content on this page\n- Modify the table structure or other columns\n- Touch the contributor model section (it was recently rewritten and is correct)","acceptance_criteria":"1. Zero instances of org.hypercerts.claim.attachment, org.hypercerts.claim.measurement, or org.hypercerts.claim.evaluation in the file. 2. The three correct NSIDs (org.hypercerts.context.attachment, org.hypercerts.context.measurement, org.hypercerts.context.evaluation) appear in the Lexicon column. 3. npm run build succeeds.","status":"open","priority":1,"issue_type":"task","owner":"sharfy-test.climateai.org","estimated_minutes":10,"created_at":"2026-03-06T18:08:15.674869+08:00","created_by":"sharfy-test.climateai.org","updated_at":"2026-03-06T18:08:15.674869+08:00","labels":["scope:trivial"],"dependencies":[{"issue_id":"hypercerts-atproto-documentation-buj.2","depends_on_id":"hypercerts-atproto-documentation-buj","type":"parent-child","created_at":"2026-03-06T18:08:15.677058+08:00","created_by":"sharfy-test.climateai.org"}]} @@ -163,3 +167,4 @@ {"id":"hypercerts-atproto-documentation-w96.5","title":"Fix wrong lexicon NSIDs in data-flow-and-lifecycle page","description":"## Files\n- pages/architecture/data-flow-and-lifecycle.md (modify)\n\n## What to do\nFix two incorrect NSIDs:\n\n### Fix 1: Line 59\nCurrently says: `org.hypercerts.claim.contributionDetails`\nChange to: `org.hypercerts.claim.contribution`\n\n### Fix 2: Line 79\nCurrently says: `org.hypercerts.claim.collection`\nChange to: `org.hypercerts.collection`\n\nThe actual lexicon IDs are:\n- org.hypercerts.claim.contribution (file: contribution.json)\n- org.hypercerts.collection (file: collection.json, no .claim. segment)\n\n## Dont\n- Do not change any surrounding text or page structure\n- Only change the two NSID strings","acceptance_criteria":"1. Line 59 (or equivalent) contains `org.hypercerts.claim.contribution` (not contributionDetails)\n2. Line 79 (or equivalent) contains `org.hypercerts.collection` (not org.hypercerts.claim.collection)\n3. No other content is changed\n4. File parses as valid Markdown","status":"closed","priority":1,"issue_type":"task","assignee":"karma.gainforest.id","owner":"karma.gainforest.id","estimated_minutes":10,"created_at":"2026-03-05T19:58:14.602853024+06:00","created_by":"karma.gainforest.id","updated_at":"2026-03-05T20:04:27.406244913+06:00","closed_at":"2026-03-05T20:04:27.406244913+06:00","close_reason":"91f3ecd Fix wrong lexicon NSIDs in data-flow-and-lifecycle page","labels":["scope:trivial"],"dependencies":[{"issue_id":"hypercerts-atproto-documentation-w96.5","depends_on_id":"hypercerts-atproto-documentation-w96","type":"parent-child","created_at":"2026-03-05T19:58:14.60526901+06:00","created_by":"karma.gainforest.id"}]} {"id":"hypercerts-atproto-documentation-w96.6","title":"Fix wrong collection NSID in roadmap page","description":"## Files\n- pages/roadmap.md (modify)\n\n## What to do\nFix incorrect collection NSID on line 69.\n\nCurrently says: `org.hypercerts.claim.collection`\nChange to: `org.hypercerts.collection`\n\nThe actual lexicon ID is org.hypercerts.collection (no .claim. segment).\n\n## Dont\n- Do not change any other content on the roadmap page\n- Only change the one NSID string","acceptance_criteria":"1. Line 69 (or the collection row in the table) shows `org.hypercerts.collection` (not org.hypercerts.claim.collection)\n2. No other content is changed\n3. File parses as valid Markdown","status":"closed","priority":1,"issue_type":"task","assignee":"karma.gainforest.id","owner":"karma.gainforest.id","estimated_minutes":5,"created_at":"2026-03-05T19:58:19.75432948+06:00","created_by":"karma.gainforest.id","updated_at":"2026-03-05T20:04:11.56608607+06:00","closed_at":"2026-03-05T20:04:11.56608607+06:00","close_reason":"3352ec1 Fix wrong collection NSID in roadmap page","labels":["scope:trivial"],"dependencies":[{"issue_id":"hypercerts-atproto-documentation-w96.6","depends_on_id":"hypercerts-atproto-documentation-w96","type":"parent-child","created_at":"2026-03-05T19:58:19.756668539+06:00","created_by":"karma.gainforest.id"}]} {"id":"hypercerts-atproto-documentation-w96.7","title":"Rewrite contributor model description and add missing record types in core data model page","description":"## Files\n- pages/core-concepts/hypercerts-core-data-model.md (modify)\n\n## What to do\nThe core data model page has structural inaccuracies about how contributors work. Fix the contributor model description and the \"How records connect\" tree.\n\n### 1. Fix the \"Additional details\" section (lines 26-33)\nThe current description says ContributorInformation and ContributionDetails are \"separate records with their own AT-URI\" that \"can be referenced from the activity claim.\" This is misleading.\n\nRewrite to explain the actual model: The activity claim has a `contributors` array where each entry is a contributor object containing:\n- `contributorIdentity`: either an inline identity string (DID) via `#contributorIdentity`, OR a strong reference to an `org.hypercerts.claim.contributorInformation` record\n- `contributionWeight`: optional relative weight string\n- `contributionDetails`: either an inline role string via `#contributorRole`, OR a strong reference to an `org.hypercerts.claim.contribution` record\n\nEmphasize the dual inline/reference pattern — simple cases use inline strings, richer profiles use separate records.\n\nUpdate the table to reflect the correct lexicon name `org.hypercerts.claim.contribution` (not contributionDetails).\n\n### 2. Fix the \"How records connect\" tree (lines 70-82)\nThe tree currently shows ContributorInformation and ContributionDetails as separate child records. Update it to show them as embedded within contributor objects, reflecting the actual structure. Example:\n\n```text\nActivity Claim (the core record)\n├── contributors[0]\n│ ├── contributorIdentity: Alice (inline DID or ref to ContributorInformation)\n│ ├── contributionWeight: \"1\"\n│ └── contributionDetails: Lead author (inline role or ref to Contribution)\n├── contributors[1]\n│ ├── contributorIdentity: → ContributorInformation record (Bob)\n│ └── contributionDetails: → Contribution record (Technical reviewer, Jan-Mar)\n├── Attachment: GitHub repository link\n├── Measurement: 12 pages written\n├── Measurement: 8,500 words\n└── Evaluation: \"High-quality documentation\" (by Carol)\n```\n\n## Dont\n- Do not change the \"The core record: activity claim\" section (lines 12-23) — the four dimensions table is correct\n- Do not change the \"Grouping hypercerts\" section\n- Do not change the \"Mutability\" or \"What happens next\" sections\n- Do not add new record types (rights, acknowledgement, funding receipt) — this task is fixes only\n- Do not add code examples or API usage — this is a conceptual page\n- Keep the writing style consistent with the rest of the page (concise, factual, no marketing language)","acceptance_criteria":"1. The \"Additional details\" section accurately describes the dual inline/reference pattern for contributors\n2. The contributor table shows `org.hypercerts.claim.contribution` (not contributionDetails)\n3. The \"How records connect\" tree shows contributors as embedded objects with inline/reference options\n4. The four dimensions table in \"The core record\" section is unchanged\n5. The \"Grouping hypercerts\", \"Mutability\", and \"What happens next\" sections are unchanged\n6. No new record types are added to the page\n7. File parses as valid Markdown","status":"closed","priority":2,"issue_type":"task","assignee":"karma.gainforest.id","owner":"karma.gainforest.id","estimated_minutes":45,"created_at":"2026-03-05T19:58:48.627566144+06:00","created_by":"karma.gainforest.id","updated_at":"2026-03-05T20:05:12.194006503+06:00","closed_at":"2026-03-05T20:05:12.194006503+06:00","close_reason":"e55324a Rewrite contributor model description and fix records tree","labels":["scope:small"],"dependencies":[{"issue_id":"hypercerts-atproto-documentation-w96.7","depends_on_id":"hypercerts-atproto-documentation-w96","type":"parent-child","created_at":"2026-03-05T19:58:48.630865221+06:00","created_by":"karma.gainforest.id"},{"issue_id":"hypercerts-atproto-documentation-w96.7","depends_on_id":"hypercerts-atproto-documentation-w96.4","type":"blocks","created_at":"2026-03-05T19:58:48.635719127+06:00","created_by":"karma.gainforest.id"}]} +{"id":"hypercerts-atproto-documentation-woj","title":"Epic: Resolve CodeRabbit review for PR #78","description":"Resolve all open CodeRabbit inline review comments on PR #78. 3 comments across 1 file (pages/tools/scaffold.md).","status":"closed","priority":1,"issue_type":"epic","owner":"kzoepa@gmail.com","created_at":"2026-03-05T15:57:13.054348747+06:00","created_by":"kzoeps","updated_at":"2026-03-06T18:07:23.476296+08:00","closed_at":"2026-03-06T18:07:23.476296+08:00","close_reason":"4e902e4 Both children closed — CodeRabbit review items resolved","labels":["needs-integration-review","scope:small"]} diff --git a/components/SearchDialog.js b/components/SearchDialog.js index cabe80c..b451eff 100644 --- a/components/SearchDialog.js +++ b/components/SearchDialog.js @@ -1,16 +1,24 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { useRouter } from 'next/router'; -import { flattenNavigation } from '../lib/navigation'; - -// Fuzzy match: all characters of query appear in text in order (case-insensitive) -function fuzzyMatch(query, text) { - const q = query.toLowerCase(); - const t = text.toLowerCase(); - let qi = 0; - for (let ti = 0; ti < t.length && qi < q.length; ti++) { - if (t[ti] === q[qi]) qi++; - } - return qi === q.length; +import FlexSearch from 'flexsearch'; + +// Module-level variables to persist across dialog opens +let searchData = null; +let searchIndex = null; +let isLoading = false; + +// Generate a context snippet with the matched term highlighted +function getSnippet(body, query, contextChars = 60) { + const lower = body.toLowerCase(); + const idx = lower.indexOf(query.toLowerCase()); + if (idx === -1) return ''; + const start = Math.max(0, idx - contextChars); + const end = Math.min(body.length, idx + query.length + contextChars); + let snippet = ''; + if (start > 0) snippet += '...'; + snippet += body.slice(start, end); + if (end < body.length) snippet += '...'; + return snippet; } // Paths for the curated quick links shown in the empty state @@ -26,31 +34,119 @@ const QUICK_LINK_PATHS = [ export function SearchDialog({ isOpen, onClose }) { const [query, setQuery] = useState(''); const [selectedIndex, setSelectedIndex] = useState(-1); + const [loading, setLoading] = useState(false); + const [results, setResults] = useState([]); const inputRef = useRef(null); const itemRefs = useRef([]); const router = useRouter(); - const allPages = flattenNavigation(); + const debounceTimerRef = useRef(null); - // Quick links for empty state - const quickLinks = QUICK_LINK_PATHS - .map(p => allPages.find(page => page.path === p)) - .filter(Boolean); - - // Filter and sort results using fuzzy matching - const trimmedQuery = query.trim(); - const results = trimmedQuery.length > 0 - ? allPages - .filter(p => fuzzyMatch(trimmedQuery, p.title)) - .sort((a, b) => { - const aExact = a.title.toLowerCase().includes(trimmedQuery.toLowerCase()); - const bExact = b.title.toLowerCase().includes(trimmedQuery.toLowerCase()); - if (aExact && !bExact) return -1; - if (!aExact && bExact) return 1; - return 0; + // Load search index on first open + useEffect(() => { + if (isOpen && !searchData && !isLoading) { + isLoading = true; + setLoading(true); + fetch('/search-index.json') + .then(res => res.json()) + .then(data => { + searchData = data; + // Create FlexSearch index + searchIndex = new FlexSearch.Document({ + document: { + id: 'path', + index: ['title', 'description', 'headings', 'body'], + }, + tokenize: 'forward', + }); + // Add all entries to the index — join headings array into string for FlexSearch + data.forEach(entry => { + searchIndex.add({ + ...entry, + headings: Array.isArray(entry.headings) ? entry.headings.join(' ') : (entry.headings || ''), + }); + }); + setLoading(false); + isLoading = false; }) + .catch(err => { + console.error('Failed to load search index:', err); + setLoading(false); + isLoading = false; + }); + } + }, [isOpen]); + + // Quick links for empty state + const quickLinks = searchData + ? QUICK_LINK_PATHS + .map(p => searchData.find(page => page.path === p)) + .filter(Boolean) : []; + // Perform search with debouncing + useEffect(() => { + const trimmedQuery = query.trim(); + + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + + if (!trimmedQuery || !searchIndex || !searchData) { + setResults([]); + return; + } + + debounceTimerRef.current = setTimeout(() => { + // Search the index + const searchResults = searchIndex.search(trimmedQuery, { limit: 20, enrich: true }); + + // Merge and deduplicate results by path, prioritizing field order + const pathMap = new Map(); + const fieldPriority = { title: 1, description: 2, headings: 3, body: 4 }; + + searchResults.forEach(fieldResult => { + const field = fieldResult.field; + const priority = fieldPriority[field] || 999; + + fieldResult.result.forEach(item => { + const path = typeof item === 'object' ? item.id : item; + if (!pathMap.has(path) || pathMap.get(path).priority > priority) { + const entry = searchData.find(e => e.path === path); + if (entry) { + pathMap.set(path, { + ...entry, + priority, + matchedField: field, + }); + } + } + }); + }); + + // Convert to array and sort by priority + const mergedResults = Array.from(pathMap.values()).sort((a, b) => a.priority - b.priority); + + // Generate snippets for body matches + mergedResults.forEach(result => { + if (result.matchedField === 'body' && result.body) { + result.snippet = getSnippet(result.body, trimmedQuery); + } else if (result.description) { + result.snippet = result.description; + } + }); + + setResults(mergedResults); + }, 150); + + return () => { + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + }; + }, [query]); + // Group results by section + const trimmedQuery = query.trim(); const groupedResults = results.reduce((acc, page) => { const section = page.section || 'General'; if (!acc[section]) acc[section] = []; @@ -107,6 +203,27 @@ export function SearchDialog({ isOpen, onClose }) { // Build a flat index counter for refs across groups let refIndex = 0; + // Helper to highlight matched term in snippet + const highlightSnippet = (snippet, query) => { + if (!snippet || !query) return snippet; + const lower = snippet.toLowerCase(); + const queryLower = query.toLowerCase(); + const idx = lower.indexOf(queryLower); + if (idx === -1) return snippet; + + const before = snippet.slice(0, idx); + const match = snippet.slice(idx, idx + query.length); + const after = snippet.slice(idx + query.length); + + return ( + <> + {before} + {match} + {after} + + ); + }; + return (
e.stopPropagation()}> @@ -137,7 +254,10 @@ export function SearchDialog({ isOpen, onClose }) { ESC
- {!hasQuery ? ( + {loading ? ( + /* Loading state */ +
Loading...
+ ) : !hasQuery ? ( /* Empty state: show Quick Links */