-
Notifications
You must be signed in to change notification settings - Fork 3
4. Ui Component Styling
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
- Introduction
- CSS Architecture and Theme System
- Markdown Component Styling
- CodeMirror Component Styling
- Responsive Design and Accessibility
- Theme Integration and Customization
- Conclusion
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.
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
The markdown rendering is handled by the renderMarkdownToSafeHtml function in markdown.ts, which processes markdown text through several stages:
- Normalization: Convert Windows-style line endings to Unix-style
- Callout Processing: Handle GitHub-style callouts (NOTE, TIP, WARNING, etc.)
-
Markdown Parsing: Convert markdown to HTML using the
markedlibrary -
Syntax Highlighting: Apply syntax highlighting using
highlight.js -
Sanitization: Clean the HTML output using
DOMPurifyto 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 ?? '');
}
}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 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 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 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 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);
}
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 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;
}
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);
}
}
}
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>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.
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.
The system includes several accessibility features:
- Semantic HTML: The markdown renderer preserves semantic HTML elements like headings, lists, and tables
- Keyboard Navigation: CodeMirror supports keyboard navigation and editing
- Focus Indicators: Visual focus indicators for interactive elements
- Color Contrast: Sufficient color contrast between text and background
- ARIA Attributes: Support for ARIA attributes in the allowed tags list
- 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.
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.
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
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.
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