├── src ├── vite-env.d.ts ├── components │ ├── util │ │ ├── suspense.tsx │ │ └── error.tsx │ ├── ui │ │ ├── index.ts │ │ ├── collapsible.tsx │ │ ├── kbd.tsx │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── theme-toggle.tsx │ │ ├── checkbox.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ └── command.tsx │ ├── renderer │ │ ├── MissingDataRenderer.tsx │ │ ├── BreadcrumbRenderer.tsx │ │ ├── DynamicBlockRenderer.tsx │ │ ├── CodeMirrorRendererBlockRenderer.tsx │ │ ├── RendererBlockRenderer.tsx │ │ ├── PanelRenderer.tsx │ │ ├── VideoPlayerRenderer.tsx │ │ ├── CodeMirrorContentRenderer.tsx │ │ ├── TopLevelRenderer.tsx │ │ ├── LayoutRenderer.tsx │ │ ├── MarkdownContentRenderer.tsx │ │ └── TextAreaContentRenderer.tsx │ ├── markdown │ │ └── VideoTimeStamp.tsx │ ├── Header.tsx │ ├── Breadcrumbs.tsx │ ├── BlockComponent.tsx │ ├── Login.tsx │ ├── settings │ │ └── OpenRouterSettings.tsx │ ├── CommandPalette.tsx │ ├── BlockEditor.tsx │ └── GenerateRendererDialog.tsx ├── test │ └── setup.ts ├── utils │ ├── react.tsx │ ├── types.ts │ ├── time.ts │ ├── async.ts │ ├── object.ts │ ├── test │ │ ├── utils.test.ts │ │ ├── templateLiterals.test.ts │ │ ├── array.test.ts │ │ ├── backlinkAutocomplete.test.ts │ │ ├── referenceParser.test.ts │ │ └── markdownParser.test.helpers.ts │ ├── paste.ts │ ├── array.ts │ ├── templateLiterals.ts │ ├── state.ts │ ├── dom.ts │ ├── ClientLocalSettings.ts │ ├── referenceParser.ts │ ├── backlinkAutocomplete.ts │ ├── copy.ts │ └── codemirror.ts ├── lib │ └── utils.ts ├── types │ └── ast.d.ts ├── markdown │ ├── test │ │ └── remark-timestamps.test.ts │ └── remark-timestamps.ts ├── data │ ├── repoInstance.ts │ ├── automerge.ts │ ├── repo.ts │ ├── aliasUtils.ts │ ├── properties.ts │ ├── test │ │ └── aliasUtils.test.ts │ └── globalState.ts ├── main.tsx ├── context │ ├── block.tsx │ └── repo.tsx ├── minimal-editor.tsx ├── App.tsx ├── hooks │ ├── block.ts │ ├── useDynamicComponent.tsx │ └── useRendererRegistry.tsx ├── initData.ts ├── shortcuts │ ├── useActionContext.ts │ └── types.ts ├── types.ts ├── assets │ └── react.svg └── index.css ├── postcss.config.js ├── tsconfig.json ├── .prettierrc.js ├── .gitignore ├── codebuff.json ├── components.json ├── .github └── workflows │ └── run-tests.yml ├── vitest.config.ts ├── tsconfig.node.json ├── minimal-editor.html ├── index.html ├── tsconfig.app.json ├── eslint.config.js ├── public └── vite.svg ├── README.md ├── tailwind.config.js ├── package.json ├── knowledge.md └── vite.config.ts /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/components/util/suspense.tsx: -------------------------------------------------------------------------------- 1 | export const SuspenseFallback = () =>
Loading...
2 | -------------------------------------------------------------------------------- /src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | 3 | // Add any global test setup here 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/react.tsx: -------------------------------------------------------------------------------- 1 | import { useMedia } from 'react-use' 2 | 3 | export const useIsMobile = () => { 4 | return useMedia('(max-width: 767px)', false); 5 | } 6 | -------------------------------------------------------------------------------- /src/components/util/error.tsx: -------------------------------------------------------------------------------- 1 | export function FallbackComponent({error}: { error: Error }) { 2 | return
Something went wrong: {error.message}
3 | } 4 | -------------------------------------------------------------------------------- /src/components/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./button" 2 | export * from "./card" 3 | export * from "./collapsible" 4 | export * from "./input" 5 | export * from "./label" 6 | export * from "./theme-toggle" 7 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://prettier.io/docs/en/configuration.html 3 | * @type {import("prettier").Config} 4 | */ 5 | const config = { 6 | trailingComma: "all", 7 | tabWidth: 2, 8 | semi: false, 9 | singleQuote: true, 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export function isNotNullish(value: T | null | undefined): value is T { 2 | return value !== null && value !== undefined 3 | } 4 | 5 | /** 6 | * Makes specified keys of T optional. 7 | */ 8 | export type Optional = Omit & Partial> 9 | -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | /** Convert "HH:MM:SS(.mmm)" or "MM:SS" to seconds as float. */ 2 | export function hmsToSeconds(hms: string): number { 3 | const parts = hms.split(':').map(Number); 4 | const [h, m, s] = parts.length === 3 ? parts : [0, parts[0], parts[1]]; 5 | return h * 3600 + m * 60 + s; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/async.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a Promise that resolves after a specified delay 3 | * @param {number} ms - Delay in milliseconds 4 | * @returns {Promise} A promise that resolves after the specified delay 5 | */ 6 | export const delay = (ms: number): Promise => 7 | new Promise(resolve => setTimeout(resolve, ms)) 8 | -------------------------------------------------------------------------------- /src/components/renderer/MissingDataRenderer.tsx: -------------------------------------------------------------------------------- 1 | import { BlockRendererProps } from "@/types" 2 | 3 | export const MissingDataRenderer = () =>
Loading block...
4 | 5 | MissingDataRenderer.canRender = ({block}: BlockRendererProps) => !block?.dataSync() 6 | MissingDataRenderer.priority = () => 1 7 | -------------------------------------------------------------------------------- /src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | 4 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 5 | 6 | const Collapsible = CollapsiblePrimitive.Root 7 | 8 | const CollapsibleTrigger = CollapsiblePrimitive.Trigger 9 | 10 | const CollapsibleContent = CollapsiblePrimitive.Content 11 | 12 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 13 | -------------------------------------------------------------------------------- /src/utils/object.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Removes all properties with undefined values from an object 3 | * @param obj The object to filter 4 | * @returns A new object with all undefined properties removed 5 | */ 6 | export const removeUndefined = (obj: T): Partial => (Object.fromEntries( 7 | Object.entries(obj).filter(([, value]) => value !== undefined) 8 | ) as Partial) 9 | 10 | -------------------------------------------------------------------------------- /src/types/ast.d.ts: -------------------------------------------------------------------------------- 1 | import {Literal} from 'mdast'; 2 | 3 | export interface Timestamp extends Literal { 4 | type: 'timestamp'; 5 | data: { 6 | hName: 'time-stamp'; // hint for mdast‑to‑hast 7 | hProperties: {hms: string}; 8 | hChildren: [{type: 'text'; value: string}]; 9 | }; 10 | } 11 | 12 | declare module 'mdast' { 13 | interface RootContentMap { timestamp: Timestamp } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # Local Netlify folder 27 | .netlify 28 | -------------------------------------------------------------------------------- /src/components/renderer/BreadcrumbRenderer.tsx: -------------------------------------------------------------------------------- 1 | import { BlockRendererProps } from '@/types.ts' 2 | import { MarkdownContentRenderer } from '@/components/renderer/MarkdownContentRenderer.tsx' 3 | 4 | export const BreadcrumbRenderer = (props: BlockRendererProps) => 5 | 6 | BreadcrumbRenderer.canRender = ({context} : BlockRendererProps) => !!context?.isBreadcrumb 7 | BreadcrumbRenderer.priority = () => 10 8 | -------------------------------------------------------------------------------- /codebuff.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Template configuration for this project. See https://www.codebuff.com/config for all options.", 3 | "startupProcesses": [ 4 | { 5 | "name": "dev", 6 | "command": "yarn dev", 7 | "stdoutFile": "logs/dev.log" 8 | } 9 | ], 10 | "fileChangeHooks": [ 11 | { 12 | "name": "typecheck", 13 | "command": "yarn run compile" 14 | } 15 | ], 16 | "maxAgentSteps": 2 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { cn } from '@/lib/utils' 3 | 4 | describe('cn utility', () => { 5 | it('should merge class names correctly', () => { 6 | const result = cn('class1', 'class2', { 'class3': true, 'class4': false }) 7 | expect(result).toContain('class1') 8 | expect(result).toContain('class2') 9 | expect(result).toContain('class3') 10 | expect(result).not.toContain('class4') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/markdown/test/remark-timestamps.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, describe, afterEach } from 'vitest' 2 | import { TS_RE } from '../remark-timestamps' 3 | 4 | describe('timestamp regex', () => { 5 | afterEach(() => { 6 | // apparently running .test on /g regex mutates it =\ 7 | TS_RE.lastIndex = 0 8 | }) 9 | it.each(['0:30', '1:23', '12:34', '1:23:45', '10:03:04.500'])( 10 | '%s should match', 11 | (str) => expect(TS_RE.test(str)).toBe(true) 12 | ); 13 | }); 14 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /src/components/ui/kbd.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { cn } from '@/lib/utils' 3 | 4 | const Kbd = ({className, children, ...props}: React.HTMLAttributes) => { 5 | return ( 6 | 13 | {children} 14 | 15 | ) 16 | } 17 | 18 | export { Kbd } 19 | -------------------------------------------------------------------------------- /src/data/repoInstance.ts: -------------------------------------------------------------------------------- 1 | import { Repo as AutomergeRepo } from '@automerge/automerge-repo' 2 | import { BrowserWebSocketClientAdapter } from '@automerge/automerge-repo-network-websocket' 3 | import { IndexedDBStorageAdapter } from '@automerge/automerge-repo-storage-indexeddb' 4 | import { UndoRedoManager } from '@onsetsoftware/automerge-repo-undo-redo' 5 | 6 | export const automergeRepo = new AutomergeRepo({ 7 | network: [new BrowserWebSocketClientAdapter('wss://sync.automerge.org')], 8 | storage: new IndexedDBStorageAdapter(), 9 | }) 10 | 11 | export const undoRedoManager = new UndoRedoManager() 12 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | repository_dispatch: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Node.js 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: '20.x' 22 | 23 | - name: Install dependencies 24 | run: yarn install 25 | 26 | - name: Run tests 27 | run: yarn run test 28 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import react from '@vitejs/plugin-react' 3 | import { resolve } from 'path' 4 | 5 | export default defineConfig({ 6 | plugins: [react()], 7 | test: { 8 | environment: 'jsdom', 9 | globals: true, 10 | setupFiles: ['./src/test/setup.ts'], 11 | include: ['**/*.{test,spec}.{ts,tsx}'], 12 | coverage: { 13 | reporter: ['text', 'json', 'html'], 14 | exclude: ['node_modules/', 'src/test/setup.ts'] 15 | } 16 | }, 17 | resolve: { 18 | alias: { 19 | '@': resolve(__dirname, './src') 20 | } 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /src/components/renderer/DynamicBlockRenderer.tsx: -------------------------------------------------------------------------------- 1 | import {ErrorBoundary} from 'react-error-boundary' 2 | import {useDynamicComponent} from '../../hooks/useDynamicComponent.tsx' 3 | import { Block } from '../../data/block.ts' 4 | 5 | function FallbackComponent({error}: { error: Error }) { 6 | return
Something went wrong: {error.message}
7 | } 8 | 9 | export function DynamicBlockRenderer({code, block}: { code: string, block: Block }) { 10 | const DynamicComp = useDynamicComponent(code) 11 | 12 | return ( 13 | 14 | {DynamicComp && } 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /minimal-editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Minimal Editor 7 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Knowledge medium 8 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | export type TextareaProps = React.TextareaHTMLAttributes 6 | 7 | const Textarea = React.forwardRef( 8 | ({className, ...props}, ref) => { 9 | return ( 10 |