Get Glyph
Warning This documentation is still a work in progress. Some details may be out of date depending on the version of Glyph you are using, but it is being actively reviewed and improved.
Documentation AI Assistant Development Licensing

Documentation

React Components

Frontend component architecture and organization

Component Structure

Glyph’s frontend is organized into logical component groups:

src/components/
├── app/              # App shell & chrome
├── editor/           # TipTap markdown editor
├── ai/               # AI chat panel
├── filetree/         # File browser sidebar
├── preview/          # File preview pane
├── tasks/            # Task list views
├── database/         # Database table/board views
├── settings/         # Settings panes
├── licensing/        # License activation
└── ui/               # shadcn/ui primitives

App Shell Components

AppShell

Location: src/components/app/AppShell.tsx

Root layout component that orchestrates the main UI:

export function AppShell() {
  return (
    <div className="app-shell">
      <Sidebar />           {/* Left sidebar: file tree, tags, etc. */}
      <MainContent />       {/* Center: editor, preview, database views */}
      <CommandPalette />    {/* Cmd+K search */}
      <KeyboardShortcutsHelp />  {/* ? help modal */}
    </div>
  );
}

Location: src/components/app/Sidebar.tsx

Navigable sidebar with multiple panes:

export function Sidebar() {
  const { activePane } = useUIContext();
  
  return (
    <aside>
      <SidebarHeader />  {/* Logo, new note button */}
      <SidebarContent>
        {activePane === 'files' && <FileTreePane />}
        {activePane === 'tags' && <TagsPane />}
        {activePane === 'ai' && <AIPanel />}
        {activePane === 'tasks' && <TasksPane />}
      </SidebarContent>
    </aside>
  );
}
const { setActivePane } = useUIContext();

<IconButton onClick={() => setActivePane('files')}>
  <FileIcon />
</IconButton>

MainContent

Location: src/components/app/MainContent.tsx

Tab-based content area:

export function MainContent() {
  const { activeFilePath, activePreviewPath } = useFileTreeContext();
  const { activeViewDoc } = useViewContext();
  
  return (
    <main>
      <TabBar />  {/* File tabs */}
      
      {activeFilePath?.endsWith('.md') && (
        <MarkdownEditorPane path={activeFilePath} />
      )}
      
      {activePreviewPath && (
        <FilePreviewPane path={activePreviewPath} />
      )}
      
      {activeViewDoc && (
        <ViewRenderer doc={activeViewDoc} />
      )}
    </main>
  );
}

CommandPalette

Location: src/components/app/CommandPalette.tsx

Cmd+K quick actions:

import { Command } from 'cmdk';

export function CommandPalette() {
  const { isOpen, setIsOpen } = useUIContext();
  const [query, setQuery] = useState('');
  
  return (
    <Command.Dialog open={isOpen} onOpenChange={setIsOpen}>
      <Command.Input
        placeholder="Search notes, commands..."
        value={query}
        onValueChange={setQuery}
      />
      
      <Command.List>
        <CommandSearchResults query={query} />
        
        <Command.Group heading="Actions">
          <Command.Item onSelect={handleNewNote}>
            <PlusIcon /> New Note
          </Command.Item>
          <Command.Item onSelect={handleOpenSettings}>
            <SettingsIcon /> Settings
          </Command.Item>
        </Command.Group>
      </Command.List>
    </Command.Dialog>
  );
}

Editor Components

CanvasNoteInlineEditor

Location: src/components/editor/CanvasNoteInlineEditor.tsx

TipTap-based markdown editor:

import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { Markdown } from '@tiptap/extension-markdown';
import { WikiLink } from './extensions/wikiLink';

export function CanvasNoteInlineEditor({ path }: { path: string }) {
  const [doc, setDoc] = useState<TextFileDoc | null>(null);
  
  const editor = useEditor({
    extensions: [
      StarterKit,
      Markdown,
      WikiLink,
      // ... more extensions
    ],
    content: doc?.text || '',
    onUpdate: ({ editor }) => {
      debouncedSave(editor.getText());
    },
  });
  
  return (
    <div className="editor-container">
      <EditorRibbon editor={editor} />
      <EditorContent editor={editor} />
    </div>
  );
}
const debouncedSave = useMemo(
  () => debounce(async (text: string) => {
    try {
      setSaveState('saving');
      await invoke('space_write_text', {
        path,
        text,
        base_mtime_ms: doc?.mtime_ms,
      });
      setSaveState('saved');
    } catch (err) {
      setSaveState('error');
      toast.error('Failed to save');
    }
  }, 500),
  [path, doc?.mtime_ms]
);

EditorRibbon

Location: src/components/editor/EditorRibbon.tsx

Formatting toolbar:

export function EditorRibbon({ editor }: { editor: Editor | null }) {
  if (!editor) return null;
  
  return (
    <div className="editor-ribbon">
      <MotionButton
        onClick={() => editor.chain().focus().toggleBold().run()}
        data-active={editor.isActive('bold')}
      >
        <BoldIcon />
      </MotionButton>
      
      <MotionButton
        onClick={() => editor.chain().focus().toggleItalic().run()}
        data-active={editor.isActive('italic')}
      >
        <ItalicIcon />
      </MotionButton>
      
      <RibbonLinkPopover editor={editor} />
      
      {/* ... more buttons */}
    </div>
  );
}

NotePropertiesPanel

Location: src/components/editor/NotePropertiesPanel.tsx

Frontmatter editor:

export function NotePropertiesPanel({ path }: { path: string }) {
  const [properties, setProperties] = useState<NoteProperty[]>([]);
  
  const handleAddProperty = () => {
    setProperties([...properties, {
      key: '',
      kind: 'text',
      value_text: null,
      value_bool: null,
      value_list: [],
    }]);
  };
  
  const handleSave = async () => {
    const frontmatter = await invoke('note_frontmatter_render_properties', {
      properties,
    });
    
    // Update note with new frontmatter...
  };
  
  return (
    <div className="properties-panel">
      {properties.map((prop, i) => (
        <NotePropertyRow
          key={i}
          property={prop}
          onChange={(updated) => updateProperty(i, updated)}
        />
      ))}
      
      <Button onClick={handleAddProperty}>
        <PlusIcon /> Add Property
      </Button>
    </div>
  );
}

File Tree Components

FileTreePane

Location: src/components/filetree/FileTreePane.tsx

Recursive file browser:

export function FileTreePane() {
  const { rootEntries, expandedDirs } = useFileTreeContext();
  const { loadDir, toggleDir, openFile } = useFileTree(/* deps */);
  
  useEffect(() => {
    loadDir(''); // Load root
  }, []);
  
  return (
    <div className="file-tree">
      {rootEntries.map(entry => (
        entry.kind === 'dir' ? (
          <FileTreeDirItem
            key={entry.rel_path}
            entry={entry}
            isExpanded={expandedDirs.has(entry.rel_path)}
            onToggle={() => toggleDir(entry.rel_path)}
          />
        ) : (
          <FileTreeFileItem
            key={entry.rel_path}
            entry={entry}
            onClick={() => openFile(entry.rel_path)}
          />
        )
      ))}
    </div>
  );
}

FileTreeDirItem

Location: src/components/filetree/FileTreeDirItem.tsx

Collapsible directory:

export function FileTreeDirItem({
  entry,
  isExpanded,
  onToggle,
}: FileTreeDirItemProps) {
  const { childrenByDir } = useFileTreeContext();
  const children = childrenByDir[entry.rel_path] || [];
  
  return (
    <div className="dir-item">
      <button onClick={onToggle}>
        <ChevronIcon rotation={isExpanded ? 90 : 0} />
        <FolderIcon />
        {entry.name}
      </button>
      
      {isExpanded && (
        <div className="dir-children">
          {children.map(child => (
            child.kind === 'dir' ? (
              <FileTreeDirItem key={child.rel_path} entry={child} />
            ) : (
              <FileTreeFileItem key={child.rel_path} entry={child} />
            )
          ))}
        </div>
      )}
    </div>
  );
}

AI Components

AIPanel

Location: src/components/ai/AIPanel.tsx

AI chat sidebar:

export function AIPanel() {
  const { messages, sendMessage, isStreaming } = useRigChat();
  
  return (
    <div className="ai-panel">
      <ModelSelector />  {/* GPT-4, Claude, etc. */}
      
      <AIChatThread messages={messages} />
      
      <AIComposer
        onSend={sendMessage}
        disabled={isStreaming}
      />
      
      {isStreaming && <AIToolTimeline />}
    </div>
  );
}

AIChatThread

Location: src/components/ai/AIChatThread.tsx

Message list:

export function AIChatThread({ messages }: { messages: AiMessage[] }) {
  const scrollRef = useRef<HTMLDivElement>(null);
  
  useEffect(() => {
    scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages.length]);
  
  return (
    <div className="chat-thread">
      {messages.map((msg, i) => (
        <div
          key={i}
          className={cn('message', msg.role)}
        >
          {msg.role === 'user' ? <UserAvatar /> : <AIAvatar />}
          <AIMessageMarkdown content={msg.content} />
        </div>
      ))}
      <div ref={scrollRef} />
    </div>
  );
}

Database Components

DatabasePane

Location: src/components/database/DatabasePane.tsx

Database view (table or board):

export function DatabasePane({ path }: { path: string }) {
  const { config, rows, loading } = useDatabaseNote(path);
  
  return (
    <div className="database-pane">
      <DatabaseToolbar config={config} />
      
      {config.view.layout === 'table' ? (
        <DatabaseTable config={config} rows={rows} />
      ) : (
        <DatabaseBoard config={config} rows={rows} />
      )}
    </div>
  );
}

DatabaseTable

Location: src/components/database/DatabaseTable.tsx

TanStack Table:

import { useReactTable, getCoreRowModel } from '@tanstack/react-table';

export function DatabaseTable({ config, rows }: DatabaseTableProps) {
  const table = useReactTable({
    data: rows,
    columns: config.columns.map(col => ({
      id: col.id,
      header: col.label,
      cell: ({ row }) => (
        <DatabaseCell
          column={col}
          row={row.original}
          onChange={(value) => handleCellUpdate(row.original, col, value)}
        />
      ),
    })),
    getCoreRowModel: getCoreRowModel(),
  });
  
  return (
    <table>
      <thead>
        {table.getHeaderGroups().map(headerGroup => (
          <tr key={headerGroup.id}>
            {headerGroup.headers.map(header => (
              <th key={header.id}>{header.column.columnDef.header}</th>
            ))}
          </tr>
        ))}
      </thead>
      <tbody>
        {table.getRowModel().rows.map(row => (
          <tr key={row.id}>
            {row.getVisibleCells().map(cell => (
              <td key={cell.id}>
                {cell.column.columnDef.cell(cell.getContext())}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

UI Primitives

shadcn/ui Components

Location: src/components/ui/shadcn/

Accessible Radix UI-based components:

  • Button - Buttons with variants
  • Dialog - Modals
  • Popover - Floating menus
  • DropdownMenu - Context menus
  • Input - Text inputs
  • Tabs - Tab navigation
  • Table - Semantic tables
  • ScrollArea - Custom scrollbars

Motion Components

Location: src/components/ui/animations.ts

Animated wrappers:

import { motion } from 'motion/react';

export const MotionButton = motion.button;
export const MotionPanel = motion.div;

export const fadeIn = {
  initial: { opacity: 0 },
  animate: { opacity: 1 },
  exit: { opacity: 0 },
};

export const slideIn = {
  initial: { x: -20, opacity: 0 },
  animate: { x: 0, opacity: 1 },
  exit: { x: 20, opacity: 0 },
};

Usage:

<MotionPanel {...fadeIn}>
  Content appears with fade
</MotionPanel>

Component Patterns

Compound Components

// Parent manages state, children consume via context
export function Accordion({ children }) {
  const [openId, setOpenId] = useState<string | null>(null);
  
  return (
    <AccordionContext.Provider value={{ openId, setOpenId }}>
      {children}
    </AccordionContext.Provider>
  );
}

Accordion.Item = function AccordionItem({ id, children }) {
  const { openId, setOpenId } = useAccordionContext();
  const isOpen = openId === id;
  
  return (
    <div>
      <button onClick={() => setOpenId(isOpen ? null : id)}>
        {children}
      </button>
    </div>
  );
};

Render Props

interface FileListProps {
  files: FsEntry[];
  renderFile: (file: FsEntry) => ReactNode;
}

export function FileList({ files, renderFile }: FileListProps) {
  return (
    <div>
      {files.map(file => (
        <div key={file.rel_path}>
          {renderFile(file)}
        </div>
      ))}
    </div>
  );
}

// Usage
<FileList
  files={entries}
  renderFile={(file) => (
    <div>{file.name}</div>
  )}
/>

Custom Hooks as Logic

function useFileTreeItem(entry: FsEntry) {
  const { openFile, renameFile, deleteFile } = useFileTree(/* deps */);
  const [isRenaming, setIsRenaming] = useState(false);
  
  const handleRename = async (newName: string) => {
    await renameFile(entry.rel_path, newName);
    setIsRenaming(false);
  };
  
  return {
    isRenaming,
    startRename: () => setIsRenaming(true),
    handleRename,
    handleDelete: () => deleteFile(entry.rel_path),
    handleOpen: () => openFile(entry.rel_path),
  };
}

Next Steps