diff --git a/.gitignore b/.gitignore index e6738dc..0fba2a6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,24 @@ node_modules/ .next/ out/ dist/ +*.exe +*.dmg +vendor/ +__pycache__/ # Environment .env* !.env.example +.envault/ +*.pem +*.key +*.p12 +*.pfx +credentials.json +service-account*.json +*_secret* +*_token* +*_password* # IDE .vscode/ @@ -20,3 +34,7 @@ dist/ # Logs *.log + +# Large assets (stored in GCS) +blog/public/audio/ +blog/public/video/ diff --git a/CLAUDE.md b/CLAUDE.md index c07c5a6..4758c84 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,8 +16,25 @@ cd blog && npm run dev # Open http://localhost:19197 ``` +## Deploy + +"Deploy" = sync assets to GCS + commit + push. Cloud Build auto-deploys on push. + +```bash +# Sync any new audio/video to GCS +gcloud storage cp blog/public/audio/notes/{slug}/* gs://orchard9-assets/research-notes/audio/notes/{slug}/ +# Then commit and push +``` + ## AI Routing +### Editorial voice +→ [`blog/vision.md`](blog/vision.md) — raw notes, direct tone, no fluff + +Writing feedback → understand it → update CLAUDE.md + +**Style:** flat over nested, show don't hide, inline over accordion, direct lowercase casual + ### For blog development → `blog/CLAUDE.md` diff --git a/Dockerfile b/Dockerfile index be09353..3e78648 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,10 @@ FROM node:20-alpine AS builder WORKDIR /app +# Asset URL for GCS storage +ARG NEXT_PUBLIC_ASSET_BASE_URL=https://storage.googleapis.com/orchard9-assets/research-notes +ENV NEXT_PUBLIC_ASSET_BASE_URL=$NEXT_PUBLIC_ASSET_BASE_URL + # Install pnpm RUN corepack enable && corepack prepare pnpm@latest --activate diff --git a/blog/CLAUDE.md b/blog/CLAUDE.md index d405419..4c2b6d0 100644 --- a/blog/CLAUDE.md +++ b/blog/CLAUDE.md @@ -71,6 +71,16 @@ content/ | `src/components/white-paper/OutlineSection.tsx` | Outline sections | | `src/components/copyable.tsx` | CopyButton, CopyableBlock, ExpandableFile | +## Inline Links + +Reference sidebar items in content.md: +- `[[prompt:id]]` — link to a prompt by its id +- `[[file:name.md]]` — link to a file by its name +- `[[skill:name]]` — link to a skill by its name +- `[[audio:file.m4a]]` — link to an audio file by its name + +Click scrolls to item + highlights for 1.5s. + ## Note meta.yaml Schema ```yaml @@ -90,6 +100,12 @@ filesCreated: - name: filename.md description: What this file is +audioFiles: # optional + - name: filename.m4a + title: "Display title" + description: "What this covers" + source: NotebookLM # optional badge + navigation: prev: null # or { slug, id, title } next: diff --git a/blog/content/notes/003-research-planning/meta.yaml b/blog/content/notes/003-research-planning/meta.yaml index 9ea342d..617ced8 100644 --- a/blog/content/notes/003-research-planning/meta.yaml +++ b/blog/content/notes/003-research-planning/meta.yaml @@ -339,6 +339,12 @@ skillsUsed: - ANNOUNCE wave plan before executing - MAX 3 fix cycles per task, then escalate +audioFiles: + - name: Maxwell_Verifies_AI_With_Thermodynamic_Fingerprints.m4a + title: "Research Overview" + description: "NotebookLM synthesis of 10 research directives" + source: NotebookLM + filesCreated: - name: firecracker-latency-benchmarks.md description: Benchmarking pause/resume latency for thermal emergencies diff --git a/blog/package-lock.json b/blog/package-lock.json index 8712410..67f4121 100644 --- a/blog/package-lock.json +++ b/blog/package-lock.json @@ -17,6 +17,7 @@ "react": "19.2.3", "react-dom": "19.2.3", "react-markdown": "^10.1.0", + "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "tailwind-merge": "^3.4.0" }, @@ -3045,6 +3046,18 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.24.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", @@ -4114,6 +4127,64 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -4141,6 +4212,25 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-whitespace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", @@ -4154,6 +4244,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -4181,6 +4288,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6490,6 +6607,18 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6737,6 +6866,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", @@ -7912,6 +8056,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vfile-message": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", @@ -7926,6 +8084,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/blog/package.json b/blog/package.json index 9d01b09..aebf8b8 100644 --- a/blog/package.json +++ b/blog/package.json @@ -18,6 +18,7 @@ "react": "19.2.3", "react-dom": "19.2.3", "react-markdown": "^10.1.0", + "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "tailwind-merge": "^3.4.0" }, diff --git a/blog/src/app/globals.css b/blog/src/app/globals.css index 01f52aa..0272a10 100644 --- a/blog/src/app/globals.css +++ b/blog/src/app/globals.css @@ -4,165 +4,187 @@ @custom-variant dark (&:is(.dark *)); @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); - --font-serif: Georgia, Cambria, "Times New Roman", serif; - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar: var(--sidebar); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --color-ring: var(--ring); - --color-input: var(--input); - --color-border: var(--border); - --color-destructive: var(--destructive); - --color-accent-foreground: var(--accent-foreground); - --color-accent: var(--accent); - --color-muted-foreground: var(--muted-foreground); - --color-muted: var(--muted); - --color-secondary-foreground: var(--secondary-foreground); - --color-secondary: var(--secondary); - --color-primary-foreground: var(--primary-foreground); - --color-primary: var(--primary); - --color-popover-foreground: var(--popover-foreground); - --color-popover: var(--popover); - --color-card-foreground: var(--card-foreground); - --color-card: var(--card); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --radius-2xl: calc(var(--radius) + 8px); - --radius-3xl: calc(var(--radius) + 12px); - --radius-4xl: calc(var(--radius) + 16px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --font-serif: Georgia, Cambria, "Times New Roman", serif; + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); } /* Research Paper Theme - Light tan/offwhite with dark charcoal */ :root { - --radius: 0.375rem; - /* Warm offwhite/cream background */ - --background: oklch(0.965 0.015 85); - /* Dark charcoal text */ - --foreground: oklch(0.25 0 0); - /* Slightly lighter cream for cards */ - --card: oklch(0.98 0.01 85); - --card-foreground: oklch(0.25 0 0); - --popover: oklch(0.98 0.01 85); - --popover-foreground: oklch(0.25 0 0); - /* Near-black for emphasis */ - --primary: oklch(0.3 0 0); - --primary-foreground: oklch(0.965 0.015 85); - /* Subtle tan for secondary */ - --secondary: oklch(0.92 0.012 85); - --secondary-foreground: oklch(0.3 0 0); - /* Muted tan for borders and backgrounds */ - --muted: oklch(0.92 0.012 85); - --muted-foreground: oklch(0.45 0 0); - /* Warm accent for hovers */ - --accent: oklch(0.9 0.025 85); - --accent-foreground: oklch(0.25 0 0); - /* Destructive - muted red */ - --destructive: oklch(0.577 0.2 25); - /* Subtle warm borders */ - --border: oklch(0.88 0.015 85); - --input: oklch(0.88 0.015 85); - --ring: oklch(0.3 0 0); - /* Chart colors - warm academic tones */ - --chart-1: oklch(0.5 0.12 45); - --chart-2: oklch(0.55 0.1 180); - --chart-3: oklch(0.45 0.08 220); - --chart-4: oklch(0.65 0.15 85); - --chart-5: oklch(0.6 0.14 70); - /* Sidebar */ - --sidebar: oklch(0.96 0.012 85); - --sidebar-foreground: oklch(0.25 0 0); - --sidebar-primary: oklch(0.3 0 0); - --sidebar-primary-foreground: oklch(0.965 0.015 85); - --sidebar-accent: oklch(0.92 0.012 85); - --sidebar-accent-foreground: oklch(0.3 0 0); - --sidebar-border: oklch(0.88 0.015 85); - --sidebar-ring: oklch(0.5 0 0); + --radius: 0.375rem; + /* Warm offwhite/cream background */ + --background: oklch(0.965 0.015 85); + /* Dark charcoal text */ + --foreground: oklch(0.25 0 0); + /* Slightly lighter cream for cards */ + --card: oklch(0.98 0.01 85); + --card-foreground: oklch(0.25 0 0); + --popover: oklch(0.98 0.01 85); + --popover-foreground: oklch(0.25 0 0); + /* Near-black for emphasis */ + --primary: oklch(0.3 0 0); + --primary-foreground: oklch(0.965 0.015 85); + /* Subtle tan for secondary */ + --secondary: oklch(0.92 0.012 85); + --secondary-foreground: oklch(0.3 0 0); + /* Muted tan for borders and backgrounds */ + --muted: oklch(0.92 0.012 85); + --muted-foreground: oklch(0.45 0 0); + /* Warm accent for hovers */ + --accent: oklch(0.9 0.025 85); + --accent-foreground: oklch(0.25 0 0); + /* Destructive - muted red */ + --destructive: oklch(0.577 0.2 25); + /* Subtle warm borders */ + --border: oklch(0.88 0.015 85); + --input: oklch(0.88 0.015 85); + --ring: oklch(0.3 0 0); + /* Chart colors - warm academic tones */ + --chart-1: oklch(0.5 0.12 45); + --chart-2: oklch(0.55 0.1 180); + --chart-3: oklch(0.45 0.08 220); + --chart-4: oklch(0.65 0.15 85); + --chart-5: oklch(0.6 0.14 70); + /* Sidebar */ + --sidebar: oklch(0.96 0.012 85); + --sidebar-foreground: oklch(0.25 0 0); + --sidebar-primary: oklch(0.3 0 0); + --sidebar-primary-foreground: oklch(0.965 0.015 85); + --sidebar-accent: oklch(0.92 0.012 85); + --sidebar-accent-foreground: oklch(0.3 0 0); + --sidebar-border: oklch(0.88 0.015 85); + --sidebar-ring: oklch(0.5 0 0); } /* Dark mode - inverted but still warm */ .dark { - --background: oklch(0.18 0.01 85); - --foreground: oklch(0.92 0.01 85); - --card: oklch(0.22 0.01 85); - --card-foreground: oklch(0.92 0.01 85); - --popover: oklch(0.22 0.01 85); - --popover-foreground: oklch(0.92 0.01 85); - --primary: oklch(0.92 0.01 85); - --primary-foreground: oklch(0.22 0.01 85); - --secondary: oklch(0.28 0.01 85); - --secondary-foreground: oklch(0.92 0.01 85); - --muted: oklch(0.28 0.01 85); - --muted-foreground: oklch(0.65 0.01 85); - --accent: oklch(0.28 0.01 85); - --accent-foreground: oklch(0.92 0.01 85); - --destructive: oklch(0.65 0.18 22); - --border: oklch(0.92 0.01 85 / 12%); - --input: oklch(0.92 0.01 85 / 15%); - --ring: oklch(0.6 0 0); - --chart-1: oklch(0.55 0.2 260); - --chart-2: oklch(0.65 0.15 160); - --chart-3: oklch(0.7 0.16 70); - --chart-4: oklch(0.6 0.22 300); - --chart-5: oklch(0.6 0.2 20); - --sidebar: oklch(0.22 0.01 85); - --sidebar-foreground: oklch(0.92 0.01 85); - --sidebar-primary: oklch(0.55 0.2 260); - --sidebar-primary-foreground: oklch(0.92 0.01 85); - --sidebar-accent: oklch(0.28 0.01 85); - --sidebar-accent-foreground: oklch(0.92 0.01 85); - --sidebar-border: oklch(0.92 0.01 85 / 12%); - --sidebar-ring: oklch(0.6 0 0); + --background: oklch(0.18 0.01 85); + --foreground: oklch(0.92 0.01 85); + --card: oklch(0.22 0.01 85); + --card-foreground: oklch(0.92 0.01 85); + --popover: oklch(0.22 0.01 85); + --popover-foreground: oklch(0.92 0.01 85); + --primary: oklch(0.92 0.01 85); + --primary-foreground: oklch(0.22 0.01 85); + --secondary: oklch(0.28 0.01 85); + --secondary-foreground: oklch(0.92 0.01 85); + --muted: oklch(0.28 0.01 85); + --muted-foreground: oklch(0.65 0.01 85); + --accent: oklch(0.28 0.01 85); + --accent-foreground: oklch(0.92 0.01 85); + --destructive: oklch(0.65 0.18 22); + --border: oklch(0.92 0.01 85 / 12%); + --input: oklch(0.92 0.01 85 / 15%); + --ring: oklch(0.6 0 0); + --chart-1: oklch(0.55 0.2 260); + --chart-2: oklch(0.65 0.15 160); + --chart-3: oklch(0.7 0.16 70); + --chart-4: oklch(0.6 0.22 300); + --chart-5: oklch(0.6 0.2 20); + --sidebar: oklch(0.22 0.01 85); + --sidebar-foreground: oklch(0.92 0.01 85); + --sidebar-primary: oklch(0.55 0.2 260); + --sidebar-primary-foreground: oklch(0.92 0.01 85); + --sidebar-accent: oklch(0.28 0.01 85); + --sidebar-accent-foreground: oklch(0.92 0.01 85); + --sidebar-border: oklch(0.92 0.01 85 / 12%); + --sidebar-ring: oklch(0.6 0 0); } @layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground antialiased; - font-feature-settings: "kern" 1, "liga" 1; - } + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground antialiased; + font-feature-settings: + "kern" 1, + "liga" 1; + } - /* Research paper typography */ - h1, h2, h3, h4, h5, h6 { - font-family: var(--font-serif); - @apply tracking-tight; - } + /* Research paper typography */ + h1, + h2, + h3, + h4, + h5, + h6 { + font-family: var(--font-serif); + @apply tracking-tight; + } - p { - @apply leading-relaxed; - } + p { + @apply leading-relaxed; + } - /* Lists */ - ol { - @apply list-decimal list-inside my-4 space-y-2 pl-4; - } + /* Lists */ + ol { + @apply list-decimal list-outside my-4 space-y-2 pl-6; + } - ul { - @apply list-disc list-inside my-4 space-y-2 pl-4; - } + ul { + @apply list-disc list-outside my-4 space-y-2 pl-6; + } - li { - @apply leading-relaxed; - } + li { + @apply leading-relaxed; + } - /* Slightly larger base font for readability */ - html { - font-size: 17px; - } + /* Slightly larger base font for readability */ + html { + font-size: 17px; + } +} + +/* Sidebar highlight animation */ +@keyframes sidebar-highlight { + 0%, + 100% { + background-color: transparent; + } + 25% { + background-color: hsl(var(--accent)); + } +} + +.sidebar-highlight { + animation: sidebar-highlight 1.5s ease-out; } diff --git a/blog/src/app/maxwell/notes/[slug]/page.tsx b/blog/src/app/maxwell/notes/[slug]/page.tsx index 7eeec60..409317f 100644 --- a/blog/src/app/maxwell/notes/[slug]/page.tsx +++ b/blog/src/app/maxwell/notes/[slug]/page.tsx @@ -1,14 +1,14 @@ import { notFound } from "next/navigation"; -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; import { getAllNoteSlugs, getNoteBySlug } from "@/lib/content"; -import { PageLayout } from "@/components/layout/PageLayout"; import { BackNav } from "@/components/layout/BackNav"; import { NoteHeader } from "@/components/notes/NoteHeader"; import { PromptsSection } from "@/components/notes/PromptsSection"; import { SkillsSection } from "@/components/notes/SkillsSection"; import { FilesSection } from "@/components/notes/FilesSection"; +import { AudioSection } from "@/components/notes/AudioSection"; import { NoteFooter } from "@/components/notes/NoteFooter"; +import { NoteSidebarLayout } from "@/components/notes/NoteSidebar"; +import { NoteContent } from "@/components/notes/NoteContent"; interface NotePageProps { params: Promise<{ slug: string }>; @@ -27,55 +27,36 @@ export default async function NotePage({ params }: NotePageProps) { notFound(); } + // Count resources for the mobile button + const resourceCount = + note.prompts.length + + note.files.length + + (note.skillsUsed?.length || 0) + + (note.audioFiles?.length || 0); + + // Sidebar content + const sidebarContent = ( + <> + + + + + + ); + return ( - + - - - - - - -
- ( -

- {children} -

- ), - h3: ({ children }) => ( -

{children}

- ), - p: ({ children }) =>

{children}

, - ul: ({ children }) => ( -
    {children}
- ), - ol: ({ children }) => ( -
    {children}
- ), - li: ({ children }) =>
  • {children}
  • , - strong: ({ children }) => ( - - {children} - - ), - em: ({ children }) => {children}, - }} - > - {note.content} -
    -
    + -
    + ); } diff --git a/blog/src/components/layout/PageLayout.tsx b/blog/src/components/layout/PageLayout.tsx index abf4e3f..1170c7d 100644 --- a/blog/src/components/layout/PageLayout.tsx +++ b/blog/src/components/layout/PageLayout.tsx @@ -7,7 +7,7 @@ interface PageLayoutProps { export function PageLayout({ children }: PageLayoutProps) { return (
    -
    +
    {children}
    diff --git a/blog/src/components/notes/AudioSection.tsx b/blog/src/components/notes/AudioSection.tsx new file mode 100644 index 0000000..19388e5 --- /dev/null +++ b/blog/src/components/notes/AudioSection.tsx @@ -0,0 +1,69 @@ +"use client"; + +import type { AudioRef } from "@/lib/content"; +import { getAudioUrl } from "@/lib/assets"; +import { SidebarItem } from "./SidebarItem"; + +interface AudioSectionProps { + noteSlug: string; + audioFiles: AudioRef[]; +} + +export function AudioSection({ noteSlug, audioFiles }: AudioSectionProps) { + if (audioFiles.length === 0) { + return null; + } + + return ( +
    +

    Audio

    +
    + {audioFiles.map((audio) => { + const audioPath = getAudioUrl(noteSlug, audio.name); + return ( + +
    +
    + {audio.title} + {audio.source && ( + + {audio.source} + + )} + + open + +
    +

    + {audio.description} +

    + +
    +
    + ); + })} +
    +

    + Made with{" "} + + NotebookLM + + : upload research files → Audio Overview → download .m4a →{" "} + scripts/upload-asset.sh +

    +
    + ); +} diff --git a/blog/src/components/notes/FilesSection.tsx b/blog/src/components/notes/FilesSection.tsx index 7c111b3..6c6da1a 100644 --- a/blog/src/components/notes/FilesSection.tsx +++ b/blog/src/components/notes/FilesSection.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { ExpandableFile } from "@/components/copyable"; +import { SidebarItem } from "./SidebarItem"; import type { NoteFile } from "@/lib/content"; interface FilesSectionProps { @@ -12,23 +13,24 @@ export function FilesSection({ files }: FilesSectionProps) { const [expandedFile, setExpandedFile] = useState(null); return ( -
    +

    Files Created

    {files.length > 0 ? (
    {files.map((file) => ( - - setExpandedFile(expandedFile === file.name ? null : file.name) - } - /> + + + setExpandedFile(expandedFile === file.name ? null : file.name) + } + /> + ))}
    ) : ( diff --git a/blog/src/components/notes/InlineLink.tsx b/blog/src/components/notes/InlineLink.tsx new file mode 100644 index 0000000..cd78927 --- /dev/null +++ b/blog/src/components/notes/InlineLink.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { useSidebar } from "./SidebarContext"; + +interface InlineLinkProps { + type: "prompt" | "file" | "skill" | "audio"; + id: string; + children: React.ReactNode; +} + +export function InlineLink({ type, id, children }: InlineLinkProps) { + const { highlightItem } = useSidebar(); + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + highlightItem(`${type}-${id}`); + }; + + return ( + + ); +} diff --git a/blog/src/components/notes/NoteContent.tsx b/blog/src/components/notes/NoteContent.tsx new file mode 100644 index 0000000..b414657 --- /dev/null +++ b/blog/src/components/notes/NoteContent.tsx @@ -0,0 +1,147 @@ +"use client"; + +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { InlineLink } from "./InlineLink"; + +interface NoteContentProps { + content: string; +} + +// Match [[type:id]] patterns +const INLINE_LINK_REGEX = /\[\[(prompt|file|skill|audio):([^\]]+)\]\]/g; + +interface ContentPart { + type: "text" | "link"; + content: string; + linkType?: "prompt" | "file" | "skill" | "audio"; + linkId?: string; +} + +function parseContentWithLinks(text: string): ContentPart[] { + const parts: ContentPart[] = []; + let lastIndex = 0; + let match; + + // Reset regex + INLINE_LINK_REGEX.lastIndex = 0; + + while ((match = INLINE_LINK_REGEX.exec(text)) !== null) { + // Add text before the match + if (match.index > lastIndex) { + parts.push({ + type: "text", + content: text.slice(lastIndex, match.index), + }); + } + + // Add the link + parts.push({ + type: "link", + content: match[2], + linkType: match[1] as "prompt" | "file" | "skill" | "audio", + linkId: match[2], + }); + + lastIndex = match.index + match[0].length; + } + + // Add remaining text + if (lastIndex < text.length) { + parts.push({ + type: "text", + content: text.slice(lastIndex), + }); + } + + return parts; +} + +function renderTextWithLinks(text: string): React.ReactNode { + const parts = parseContentWithLinks(text); + + if (parts.length === 1 && parts[0].type === "text") { + return text; + } + + return parts.map((part, index) => { + if (part.type === "link") { + return ( + + {part.content} + + ); + } + return part.content; + }); +} + +export function NoteContent({ content }: NoteContentProps) { + return ( +
    + ( +

    + {children} +

    + ), + h3: ({ children }) => ( +

    {children}

    + ), + p: ({ children }) => { + // Process text children for inline links + const processedChildren = processChildren(children); + return

    {processedChildren}

    ; + }, + ul: ({ children }) => ( +
      {children}
    + ), + ol: ({ children }) => ( +
      {children}
    + ), + li: ({ children }) => { + const processedChildren = processChildren(children); + return
  • {processedChildren}
  • ; + }, + strong: ({ children }) => ( + + {children} + + ), + em: ({ children }) => {children}, + }} + > + {content} +
    +
    + ); +} + +function processChildren(children: React.ReactNode): React.ReactNode { + if (typeof children === "string") { + return renderTextWithLinks(children); + } + + if (Array.isArray(children)) { + return children.map((child, index) => { + if (typeof child === "string") { + const result = renderTextWithLinks(child); + // If it's still just a string, return as-is + if (typeof result === "string") { + return result; + } + // If it's an array of parts, wrap in fragment with key + return {result}; + } + return child; + }); + } + + return children; +} diff --git a/blog/src/components/notes/NoteSidebar.tsx b/blog/src/components/notes/NoteSidebar.tsx new file mode 100644 index 0000000..aa56d6f --- /dev/null +++ b/blog/src/components/notes/NoteSidebar.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { ReactNode } from "react"; +import { useSidebar, SidebarProvider } from "./SidebarContext"; + +interface NoteSidebarLayoutProps { + children: ReactNode; + sidebar: ReactNode; + resourceCount: number; +} + +function MobileDrawerButton({ count }: { count: number }) { + const { openDrawer } = useSidebar(); + + return ( + + ); +} + +function MobileDrawer({ children }: { children: ReactNode }) { + const { isDrawerOpen, closeDrawer } = useSidebar(); + + return ( + <> + {/* Backdrop */} +
    + + {/* Drawer */} +
    +
    + + Resources + + +
    +
    {children}
    +
    + + ); +} + +function LayoutInner({ + children, + sidebar, + resourceCount, +}: NoteSidebarLayoutProps) { + return ( +
    + + {sidebar} + +
    + {/* Main content */} +
    + {children} +
    + + {/* Desktop sidebar */} + +
    +
    + ); +} + +export function NoteSidebarLayout(props: NoteSidebarLayoutProps) { + return ( + + + + ); +} diff --git a/blog/src/components/notes/PromptsSection.tsx b/blog/src/components/notes/PromptsSection.tsx index 680e8e6..6139960 100644 --- a/blog/src/components/notes/PromptsSection.tsx +++ b/blog/src/components/notes/PromptsSection.tsx @@ -1,4 +1,7 @@ +"use client"; + import { CopyableBlock } from "@/components/copyable"; +import { SidebarItem } from "./SidebarItem"; import type { Prompt } from "@/lib/content"; interface PromptsSectionProps { @@ -7,17 +10,18 @@ interface PromptsSectionProps { export function PromptsSection({ prompts }: PromptsSectionProps) { return ( -
    +

    Prompts Used

    {prompts.map((prompt) => ( - + + + ))}
    diff --git a/blog/src/components/notes/SidebarContext.tsx b/blog/src/components/notes/SidebarContext.tsx new file mode 100644 index 0000000..9feea67 --- /dev/null +++ b/blog/src/components/notes/SidebarContext.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { + createContext, + useContext, + useState, + useCallback, + ReactNode, +} from "react"; + +interface SidebarContextValue { + isDrawerOpen: boolean; + openDrawer: () => void; + closeDrawer: () => void; + highlightedItem: string | null; + highlightItem: (id: string) => void; +} + +const SidebarContext = createContext(null); + +export function SidebarProvider({ children }: { children: ReactNode }) { + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [highlightedItem, setHighlightedItem] = useState(null); + + const openDrawer = useCallback(() => setIsDrawerOpen(true), []); + const closeDrawer = useCallback(() => setIsDrawerOpen(false), []); + + const highlightItem = useCallback((id: string) => { + // Scroll to the element + const element = document.getElementById(id); + if (element) { + element.scrollIntoView({ behavior: "smooth", block: "center" }); + } + + // Open drawer on mobile + setIsDrawerOpen(true); + + // Set highlight (will trigger animation) + setHighlightedItem(id); + + // Clear highlight after animation completes + setTimeout(() => setHighlightedItem(null), 1500); + }, []); + + return ( + + {children} + + ); +} + +export function useSidebar() { + const context = useContext(SidebarContext); + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider"); + } + return context; +} diff --git a/blog/src/components/notes/SidebarItem.tsx b/blog/src/components/notes/SidebarItem.tsx new file mode 100644 index 0000000..3b24eb8 --- /dev/null +++ b/blog/src/components/notes/SidebarItem.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { ReactNode } from "react"; +import { useSidebar } from "./SidebarContext"; + +interface SidebarItemProps { + id: string; + children: ReactNode; +} + +export function SidebarItem({ id, children }: SidebarItemProps) { + const { highlightedItem } = useSidebar(); + const isHighlighted = highlightedItem === id; + + return ( +
    + {children} +
    + ); +} diff --git a/blog/src/components/notes/SkillsSection.tsx b/blog/src/components/notes/SkillsSection.tsx index a5acbe5..f59b0aa 100644 --- a/blog/src/components/notes/SkillsSection.tsx +++ b/blog/src/components/notes/SkillsSection.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { CopyableBlock } from "@/components/copyable"; +import { SidebarItem } from "./SidebarItem"; export interface SkillUsed { name: string; @@ -20,37 +21,39 @@ export function SkillsSection({ skills }: SkillsSectionProps) { if (skills.length === 0) return null; return ( -
    +

    Skills Used

    {skills.map((skill) => ( -
    -
    - + +
    +
    + +
    + {expandedSkill === skill.name && skill.usage && ( + + )}
    - {expandedSkill === skill.name && skill.usage && ( - - )} -
    + ))}
    diff --git a/blog/src/lib/assets.ts b/blog/src/lib/assets.ts new file mode 100644 index 0000000..e31218b --- /dev/null +++ b/blog/src/lib/assets.ts @@ -0,0 +1,10 @@ +const ASSET_BASE = process.env.NEXT_PUBLIC_ASSET_BASE_URL + || "https://storage.googleapis.com/orchard9-assets/research-notes"; + +export function getAssetUrl(path: string): string { + return `${ASSET_BASE}/${path}`; +} + +export function getAudioUrl(noteSlug: string, filename: string): string { + return getAssetUrl(`audio/notes/${noteSlug}/${filename}`); +} diff --git a/blog/src/lib/content.ts b/blog/src/lib/content.ts index c7b399d..b2b9378 100644 --- a/blog/src/lib/content.ts +++ b/blog/src/lib/content.ts @@ -30,6 +30,13 @@ export interface FileRef { description: string; } +export interface AudioRef { + name: string; + title: string; + description: string; + source?: string; +} + export interface SkillUsed { name: string; command: string; @@ -52,6 +59,7 @@ export interface NoteMeta { prompts: Prompt[]; skillsUsed?: SkillUsed[]; filesCreated: FileRef[]; + audioFiles?: AudioRef[]; navigation: { prev: NoteNavLink | null; next: NoteNavLink | null; @@ -179,3 +187,32 @@ export function getWhitePaperOutline(): WhitePaperOutline { const fileContents = fs.readFileSync(filePath, "utf8"); return yaml.load(fileContents) as WhitePaperOutline; } + +// Inline link parsing for [[type:id]] syntax +const INLINE_LINK_REGEX = /\[\[(prompt|file|skill|audio):([^\]]+)\]\]/g; + +export interface InlineLinkMatch { + type: "prompt" | "file" | "skill" | "audio"; + id: string; + displayText: string; +} + +export function parseInlineLinks(content: string): string { + return content.replace(INLINE_LINK_REGEX, (_match, type, id) => { + // Create a custom element that React can render + return `${id}`; + }); +} + +export function extractInlineLinks(content: string): InlineLinkMatch[] { + const matches: InlineLinkMatch[] = []; + let match; + while ((match = INLINE_LINK_REGEX.exec(content)) !== null) { + matches.push({ + type: match[1] as InlineLinkMatch["type"], + id: match[2], + displayText: match[2], + }); + } + return matches; +} diff --git a/blog/vision.md b/blog/vision.md new file mode 100644 index 0000000..88cc1ac --- /dev/null +++ b/blog/vision.md @@ -0,0 +1,41 @@ +# Vision + +Notes from building things. + +## What This Is + +A project log. I write down what I'm doing as I do it. Prompts, decisions, dead ends, things that worked. + +Sometimes there's audio or video when it's easier to explain that way. + +## Why + +Two reasons: + +1. **For me** — I forget what I did and why. Notes help me pick up where I left off. +2. **For others** — If you're building something similar, maybe this saves you time. + +## Tone + +- Casual +- Concise +- Direct +- No fluff +- No hyperbole + +If something sucked, I'll say it sucked. If something worked, I'll say what worked. + +## Format + +Each project gets a series of notes. Each note covers one session or one phase. Notes include: + +- What I was trying to do +- The prompts I used +- What happened +- What I learned + +Code snippets, screenshots, audio/video when useful. + +## Not a Tutorial + +This isn't polished educational content. It's raw notes. Some things will be wrong. Some things will be incomplete. That's fine. diff --git a/scripts/upload-asset.sh b/scripts/upload-asset.sh new file mode 100755 index 0000000..fdb45d3 --- /dev/null +++ b/scripts/upload-asset.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e +BUCKET="orchard9-assets" +PREFIX="research-notes" + +LOCAL_FILE="$1" +REMOTE_PATH="$2" + +[[ -z "$LOCAL_FILE" || -z "$REMOTE_PATH" ]] && { + echo "Usage: $0 " + echo "Example: $0 ./audio.m4a audio/notes/003-research-planning/audio.m4a" + exit 1 +} + +gcloud storage cp "$LOCAL_FILE" "gs://${BUCKET}/${PREFIX}/${REMOTE_PATH}" +echo "https://storage.googleapis.com/${BUCKET}/${PREFIX}/${REMOTE_PATH}"