Skip to content

4. Ui Component Styling

FerrisMind edited this page Sep 10, 2025 · 1 revision

UI Component Styling

Update Summary

Changes Made

  • Updated documentation to reflect changes in stylelint configuration
  • Added information about the current linting status and future configuration plans
  • Updated section sources to include the LINT_REMAINING.md file
  • Enhanced documentation on callout styling with new warning message classes and responsive design adjustments
  • Added details about responsive font sizes and padding adjustments for mobile devices
  • Updated responsive design section with specific media query implementations

Table of Contents

  1. Introduction
  2. CSS Architecture and Theme System
  3. Markdown Component Styling
  4. CodeMirror Component Styling
  5. Responsive Design and Accessibility
  6. Theme Integration and Customization
  7. Conclusion

Introduction

This document provides comprehensive documentation for the UI component styling system in the Oxide-Lab repository. It covers the styling implementation for markdown content (headings, lists, tables, blockquotes) and CodeMirror components, including responsive design considerations, accessibility features, and theme integration. The documentation details the CSS architecture, customization options, and the integration between JavaScript/TypeScript components and CSS styling.

CSS Architecture and Theme System

The styling system is built on a comprehensive CSS architecture that utilizes CSS custom properties (variables) for theming, Svelte component styling, and external CSS files for global and component-specific styles. The system supports both light and dark themes through CSS media queries and JavaScript-based theme detection.

``mermaid graph TD A[Global CSS Variables] --> B[Theme System] A --> C[Component Styling] B --> D[Light Theme] B --> E[Dark Theme] C --> F[Markdown Components] C --> G[CodeMirror Components] C --> H[Progress Indicators] I[CSS Files] --> A J[JavaScript/TypeScript] --> B K[Svelte Components] --> C


**Diagram sources**
- [app.css](file://src/app.css)
- [markdown.css](file://src/lib/chat/styles/markdown.css)
- [progress.css](file://src/lib/chat/styles/progress.css)

**Section sources**
- [app.css](file://src/app.css)

### Global CSS Variables and Theme System
The application uses a comprehensive set of CSS custom properties defined in the `:root` selector for consistent theming across components. These variables define colors, spacing, shadows, and other visual properties that can be easily customized.

The theme system supports both light and dark modes, with the dark theme activated automatically based on the user's system preference via the `prefers-color-scheme` media query. The CSS variables are redefined in the media query to provide appropriate values for the dark theme.

css :root { --bg: #f7f5f2; --card: #ffffff; --panel-bg: #f8f9fa; --text: #2b2a29; --muted: #6d6a6a; --border-color: #e8e6e3; --accent: #3b82f6; --accent-2: #1d4ed8; --panel-alt-bg: #f8f9fa; --shadow: 0 4px 20px rgba(0,0,0,0.06); --shadow-hover: 0 8px 30px rgba(0,0,0,0.12); --content-gap: 8px; --content-gap-top: 8px; --code-bg: var(--panel-bg); --code-fg: var(--text); --code-keyword: var(--accent-2); --code-string: #0e814a; --code-title: #995500; --code-number: #b45309; --code-variable: #6d28d9; --code-tag: #2563eb; --code-comment: var(--muted); }

@media (prefers-color-scheme: dark) { :root { --bg: #1a1a1a; --card: #2d2d2d; --panel-bg: #252525; --text: #ffffff; --muted: #a0a0a0; --border-color: #404040; --panel-alt-bg: #252525; --code-bg: var(--panel-bg); --code-fg: var(--text); --code-keyword: #93c5fd; --code-string: #86efac; --code-title: #fcd34d; --code-number: #fdba74; --code-variable: #c4b5fd; --code-tag: #93c5fd; --code-comment: #9ca3af; } }


The system also includes variables for syntax highlighting in code blocks, ensuring consistent color schemes across different code languages and themes.

## Markdown Component Styling

The markdown styling system is implemented through a combination of JavaScript/TypeScript processing and CSS styling. The system uses the `marked` library for markdown parsing, with custom extensions for syntax highlighting and security sanitization.

``mermaid
sequenceDiagram
participant Markdown as Markdown Text
participant Renderer as Markdown Renderer
participant Sanitizer as DOMPurify
participant Highlighter as Highlight.js
participant CSS as CSS Styling
Markdown->>Renderer : Raw Markdown Input
Renderer->>Renderer : Process Callouts
Renderer->>Renderer : Parse Markdown to HTML
Renderer->>Highlighter : Apply Syntax Highlighting
Highlighter-->>Renderer : Highlighted HTML
Renderer->>Sanitizer : Sanitize HTML
Sanitizer-->>Renderer : Clean HTML
Renderer-->>CSS : Apply CSS Classes
CSS-->>Output : Styled Markdown Output

Diagram sources

  • markdown.ts
  • markdown.css

Section sources

  • markdown.ts
  • markdown.css
  • callouts.css

Markdown Parsing and Processing

The markdown rendering is handled by the renderMarkdownToSafeHtml function in markdown.ts, which processes markdown text through several stages:

  1. Normalization: Convert Windows-style line endings to Unix-style
  2. Callout Processing: Handle GitHub-style callouts (NOTE, TIP, WARNING, etc.)
  3. Markdown Parsing: Convert markdown to HTML using the marked library
  4. Syntax Highlighting: Apply syntax highlighting using highlight.js
  5. Sanitization: Clean the HTML output using DOMPurify to prevent XSS attacks

The function includes special handling for markdown code blocks that contain markdown syntax, allowing for nested markdown examples.

export function renderMarkdownToSafeHtml(markdownText: string): string {
  try {
    let input = markdownText ?? '';
    input = input.replace(/\r\n?/g, '\n');
    
    input = processCallouts(input);

    let enhanced = input.replace(/```
(?:markdown|md|gfm)\s*\n([\s\S]*?)
```/gi, (_m, inner) => inner);
    enhanced = enhanced.replace(/~~~(?:markdown|md|gfm)\s*\n([\s\S]*?)~~~/gi, (_m, inner) => inner);
    
    if (/^```
(?:markdown|md|gfm)\s*$/im.test(enhanced)) {
      // Handle unclosed markdown code blocks
      const lines = enhanced.split(/\n/);
      let inMdFence = false;
      const out: string[] = [];
      for (const line of lines) {
        if (/^
```(?:markdown|md|gfm)\s*$/i.test(line)) {
          inMdFence = true;
          continue;
        }
        if (inMdFence && /^```
+\s*$/.test(line)) {
          inMdFence = false;
          continue;
        }
        out.push(line);
      }
      enhanced = out.join('\n');
    }
    
    const dirty = (typeof (marked as any).parse === 'function'
      ? (marked as any).parse(enhanced)
      : (marked as any)(enhanced)) as string;
      
    const sanitized = typeof window !== 'undefined'
      ? DOMPurify.sanitize(dirty, {
          ALLOWED_TAGS: [
            'h1','h2','h3','h4','h5','h6','p','br','hr','div','section','article','aside',
            'nav','header','footer','main','address',
            'strong','em','b','i','u','s','mark','small','del','ins','sub','sup',
            'ul','ol','li','dl','dt','dd',
            'blockquote','code','pre','kbd','samp','var',
            'a','img','figure','figcaption','picture','source',
            'table','thead','tbody','tfoot','tr','th','td','caption','colgroup','col',
            'details','summary',
            'span','abbr','dfn','q','cite','time','data','output',
            'math','mi','mo','mn','ms','mtext','mrow','msup','msub','mfrac','msqrt','mroot',
            'ruby','rt','rp',
            'svg','path',
            'wbr','bdi','bdo'
          ],
          ALLOWED_ATTR: [
            'href','title','target','rel','download','hreflang',
            'src','alt','width','height','sizes','srcset','loading','decoding',
            'id','name','class','lang','dir','translate',
            'style','data-*','content',
            'aria-*','role','tabindex','accesskey',
            'colspan','rowspan','scope','headers','summary',
            'type','value','placeholder','readonly','disabled','checked',
            'min','max','step','pattern','required','autocomplete',
            'datetime','cite','open','reversed','start',
            'controls','autoplay','muted','loop','preload','poster',
            'mathvariant','mathsize','mathcolor','mathbackground',
            'viewBox','fill','d','width','height',
            'hidden','contenteditable','spellcheck','draggable'
          ]
        })
      : dirty;
    return sanitized;
  } catch {
    return DOMPurify.sanitize(markdownText ?? '');
  }
}

CSS Styling for Markdown Components

The markdown CSS is defined in markdown.css and targets elements with the .md-stream class, which is applied to the container for rendered markdown content. The styling covers all standard markdown elements including headings, lists, code blocks, blockquotes, and tables.

Headings

Headings are styled with appropriate font sizes, weights, and margins. H1 and H2 headings have a bottom border to visually separate sections.

css
.md-stream h1, .md-stream h2, .md-stream h3, 
.md-stream h4, .md-stream h5, .md-stream h6 {
  margin: 1.2em 0 0.6em;
  font-weight: 600;
  line-height: 1.25;
  color: var(--text);
}

.md-stream h1 {
  font-size: 2em;
  border-bottom: 1px solid var(--border-color);
  padding-bottom: 0.3em;
}

.md-stream h2 {
  font-size: 1.5em;
  border-bottom: 1px solid var(--border-color);
  padding-bottom: 0.3em;
}

.md-stream h3 {
  font-size: 1.25em;
}

.md-stream h4 {
  font-size: 1em;
}

.md-stream h5 {
  font-size: 0.875em;
}

.md-stream h6 {
  font-size: 0.85em;
  color: var(--muted);
}

Lists

Lists are styled with appropriate indentation and bullet styles. Nested lists have reduced indentation and different bullet types.

css
.md-stream ul, .md-stream ol {
  margin: 1em 0;
  padding-left: 2em;
  line-height: 1.6;
}

.md-stream ul ul, .md-stream ol ol, 
.md-stream ul ol, .md-stream ol ul {
  margin: 0.25em 0;
  padding-left: 1.5em;
}

.md-stream li {
  margin: 0.25em 0;
}

.md-stream ul > li {
  list-style-type: disc;
}

.md-stream ul ul > li {
  list-style-type: circle;
}

.md-stream ul ul ul > li {
  list-style-type: square;
}

Code Blocks

Code blocks are styled with a background color, padding, and rounded corners. Inline code is styled with a lighter background.

css
.md-stream pre {
  margin: 8px 0;
  padding: 10px;
  background: var(--code-bg);
  color: var(--code-fg);
  border-radius: 8px;
  overflow: auto;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
  max-width: 100%;
}

.md-stream pre code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  white-space: inherit;
  overflow-wrap: inherit;
  word-break: inherit;
}

.md-stream :not(pre) > code {
  background: var(--code-bg);
  color: var(--code-fg);
  padding: 0.15em 0.35em;
  border-radius: 6px;
}

Blockquotes and Callouts

Blockquotes are styled with left border and background color. The system also supports GitHub-style callouts with custom icons and styling for different types (NOTE, TIP, WARNING, etc.).

css
.md-stream blockquote {
  margin: 1em 0;
  padding: 1em;
  border-left: 4px solid var(--accent);
  background: var(--panel-alt-bg);
  color: var(--text);
  font-style: italic;
}

.md-stream .callout {
  margin: 1em 0;
  padding: 16px;
  border-radius: 8px;
  border-left: 4px solid;
  background-color: var(--panel-alt-bg);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  transition: all 0.2s ease;
}

.md-stream .callout:hover {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}

.md-stream .callout-body {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}

.md-stream .callout-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  margin-top: 2px;
}

.md-stream .callout-icon svg {
  width: 16px;
  height: 16px;
}

.md-stream .callout-content {
  flex: 1;
  color: var(--text);
  line-height: 1.6;
}

.md-stream .callout-content > :first-child {
  margin-top: 0;
}

.md-stream .callout-content > :last-child {
  margin-bottom: 0;
}

.md-stream .callout-custom-title {
  font-weight: 600;
  margin-bottom: 8px;
  color: var(--text);
}

Callout Type-Specific Styling

The system includes specific styling for different callout types, with distinct colors and icons for each type:

  • Note: Blue accent color with information icon
  • Tip: Green accent color with lightbulb icon
  • Important: Purple accent color with seal warning icon
  • Warning: Red accent color with warning icon
  • Caution: Orange accent color with shield warning icon
css
/* Callout type-specific styling */

.md-stream .callout-note {
  border-left-color: #0969da;
  background-color: rgb(9 105 218 / 0.05);
}

.md-stream .callout-note .callout-icon {
  color: #0969da;
}

.md-stream .callout-tip {
  border-left-color: #1f883d;
  background-color: rgb(31 136 61 / 0.05);
}

.md-stream .callout-tip .callout-icon {
  color: #1f883d;
}

.md-stream .callout-important {
  border-left-color: #8250df;
  background-color: rgb(130 80 223 / 0.05);
}

.md-stream .callout-important .callout-icon {
  color: #8250df;
}

.md-stream .callout-warning {
  border-left-color: #d1242f;
  background-color: rgb(209 36 47 / 0.05);
}

.md-stream .callout-warning .callout-icon {
  color: #d1242f;
}

.md-stream .callout-caution {
  border-left-color: #d97706;
  background-color: rgb(217 119 6 / 0.05);
}

.md-stream .callout-caution .callout-icon {
  color: #d97706;
}

Tables

Tables are styled with full width, collapsed borders, and appropriate spacing.

css
.md-stream table {
  width: 100%;
  max-width: 100%;
  border-collapse: collapse;
  margin: 1em 0;
}

.md-stream th, .md-stream td {
  padding: 0.5em;
  border: 1px solid var(--border-color);
  text-align: left;
}

.md-stream th {
  background: var(--panel-alt-bg);
  font-weight: 600;
}

CodeMirror Component Styling

The CodeMirror component provides enhanced code editing and viewing capabilities with syntax highlighting, line numbers, and copy functionality. The implementation uses CodeMirror 6 with Svelte integration.

``mermaid graph TD A[Markdown Code Block] --> B[CodeMirrorRenderer] B --> C[Create Container] C --> D[Add Toolbar] D --> E[Add Language Label] D --> F[Add Copy Button] C --> G[Create Editor Container] G --> H[Mount CodeMirror Component] H --> I[Apply Extensions] I --> J[Syntax Highlighting] I --> K[Line Numbers] I --> L[Theme] H --> M[Store Reference]



**Diagram sources**
- [codemirror-renderer.ts](file://src/lib/chat/codemirror-renderer.ts)
- [CodeMirror.svelte](file://src/lib/components/CodeMirror.svelte)

**Section sources**
- [codemirror-renderer.ts](file://src/lib/chat/codemirror-renderer.ts)
- [CodeMirror.svelte](file://src/lib/components/CodeMirror.svelte)

### CodeMirror Renderer Implementation
The `CodeMirrorRenderer` class in `codemirror-renderer.ts` is responsible for detecting code blocks in markdown content and replacing them with interactive CodeMirror editors. The renderer uses a MutationObserver to detect when new content is added to the DOM and processes any code blocks it finds.


```typescript
export class CodeMirrorRenderer {
  private codeBlocks: Map<HTMLElement, CodeBlock> = new Map();
  private observer: MutationObserver | null = null;
  private isWatching: boolean = false;
  private container: HTMLElement | null = null;

  constructor() {
    this.observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        mutation.addedNodes.forEach((node) => {
          if (node.nodeType === Node.ELEMENT_NODE) {
            this.processElement(node as HTMLElement);
          }
        });
        mutation.removedNodes.forEach((node) => {
          if (node.nodeType === Node.ELEMENT_NODE) {
            this.cleanupElement(node as HTMLElement);
          }
        });
      });
    });
  }

  public startWatching(container: HTMLElement) {
    if (this.isWatching && this.container === container) {
      return;
    }
    
    if (this.isWatching) {
      this.stopWatching();
    }
    
    this.container = container;
    this.isWatching = true;
    
    if (this.observer) {
      this.observer.observe(container, {
        childList: true,
        subtree: true,
      });
    }
    
    this.processElement(container);
  }

  private processElement(element: HTMLElement) {
    const codeElements = element.querySelectorAll('pre > code');
    
    codeElements.forEach((codeEl) => {
      const preEl = codeEl.parentElement as HTMLPreElement;
      if (!preEl || this.codeBlocks.has(preEl)) return;

      const code = codeEl.textContent || '';
      const language = this.extractLanguage(codeEl);
      
      if (code.trim().length > 10 || code.includes('\n')) {
        this.replaceWithCodeMirror(preEl, code, language);
      }
    });
  }

  private replaceWithCodeMirror(preElement: HTMLPreElement, code: string, language: string) {
    const container = document.createElement('div');
    container.className = 'codemirror-container';
    
    const toolbar = document.createElement('div');
    toolbar.className = 'codemirror-toolbar';
    
    const languageLabel = document.createElement('span');
    languageLabel.className = 'codemirror-language';
    languageLabel.textContent = language || 'text';
    
    const copyButton = document.createElement('button');
    copyButton.className = 'codemirror-copy-btn';
    copyButton.title = 'Copy code';
    
    const iconContainer = document.createElement('span');
    iconContainer.className = 'codemirror-copy-icon';
    copyButton.appendChild(iconContainer);
    
    let currentIcon = mount(Copy, {
      target: iconContainer,
      props: { size: 16, weight: 'regular' }
    });
    
    copyButton.addEventListener('click', () => {
      navigator.clipboard.writeText(code).then(() => {
        if (currentIcon) {
          try { unmount(currentIcon); } catch {}
        }
        currentIcon = mount(Check, {
          target: iconContainer,
          props: { size: 16, weight: 'regular' }
        });
        
        setTimeout(() => {
          if (currentIcon) {
            try { unmount(currentIcon); } catch {}
          }
          currentIcon = mount(Copy, {
            target: iconContainer,
            props: { size: 16, weight: 'regular' }
          });
        }, 1000);
      }).catch(() => {
        // Fallback for older browsers
        const textArea = document.createElement('textarea');
        textArea.value = code;
        document.body.appendChild(textArea);
        textArea.select();
        document.execCommand('copy');
        document.body.removeChild(textArea);
        
        if (currentIcon) {
          try { unmount(currentIcon); } catch {}
        }
        currentIcon = mount(Check, {
          target: iconContainer,
          props: { size: 16, weight: 'regular' }
        });
        
        setTimeout(() => {
          if (currentIcon) {
            try { unmount(currentIcon); } catch {}
          }
          currentIcon = mount(Copy, {
            target: iconContainer,
            props: { size: 16, weight: 'regular' }
          });
        }, 1000);
      });
    });

    toolbar.appendChild(languageLabel);
    toolbar.appendChild(copyButton);
    
    const editorContainer = document.createElement('div');
    editorContainer.className = 'codemirror-editor';
    
    container.appendChild(toolbar);
    container.appendChild(editorContainer);
    
    preElement.parentNode?.replaceChild(container, preElement);

    try {
      const component = mount(CodeMirror, {
        target: editorContainer,
        props: {
          code: code,
          language: language,
          readonly: true,
          theme: 'auto',
          showLineNumbers: true,
          wrap: true
        }
      });

      this.codeBlocks.set(container, {
        element: container,
        code,
        language,
        component,
        iconComponent: currentIcon
      });
    } catch (error) {
      console.error('Failed to mount CodeMirror component:', error);
      container.parentNode?.replaceChild(preElement, container);
    }
  }
}

CodeMirror Component Implementation

The CodeMirror.svelte component is a Svelte wrapper for CodeMirror 6 that provides a customizable code editor with syntax highlighting, line numbers, and theming.

<script lang="ts">
  import { onMount, onDestroy, createEventDispatcher } from 'svelte';
  import { EditorView, keymap, highlightActiveLine, highlightActiveLineGutter } from '@codemirror/view';
  import { EditorState, StateEffect } from '@codemirror/state';
  import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
  import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
  import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
  import { foldGutter, indentOnInput, indentUnit, bracketMatching, foldKeymap, syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
  import { lineNumbers, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor } from '@codemirror/view';
  import { javascript } from '@codemirror/lang-javascript';
  import { python } from '@codemirror/lang-python';
  import { html } from '@codemirror/lang-html';
  import { css } from '@codemirror/lang-css';
  import { json } from '@codemirror/lang-json';
  import { xml } from '@codemirror/lang-xml';
  import { sql } from '@codemirror/lang-sql';
  import { oneDark } from '@codemirror/theme-one-dark';

  export let code: string = '';
  export let language: string = '';
  export let readonly: boolean = true;
  export let theme: 'light' | 'dark' | 'auto' = 'auto';
  export let showLineNumbers: boolean = true;
  export const wrap: boolean = true;

  const dispatch = createEventDispatcher();
  
  let container: HTMLElement;
  let editorView: EditorView | null = null;

  const languageExtensions: Record<string, () => any> = {
    'javascript': () => javascript(),
    'js': () => javascript(),
    'typescript': () => javascript({ typescript: true }),
    'ts': () => javascript({ typescript: true }),
    'python': () => python(),
    'py': () => python(),
    'html': () => html(),
    'css': () => css(),
    'json': () => json(),
    'xml': () => xml(),
    'sql': () => sql(),
    'jsx': () => javascript({ jsx: true }),
    'tsx': () => javascript({ typescript: true, jsx: true }),
  };

  function getTheme(): 'light' | 'dark' {
    if (theme === 'auto') {
      return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
    }
    return theme;
  }

  function getLanguageExtension() {
    const lang = language.toLowerCase();
    if (languageExtensions[lang]) {
      try {
        return languageExtensions[lang]();
      } catch (error) {
        console.warn('Failed to load language extension for:', lang, error);
        return null;
      }
    }
    return null;
  }

  function createEditor() {
    if (!container) return;

    const extensions = [
      lineNumbers(),
      highlightActiveLineGutter(),
      highlightSpecialChars(),
      history(),
      foldGutter(),
      drawSelection(),
      dropCursor(),
      EditorState.allowMultipleSelections.of(true),
      indentOnInput(),
      bracketMatching(),
      closeBrackets(),
      autocompletion(),
      rectangularSelection(),
      crosshairCursor(),
      highlightActiveLine(),
      highlightSelectionMatches(),
      syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
      keymap.of([
        ...closeBracketsKeymap,
        ...defaultKeymap,
        ...searchKeymap,
        ...historyKeymap,
        ...foldKeymap,
        ...completionKeymap,
      ]),
      EditorView.theme({
        '&': {
          fontSize: '14px',
          border: '1px solid var(--border-color)',
          borderRadius: '8px',
          overflow: 'hidden',
        },
        '.cm-content': {
          padding: '12px',
          minHeight: '20px',
        },
        '.cm-editor': {
          borderRadius: '8px',
        },
        '.cm-focused': {
          outline: 'none',
        },
        '.cm-scroller': {
          lineHeight: '1.5',
        }
      }),
      EditorView.lineWrapping,
    ];

    const langExt = getLanguageExtension();
    if (langExt) {
      extensions.push(langExt);
    }

    const currentTheme = getTheme();
    if (currentTheme === 'dark') {
      extensions.push(oneDark);
    }

    if (readonly) {
      extensions.push(EditorState.readOnly.of(true));
    }

    if (!showLineNumbers) {
      extensions.push(EditorView.theme({
        '.cm-lineNumbers': { display: 'none' },
        '.cm-gutters': { display: 'none' }
      }));
    }

    const state = EditorState.create({
      doc: code,
      extensions,
    });

    editorView = new EditorView({
      state,
      parent: container,
    });

    if (!readonly) {
      const updateListener = EditorView.updateListener.of((update) => {
        if (update.docChanged) {
          const newCode = update.state.doc.toString();
          dispatch('change', { code: newCode });
        }
      });
      editorView.dispatch({
        effects: StateEffect.reconfigure.of([...extensions, updateListener])
      });
    }
  }

  function updateEditor() {
    if (!editorView) return;

    const currentCode = editorView.state.doc.toString();
    if (currentCode !== code) {
      editorView.dispatch({
        changes: {
          from: 0,
          to: currentCode.length,
          insert: code
        }
      });
    }
  }

  function destroyEditor() {
    if (editorView) {
      editorView.destroy();
      editorView = null;
    }
  }

  onMount(() => {
    createEditor();
  });

  onDestroy(() => {
    destroyEditor();
  });

  $: if (editorView && code !== undefined) {
    updateEditor();
  }

  $: if (container && (language || theme)) {
    destroyEditor();
    createEditor();
  }

  let mediaQuery: MediaQueryList;
  onMount(() => {
    if (theme === 'auto') {
      mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
      const handleChange = () => {
        destroyEditor();
        createEditor();
      };
      mediaQuery.addEventListener('change', handleChange);
      
      return () => {
        mediaQuery.removeEventListener('change', handleChange);
      };
    }
  });
</script>

<div bind:this={container} class="codemirror-wrapper"></div>

<style>
  .codemirror-wrapper {
    width: 100%;
    margin: 8px 0;
  }

  :global(.cm-editor) {
    background: var(--code-bg) !important;
  }

  :global(.cm-content) {
    color: var(--code-fg) !important;
  }

  :global(.cm-gutters) {
    background: var(--panel-alt-bg) !important;
    border-right: 1px solid var(--border-color) !important;
  }

  :global(.cm-lineNumbers .cm-gutterElement) {
    color: var(--muted) !important;
  }

  :global(.cm-editor:not(.cm-dark)) {
    background: var(--code-bg) !important;
    color: var(--code-fg) !important;
  }

  @media (prefers-color-scheme: dark) {
    :global(.cm-editor) {
      background: var(--code-bg) !important;
    }
  }
</style>

Responsive Design and Accessibility

The styling system includes comprehensive responsive design features and accessibility considerations to ensure the UI is usable across different devices and for users with various needs.

Responsive Design

The system uses CSS media queries to adapt the layout and styling for different screen sizes, particularly for mobile devices.

@media (width <= 768px) {
  .wrap {
    padding: 12px;
  }
  
  h1 {
    font-size: 1.75rem;
  }
  
  h2 {
    font-size: 1.5rem;
  }
  
  h3 {
    font-size: 1.25rem;
  }
}

The responsive design adjusts:

  • Scrollbar size for touch devices
  • Padding and spacing for smaller screens
  • Font sizes for better readability on mobile
  • Card padding for better use of screen space

The system implements responsive adjustments through the base.css file, which contains media queries that modify font sizes and padding when the viewport width is 768px or less. This ensures that content remains readable and accessible on mobile devices while maintaining the desktop layout on larger screens.

Accessibility Features

The system includes several accessibility features:

  1. Semantic HTML: The markdown renderer preserves semantic HTML elements like headings, lists, and tables
  2. Keyboard Navigation: CodeMirror supports keyboard navigation and editing
  3. Focus Indicators: Visual focus indicators for interactive elements
  4. Color Contrast: Sufficient color contrast between text and background
  5. ARIA Attributes: Support for ARIA attributes in the allowed tags list
  6. Screen Reader Support: Semantic structure and proper heading hierarchy

The copy button in CodeMirror components includes a title attribute for screen readers and changes its icon to provide visual feedback when clicked.

Theme Integration and Customization

The theme system is designed to be easily customizable through CSS variables. Users can modify the appearance of the entire application by changing the values of the CSS custom properties.

Theme Variables

The following variables can be customized to change the appearance of the application:

Color Variables:

  • --bg: Background color
  • --card: Card background color
  • --panel-bg: Panel background color
  • --text: Text color
  • --muted: Muted text color
  • --border-color: Border color
  • --accent: Accent color (used for links and buttons)
  • --accent-2: Darker accent color (used for hover states)

Code Syntax Highlighting Variables:

  • --code-bg: Code block background color
  • --code-fg: Code text color
  • --code-keyword: Color for keywords
  • --code-string: Color for strings
  • --code-title: Color for titles and names
  • --code-number: Color for numbers
  • --code-variable: Color for variables
  • --code-tag: Color for tags and meta
  • --code-comment: Color for comments

Layout Variables:

  • --shadow: Box shadow for normal state
  • --shadow-hover: Box shadow for hover state
  • --content-gap: Default gap between content and container edges
  • --content-gap-top: Larger gap from header

Customization Examples

To create a custom theme, users can override these variables in their CSS:

:root {
  --bg: #f8f9fa;
  --card: #ffffff;
  --panel-bg: #ffffff;
  --text: #212529;
  --muted: #6c757d;
  --border-color: #dee2e6;
  --accent: #0d6efd;
  --accent-2: #0b5ed7;
  --panel-alt-bg: #f8f9fa;
  --code-bg: #f8f9fa;
  --code-fg: #212529;
  --code-keyword: #d63384;
  --code-string: #198754;
  --code-title: #0d6efd;
  --code-number: #fd7e14;
  --code-variable: #6f42c1;
  --code-tag: #0d6efd;
  --code-comment: #6c757d;
}

The system also supports automatic theme switching based on the user's system preference through the prefers-color-scheme media query.

Conclusion

The UI component styling system in the Oxide-Lab repository is a comprehensive solution for rendering markdown content and code blocks with consistent theming, responsive design, and accessibility features. The system uses CSS custom properties for theming, with support for both light and dark modes. The markdown rendering is handled by the marked library with custom extensions for syntax highlighting and security sanitization. Code blocks are enhanced with the CodeMirror editor, providing syntax highlighting, line numbers, and copy functionality. The styling is implemented through a combination of global CSS, component-specific CSS, and Svelte component styling. The system is designed to be easily customizable through CSS variables, allowing users to modify the appearance of the entire application.

Section sources

  • LINT_REMAINING.md - Updated in commit 5e0382a79fcaaf01765794eaee28242666730cfd

Referenced Files in This Document

  • app.css
  • markdown.ts
  • codemirror-renderer.ts
  • CodeMirror.svelte
  • markdown.css
  • progress.css
  • callouts.css
  • base.css
  • LINT_REMAINING.md - Updated in commit 5e0382a79fcaaf01765794eaee28242666730cfd

Clone this wiki locally