From 1b35c4ccc190cce15931afb1018139f8116fec8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Wed, 15 Oct 2025 16:52:39 +0200 Subject: [PATCH] Tired --- THEME.md | 189 ++++++++++++++ index.html | 5 +- package.json | 1 + pnpm-lock.yaml | 3 + public/favicon.svg | 14 + src/App.svelte | 65 ++--- src/lib/components/editor/Editor.svelte | 28 +- .../components/editor/EditorSettings.svelte | 111 ++++++-- .../components/editor/EditorWithLogs.svelte | 6 +- src/lib/components/editor/LogPanel.svelte | 80 +++--- .../reference/CsoundReference.svelte | 241 ++++++++++-------- src/lib/components/ui/ConfirmDialog.svelte | 24 +- src/lib/components/ui/FileBrowser.svelte | 92 +++---- src/lib/components/ui/InputDialog.svelte | 38 +-- src/lib/components/ui/Modal.svelte | 14 +- src/lib/components/ui/Popup.svelte | 22 +- src/lib/components/ui/ResizablePopup.svelte | 22 +- src/lib/components/ui/SidePanel.svelte | 52 ++-- src/lib/components/ui/TemplateDialog.svelte | 30 +-- src/lib/components/ui/TopBar.svelte | 16 +- src/lib/csound-reference/csoundTooltips.ts | 153 ++++++----- src/lib/editor/codemirror-theme.ts | 105 ++++++++ src/lib/stores/editorSettings.ts | 8 +- src/lib/stores/uiState.svelte.ts | 8 + src/main.ts | 15 ++ src/theme.css | 169 ++++++++++++ 26 files changed, 1078 insertions(+), 433 deletions(-) create mode 100644 THEME.md create mode 100644 public/favicon.svg create mode 100644 src/lib/editor/codemirror-theme.ts create mode 100644 src/theme.css diff --git a/THEME.md b/THEME.md new file mode 100644 index 0000000..b0884ef --- /dev/null +++ b/THEME.md @@ -0,0 +1,189 @@ +# Theme System + +## Overview + +The application uses a centralized CSS custom properties (variables) system defined in `src/theme.css`. This provides a consistent design language across all components and makes theme switching possible in the future. + +## Architecture + +### Theme Variables + +All theme variables are defined at the `:root` level in `theme.css` and use the `--` prefix: + +```css +:root { + --color-accent-primary: #646cff; + --space-md: 0.75rem; + --font-base: 0.875rem; +} +``` + +### Usage in Components + +Components reference theme variables using `var()`: + +```css +.my-component { + color: var(--color-text-primary); + padding: var(--space-md); + font-size: var(--font-base); +} +``` + +## Variable Categories + +### Colors + +#### Base & Surface +- `--color-bg-base`: Main background +- `--color-surface-0` through `--color-surface-3`: Elevated surfaces + +#### Text Hierarchy +- `--color-text-primary`: Main text (87% opacity) +- `--color-text-secondary`: Secondary text (60% opacity) +- `--color-text-tertiary`: Labels and hints (40% opacity) +- `--color-text-disabled`: Disabled state (25% opacity) + +#### Accent +- `--color-accent-primary`: Primary brand color (#646cff) +- `--color-accent-primary-hover`: Hover state +- `--color-accent-primary-subtle`: Transparent accent for backgrounds + +#### Semantic +- `--color-success`: Success states (#10b981) +- `--color-warning`: Warning states (#fbbf24) +- `--color-error`: Error states (#ef4444) +- `--color-info`: Informational elements (#60a5fa) + +#### Borders +- `--color-border-subtle`: Very subtle borders (6% opacity) +- `--color-border-default`: Standard borders (10% opacity) +- `--color-border-strong`: Emphasized borders (20% opacity) + +#### Interactive States +- `--color-hover-overlay`: Hover effect +- `--color-active-overlay`: Active/pressed effect +- `--color-selected-overlay`: Selection background + +### Spacing + +Uses a consistent scale from `--space-xs` (0.25rem) to `--space-2xl` (2rem): + +``` +xs: 0.25rem +sm: 0.5rem +md: 0.75rem +lg: 1rem +xl: 1.5rem +2xl: 2rem +``` + +### Typography + +#### Font Sizes +- `--font-xs` through `--font-lg` for consistent sizing +- Base size is `--font-base: 0.875rem` + +#### Font Families +- `--font-mono`: Monospace font for code +- `--font-sans`: System UI font + +#### Line Heights +- `--leading-tight`: 1.4 +- `--leading-normal`: 1.5 +- `--leading-relaxed`: 1.6 + +### Visual Effects + +- `--radius-sm/md/lg`: Border radius values +- `--shadow-sm/md/lg`: Box shadow presets +- `--transition-fast/base/slow`: Animation durations + +### Component-Specific + +Specialized tokens for specific use cases: +- `--accordion-*`: Accordion styling +- `--input-*`: Form inputs +- `--tooltip-*`: Tooltip components +- `--code-*`: Code blocks + +## Design Principles + +### Color Usage + +1. **Use accent colors sparingly**: Primary accent (#646cff) for: + - Interactive elements + - Primary actions + - Highlighted content (e.g., opcode names) + +2. **Semantic colors for meaning**: + - Blue (`--color-info`) for informational data (e.g., rates) + - Green (`--color-success`) for success states + - Yellow (`--color-warning`) for warnings + - Red (`--color-error`) for errors + +3. **Text hierarchy through opacity**: + - Use `--color-text-*` variables instead of custom opacity values + - Maintains consistency and improves readability + +### Spacing + +- Use the spacing scale consistently +- Avoid magic numbers - use defined spacing tokens +- Vertical rhythm: prefer consistent spacing between elements + +### Borders + +- Most borders should use `--color-border-subtle` for minimal visual weight +- Use `--color-border-default` for more emphasis +- Reserve `--color-border-strong` for important separators + +## Adding a New Theme + +To add a new theme (e.g., light mode): + +1. Create a new CSS file (e.g., `theme-light.css`) +2. Override the root variables: + +```css +:root { + --color-bg-base: #ffffff; + --color-text-primary: rgba(0, 0, 0, 0.87); + /* ... other overrides */ +} +``` + +3. Conditionally import based on theme preference +4. All components automatically adapt to the new values + +## Migration Guide + +When updating existing components to use the theme system: + +### Before +```css +.component { + background-color: #1a1a1a; + color: rgba(255, 255, 255, 0.87); + padding: 0.75rem; + border: 1px solid #3a3a3a; +} +``` + +### After +```css +.component { + background-color: var(--color-bg-base); + color: var(--color-text-primary); + padding: var(--space-md); + border: 1px solid var(--color-border-default); +} +``` + +## Benefits + +1. **Consistency**: Single source of truth for design tokens +2. **Maintainability**: Change once, apply everywhere +3. **Theme switching**: Easy to add light/dark mode or custom themes +4. **Readability**: Semantic names instead of magic values +5. **Scalability**: Add new tokens without touching components diff --git a/index.html b/index.html index 4c3f06d..e4aa5bc 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,10 @@ - + - oldboy + + Online Csound Editor
diff --git a/package.json b/package.json index 83f5a22..0529f05 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@codemirror/view": "^6.38.6", "@csound/browser": "7.0.0-beta11", "@hlolli/codemirror-lang-csound": "1.0.0-alpha10", + "@lezer/highlight": "^1.2.1", "@replit/codemirror-vim": "^6.3.0", "codemirror": "^6.0.2", "fuse.js": "^7.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c82994..66fb91a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: '@hlolli/codemirror-lang-csound': specifier: 1.0.0-alpha10 version: 1.0.0-alpha10(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)(codemirror@6.0.2) + '@lezer/highlight': + specifier: ^1.2.1 + version: 1.2.1 '@replit/codemirror-vim': specifier: ^6.3.0 version: 6.3.0(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6) diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..7564243 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/src/App.svelte b/src/App.svelte index 8467f3a..25b80af 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -237,6 +237,13 @@ } } + $effect(() => { + const theme = $editorSettings.theme; + if (typeof document !== 'undefined') { + document.documentElement.dataset.theme = theme; + } + }); + $effect(() => { if (uiState.scopePopupVisible || uiState.spectrogramPopupVisible) { analyserNode = csound.getAnalyserNode(); @@ -343,16 +350,16 @@ {/snippet} + + + +
+ +
diff --git a/src/lib/components/ui/ConfirmDialog.svelte b/src/lib/components/ui/ConfirmDialog.svelte index 4418fc8..3fdf3f9 100644 --- a/src/lib/components/ui/ConfirmDialog.svelte +++ b/src/lib/components/ui/ConfirmDialog.svelte @@ -48,42 +48,42 @@ diff --git a/src/lib/components/ui/FileBrowser.svelte b/src/lib/components/ui/FileBrowser.svelte index fe615eb..dcff52d 100644 --- a/src/lib/components/ui/FileBrowser.svelte +++ b/src/lib/components/ui/FileBrowser.svelte @@ -235,46 +235,46 @@ display: flex; flex-direction: column; height: 100%; - background-color: #1a1a1a; + background-color: var(--color-bg-base); } .browser-header { display: flex; justify-content: space-between; align-items: center; - padding: 0.75rem; - background-color: #2a2a2a; - border-bottom: 1px solid #3a3a3a; + padding: var(--space-md); + background-color: var(--color-surface-3); + border-bottom: 1px solid var(--color-border-default); } .browser-title { - font-size: 0.75rem; + font-size: var(--font-sm); font-weight: 600; - color: rgba(255, 255, 255, 0.6); + color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.05em; } .header-actions { display: flex; - gap: 0.25rem; + gap: var(--space-xs); } .action-button { padding: 0.375rem; background-color: transparent; - color: rgba(255, 255, 255, 0.7); + color: var(--color-text-secondary); border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; - transition: all 0.2s; + transition: all var(--transition-base); } .action-button:hover { - color: rgba(255, 255, 255, 1); - background-color: rgba(255, 255, 255, 0.1); + color: var(--color-text-primary); + background-color: var(--color-active-overlay); } .browser-content { @@ -283,9 +283,9 @@ } .empty-state { - padding: 2rem 1rem; + padding: var(--space-2xl) var(--space-lg); text-align: center; - color: rgba(255, 255, 255, 0.4); + color: var(--color-text-tertiary); } .project-list { @@ -296,24 +296,24 @@ .project-item { display: flex; align-items: center; - gap: 0.75rem; - padding: 0.75rem; - border-bottom: 1px solid #2a2a2a; + gap: var(--space-md); + padding: var(--space-md); + border-bottom: 1px solid var(--color-surface-3); cursor: pointer; - transition: background-color 0.2s; + transition: background-color var(--transition-base); } .project-item:hover { - background-color: #252525; + background-color: var(--color-surface-2); } .project-item.selected { background-color: rgba(100, 108, 255, 0.2); - border-left: 3px solid #646cff; + border-left: 3px solid var(--color-accent-primary); } .project-icon { - color: rgba(255, 255, 255, 0.6); + color: var(--color-text-secondary); display: flex; align-items: center; } @@ -324,24 +324,24 @@ } .project-title { - font-size: 0.875rem; - color: rgba(255, 255, 255, 0.87); + font-size: var(--font-base); + color: var(--color-text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .delete-button { - padding: 0.25rem; + padding: var(--space-xs); background-color: transparent; - color: rgba(255, 255, 255, 0.4); + color: var(--color-text-tertiary); border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; opacity: 0; - transition: all 0.2s; + transition: all var(--transition-base); } .project-item:hover .delete-button { @@ -354,50 +354,50 @@ } .metadata-editor { - border-top: 1px solid #2a2a2a; - background-color: #1a1a1a; - padding: 1rem; + border-top: 1px solid var(--color-surface-3); + background-color: var(--color-bg-base); + padding: var(--space-lg); } .metadata-header { - font-size: 0.75rem; + font-size: var(--font-sm); font-weight: 600; - color: rgba(255, 255, 255, 0.6); + color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.05em; - margin-bottom: 1rem; + margin-bottom: var(--space-lg); } .metadata-fields { display: flex; flex-direction: column; - gap: 0.75rem; + gap: var(--space-md); } .field { display: flex; flex-direction: column; - gap: 0.25rem; + gap: var(--space-xs); } .field label { - font-size: 0.75rem; - color: rgba(255, 255, 255, 0.6); + font-size: var(--font-sm); + color: var(--color-text-secondary); } .field input, .field select { - padding: 0.5rem; - background-color: #2a2a2a; - border: 1px solid #3a3a3a; - color: rgba(255, 255, 255, 0.87); - font-size: 0.875rem; + padding: var(--space-sm); + background-color: var(--color-surface-3); + border: 1px solid var(--color-border-default); + color: var(--color-text-primary); + font-size: var(--font-base); outline: none; } .field input:focus, .field select:focus { - border-color: #646cff; + border-color: var(--color-accent-primary); } .field select { @@ -405,11 +405,11 @@ } .field.readonly .readonly-value { - padding: 0.5rem; - background-color: #1a1a1a; - border: 1px solid #2a2a2a; - color: rgba(255, 255, 255, 0.6); - font-size: 0.875rem; + padding: var(--space-sm); + background-color: var(--color-bg-base); + border: 1px solid var(--color-surface-3); + color: var(--color-text-secondary); + font-size: var(--font-base); font-family: monospace; } diff --git a/src/lib/components/ui/InputDialog.svelte b/src/lib/components/ui/InputDialog.svelte index b75f800..87eeb76 100644 --- a/src/lib/components/ui/InputDialog.svelte +++ b/src/lib/components/ui/InputDialog.svelte @@ -64,58 +64,58 @@ .input-dialog { display: flex; flex-direction: column; - gap: 1rem; + gap: var(--space-lg); } label { - color: rgba(255, 255, 255, 0.87); - font-size: 0.875rem; + color: var(--color-text-primary); + font-size: var(--font-base); } input { - padding: 0.5rem; - background-color: #1a1a1a; - border: 1px solid #3a3a3a; - color: rgba(255, 255, 255, 0.87); - font-size: 0.875rem; + padding: var(--space-sm); + background-color: var(--color-bg-base); + border: 1px solid var(--color-border-default); + color: var(--color-text-primary); + font-size: var(--font-base); outline: none; } input:focus { - border-color: #646cff; + border-color: var(--color-accent-primary); } .button-group { display: flex; - gap: 0.75rem; + gap: var(--space-md); justify-content: flex-end; } .button { - padding: 0.5rem 1rem; + padding: var(--space-sm) var(--space-lg); border: none; cursor: pointer; - font-size: 0.875rem; - transition: all 0.2s; + font-size: var(--font-base); + transition: all var(--transition-base); } .button-primary { - background-color: #646cff; + background-color: var(--color-accent-primary); color: white; } .button-primary:hover { - background-color: #535bf2; + background-color: var(--color-accent-primary-active); } .button-secondary { background-color: transparent; - color: rgba(255, 255, 255, 0.6); - border: 1px solid #3a3a3a; + color: var(--color-text-secondary); + border: 1px solid var(--color-border-default); } .button-secondary:hover { - background-color: rgba(255, 255, 255, 0.05); - color: rgba(255, 255, 255, 0.87); + background-color: var(--color-hover-overlay); + color: var(--color-text-primary); } diff --git a/src/lib/components/ui/Modal.svelte b/src/lib/components/ui/Modal.svelte index d04eb27..91c6a3c 100644 --- a/src/lib/components/ui/Modal.svelte +++ b/src/lib/components/ui/Modal.svelte @@ -52,8 +52,8 @@ } .modal-content { - background-color: #2a2a2a; - border: 1px solid #3a3a3a; + background-color: var(--color-surface-3); + border: 1px solid var(--color-border-default); min-width: 300px; max-width: 500px; max-height: 80vh; @@ -61,18 +61,18 @@ } .modal-header { - padding: 1rem 1.5rem; - border-bottom: 1px solid #3a3a3a; + padding: var(--space-lg) var(--space-xl); + border-bottom: 1px solid var(--color-border-default); } .modal-header h3 { margin: 0; - color: rgba(255, 255, 255, 0.87); - font-size: 1rem; + color: var(--color-text-primary); + font-size: var(--font-lg); font-weight: 600; } .modal-body { - padding: 1.5rem; + padding: var(--space-xl); } diff --git a/src/lib/components/ui/Popup.svelte b/src/lib/components/ui/Popup.svelte index 32405eb..f178648 100644 --- a/src/lib/components/ui/Popup.svelte +++ b/src/lib/components/ui/Popup.svelte @@ -83,8 +83,8 @@ .popup { position: fixed; z-index: 9999; - background-color: #1a1a1a; - border: 1px solid #333; + background-color: var(--color-bg-base); + border: 1px solid var(--color-border-default); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); display: flex; flex-direction: column; @@ -94,23 +94,23 @@ display: flex; justify-content: space-between; align-items: center; - padding: 0.75rem 1rem; - background-color: #252525; - border-bottom: 1px solid #333; + padding: var(--space-md) var(--space-lg); + background-color: var(--color-surface-2); + border-bottom: 1px solid var(--color-border-default); cursor: move; user-select: none; } .popup-title { font-weight: 600; - font-size: 0.875rem; - color: rgba(255, 255, 255, 0.87); + font-size: var(--font-base); + color: var(--color-text-primary); } .close-button { background: none; border: none; - color: rgba(255, 255, 255, 0.6); + color: var(--color-text-secondary); font-size: 1.5rem; line-height: 1; padding: 0; @@ -120,16 +120,16 @@ display: flex; align-items: center; justify-content: center; - transition: color 0.2s; + transition: color var(--transition-base); } .close-button:hover { - color: rgba(255, 255, 255, 1); + color: var(--color-text-primary); } .popup-content { flex: 1; overflow: auto; - padding: 1rem; + padding: var(--space-lg); } diff --git a/src/lib/components/ui/ResizablePopup.svelte b/src/lib/components/ui/ResizablePopup.svelte index de75b98..320ad56 100644 --- a/src/lib/components/ui/ResizablePopup.svelte +++ b/src/lib/components/ui/ResizablePopup.svelte @@ -148,8 +148,8 @@ .resizable-popup { position: fixed; z-index: 9999; - background-color: #1a1a1a; - border: 1px solid #333; + background-color: var(--color-bg-base); + border: 1px solid var(--color-border-default); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); display: flex; flex-direction: column; @@ -159,23 +159,23 @@ display: flex; justify-content: space-between; align-items: center; - padding: 0.75rem 1rem; - background-color: #252525; - border-bottom: 1px solid #333; + padding: var(--space-md) var(--space-lg); + background-color: var(--color-surface-2); + border-bottom: 1px solid var(--color-border-default); cursor: move; user-select: none; } .popup-title { font-weight: 600; - font-size: 0.875rem; - color: rgba(255, 255, 255, 0.87); + font-size: var(--font-base); + color: var(--color-text-primary); } .close-button { background: none; border: none; - color: rgba(255, 255, 255, 0.6); + color: var(--color-text-secondary); font-size: 1.5rem; line-height: 1; padding: 0; @@ -185,17 +185,17 @@ display: flex; align-items: center; justify-content: center; - transition: color 0.2s; + transition: color var(--transition-base); } .close-button:hover { - color: rgba(255, 255, 255, 1); + color: var(--color-text-primary); } .popup-content { flex: 1; overflow: auto; - padding: 1rem; + padding: var(--space-lg); } .popup-content.no-padding { diff --git a/src/lib/components/ui/SidePanel.svelte b/src/lib/components/ui/SidePanel.svelte index b3817ce..47cad50 100644 --- a/src/lib/components/ui/SidePanel.svelte +++ b/src/lib/components/ui/SidePanel.svelte @@ -166,7 +166,7 @@ diff --git a/src/lib/csound-reference/csoundTooltips.ts b/src/lib/csound-reference/csoundTooltips.ts index 8a0bb8a..717945c 100644 --- a/src/lib/csound-reference/csoundTooltips.ts +++ b/src/lib/csound-reference/csoundTooltips.ts @@ -90,32 +90,33 @@ export const csoundTooltip = hoverTooltip((view, pos) => { const paramsList = document.createElement('div'); paramsList.className = 'csound-tooltip-params'; - reference.parameters.slice(0, 5).forEach(param => { + reference.parameters.forEach(param => { const paramItem = document.createElement('div'); paramItem.className = 'csound-tooltip-param'; + const paramHeader = document.createElement('div'); + paramHeader.className = 'csound-tooltip-param-header'; + const paramName = document.createElement('span'); paramName.className = 'csound-tooltip-param-name'; paramName.textContent = param.name; - const paramDesc = document.createElement('span'); - paramDesc.className = 'csound-tooltip-param-desc'; - paramDesc.textContent = param.description.length > 60 - ? param.description.substring(0, 60) + '...' - : param.description; + const paramType = document.createElement('span'); + paramType.className = 'csound-tooltip-param-type'; + paramType.textContent = param.type; - paramItem.appendChild(paramName); + paramHeader.appendChild(paramName); + paramHeader.appendChild(paramType); + + const paramDesc = document.createElement('div'); + paramDesc.className = 'csound-tooltip-param-desc'; + paramDesc.textContent = param.description; + + paramItem.appendChild(paramHeader); paramItem.appendChild(paramDesc); paramsList.appendChild(paramItem); }); - if (reference.parameters.length > 5) { - const more = document.createElement('div'); - more.className = 'csound-tooltip-more'; - more.textContent = `... and ${reference.parameters.length - 5} more`; - paramsList.appendChild(more); - } - dom.appendChild(paramsList); } @@ -126,114 +127,126 @@ export const csoundTooltip = hoverTooltip((view, pos) => { export const csoundTooltipTheme = EditorView.theme({ '.cm-tooltip.cm-tooltip-hover': { - backgroundColor: 'rgba(0, 0, 0, 0.95)', + backgroundColor: 'var(--tooltip-bg)', backdropFilter: 'blur(8px)', - color: '#ffffff', - fontSize: '12px', - fontFamily: 'monospace', + color: 'var(--color-text-primary)', + fontSize: 'var(--font-sm)', + fontFamily: 'var(--font-mono)', padding: '0', maxWidth: '500px', - border: '1px solid #3a3a3a', + maxHeight: '400px', + overflowY: 'auto', + border: '1px solid var(--tooltip-border)', + borderRadius: 'var(--radius-md)', + boxShadow: 'var(--shadow-lg)', zIndex: '100' }, '.csound-tooltip': { - padding: '10px 12px' + padding: 'var(--space-md) var(--space-lg)' }, '.csound-tooltip-header': { display: 'flex', alignItems: 'center', - gap: '8px', - marginBottom: '6px', - paddingBottom: '6px', - borderBottom: '1px solid #3a3a3a' + gap: 'var(--space-sm)', + marginBottom: 'var(--space-sm)', + paddingBottom: 'var(--space-sm)', + borderBottom: '1px solid var(--color-border-subtle)' }, '.csound-tooltip-title': { fontWeight: '600', - color: '#646cff', - fontSize: '14px' + color: 'var(--color-accent-primary)', + fontSize: 'var(--font-md)' }, '.csound-tooltip-type': { - fontSize: '10px', - color: '#9ca3af', - backgroundColor: 'rgba(255, 255, 255, 0.1)', - padding: '2px 6px', - textTransform: 'uppercase' + fontSize: 'var(--font-xs)', + color: 'var(--color-text-tertiary)', + backgroundColor: 'transparent', + padding: '0', + textTransform: 'lowercase' }, '.csound-tooltip-description': { - color: '#e5e7eb', - lineHeight: '1.5', - marginBottom: '8px' + color: 'var(--color-text-secondary)', + lineHeight: 'var(--leading-relaxed)', + marginBottom: 'var(--space-md)', + fontSize: 'var(--font-sm)' }, '.csound-tooltip-syntax-label': { - fontSize: '10px', - color: '#9ca3af', - marginTop: '8px', - marginBottom: '4px', - textTransform: 'uppercase', + fontSize: 'var(--font-xs)', + color: 'var(--color-text-tertiary)', + marginTop: 'var(--space-md)', + marginBottom: 'var(--space-xs)', + textTransform: 'lowercase', fontWeight: '500' }, '.csound-tooltip-syntax': { - backgroundColor: 'rgba(0, 0, 0, 0.4)', - color: '#fbbf24', - padding: '6px 8px', - fontSize: '11px', - fontFamily: 'monospace', - margin: '0 0 8px 0', + backgroundColor: 'var(--code-bg)', + color: 'var(--code-text)', + padding: 'var(--space-sm) 0 var(--space-sm) var(--space-sm)', + fontSize: 'var(--font-sm)', + fontFamily: 'var(--font-mono)', + margin: '0 0 var(--space-md) 0', whiteSpace: 'pre-wrap', - lineHeight: '1.4', - border: '1px solid #2a2a2a' + lineHeight: 'var(--leading-normal)', + border: 'none', + borderLeft: '2px solid var(--code-border)' }, '.csound-tooltip-rates': { - fontSize: '11px', - color: '#60a5fa', - marginBottom: '8px' + fontSize: 'var(--font-sm)', + color: 'var(--color-info)', + marginBottom: 'var(--space-md)' }, '.csound-tooltip-params-label': { - fontSize: '10px', - color: '#9ca3af', - marginTop: '8px', - marginBottom: '4px', - textTransform: 'uppercase', + fontSize: 'var(--font-xs)', + color: 'var(--color-text-tertiary)', + marginTop: 'var(--space-md)', + marginBottom: 'var(--space-sm)', + textTransform: 'lowercase', fontWeight: '500' }, '.csound-tooltip-params': { display: 'flex', flexDirection: 'column', - gap: '4px' + gap: 'var(--space-md)' }, '.csound-tooltip-param': { display: 'flex', - gap: '6px', - fontSize: '11px', - lineHeight: '1.4' + flexDirection: 'column', + gap: 'var(--space-xs)', + fontSize: 'var(--font-sm)', + lineHeight: 'var(--leading-normal)' + }, + + '.csound-tooltip-param-header': { + display: 'flex', + alignItems: 'center', + gap: 'var(--space-sm)' }, '.csound-tooltip-param-name': { - color: '#10b981', + color: 'var(--color-text-primary)', fontWeight: '500', - minWidth: '60px' + fontSize: 'var(--font-sm)' + }, + + '.csound-tooltip-param-type': { + color: 'var(--color-text-tertiary)', + fontSize: 'var(--font-xs)' }, '.csound-tooltip-param-desc': { - color: '#d1d5db', - flex: '1' - }, - - '.csound-tooltip-more': { - fontSize: '10px', - color: '#9ca3af', - fontStyle: 'italic', - marginTop: '4px' + color: 'var(--color-text-secondary)', + fontSize: 'var(--font-sm)', + lineHeight: 'var(--leading-normal)' } }); diff --git a/src/lib/editor/codemirror-theme.ts b/src/lib/editor/codemirror-theme.ts new file mode 100644 index 0000000..60e73d3 --- /dev/null +++ b/src/lib/editor/codemirror-theme.ts @@ -0,0 +1,105 @@ +import { EditorView } from '@codemirror/view'; +import type { Extension } from '@codemirror/state'; +import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; +import { tags as t } from '@lezer/highlight'; + +function getCSSVariable(name: string): string { + if (typeof window === 'undefined') return ''; + return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); +} + +export function createCodeMirrorTheme(): Extension { + const editorTheme = EditorView.theme({ + '&': { + backgroundColor: getCSSVariable('--color-bg-base'), + color: getCSSVariable('--color-text-primary'), + }, + '.cm-content': { + caretColor: getCSSVariable('--color-accent-primary'), + }, + '.cm-cursor, .cm-dropCursor': { + borderLeftColor: getCSSVariable('--color-accent-primary'), + }, + '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': { + backgroundColor: getCSSVariable('--color-selected-overlay'), + }, + '.cm-panels': { + backgroundColor: getCSSVariable('--color-surface-1'), + color: getCSSVariable('--color-text-primary'), + }, + '.cm-panels.cm-panels-top': { + borderBottom: `1px solid ${getCSSVariable('--color-border-default')}`, + }, + '.cm-panels.cm-panels-bottom': { + borderTop: `1px solid ${getCSSVariable('--color-border-default')}`, + }, + '.cm-searchMatch': { + backgroundColor: getCSSVariable('--color-warning'), + outline: `1px solid ${getCSSVariable('--color-border-accent')}`, + }, + '.cm-searchMatch.cm-searchMatch-selected': { + backgroundColor: getCSSVariable('--color-accent-primary-subtle'), + }, + '.cm-activeLine': { + backgroundColor: getCSSVariable('--color-hover-overlay'), + }, + '.cm-selectionMatch': { + backgroundColor: getCSSVariable('--color-selected-overlay'), + }, + '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { + backgroundColor: getCSSVariable('--color-active-overlay'), + }, + '.cm-gutters': { + backgroundColor: getCSSVariable('--color-surface-1'), + color: getCSSVariable('--color-text-tertiary'), + border: 'none', + borderRight: `1px solid ${getCSSVariable('--color-border-subtle')}`, + }, + '.cm-activeLineGutter': { + backgroundColor: getCSSVariable('--color-hover-overlay'), + color: getCSSVariable('--color-text-secondary'), + }, + '.cm-foldPlaceholder': { + backgroundColor: getCSSVariable('--color-surface-2'), + border: `1px solid ${getCSSVariable('--color-border-default')}`, + color: getCSSVariable('--color-text-secondary'), + }, + '.cm-tooltip': { + border: `1px solid ${getCSSVariable('--color-border-default')}`, + backgroundColor: getCSSVariable('--color-tooltip-bg'), + }, + '.cm-tooltip .cm-tooltip-arrow:before': { + borderTopColor: getCSSVariable('--color-border-default'), + }, + '.cm-tooltip .cm-tooltip-arrow:after': { + borderTopColor: getCSSVariable('--color-tooltip-bg'), + }, + '.cm-tooltip-autocomplete': { + '& > ul > li[aria-selected]': { + backgroundColor: getCSSVariable('--color-selected-overlay'), + color: getCSSVariable('--color-text-primary'), + }, + }, + }, { dark: getCSSVariable('--color-bg-base').startsWith('#1') || getCSSVariable('--color-bg-base').startsWith('#2') }); + + const highlightStyle = HighlightStyle.define([ + { tag: t.keyword, color: getCSSVariable('--color-accent-primary') }, + { tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: getCSSVariable('--color-text-primary') }, + { tag: [t.function(t.variableName), t.labelName], color: getCSSVariable('--color-info') }, + { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: getCSSVariable('--color-warning') }, + { tag: [t.definition(t.name), t.separator], color: getCSSVariable('--color-text-primary') }, + { tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: getCSSVariable('--color-warning') }, + { tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: getCSSVariable('--color-accent-primary-hover') }, + { tag: [t.meta, t.comment], color: getCSSVariable('--color-text-tertiary'), fontStyle: 'italic' }, + { tag: t.strong, fontWeight: 'bold' }, + { tag: t.emphasis, fontStyle: 'italic' }, + { tag: t.strikethrough, textDecoration: 'line-through' }, + { tag: t.link, color: getCSSVariable('--color-info'), textDecoration: 'underline' }, + { tag: t.heading, fontWeight: 'bold', color: getCSSVariable('--color-accent-primary') }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: getCSSVariable('--color-warning') }, + { tag: [t.processingInstruction, t.string, t.inserted], color: getCSSVariable('--color-success') }, + { tag: t.invalid, color: getCSSVariable('--color-error') }, + ]); + + return [editorTheme, syntaxHighlighting(highlightStyle)]; +} diff --git a/src/lib/stores/editorSettings.ts b/src/lib/stores/editorSettings.ts index f11a6d3..4114f7c 100644 --- a/src/lib/stores/editorSettings.ts +++ b/src/lib/stores/editorSettings.ts @@ -2,6 +2,8 @@ import { writable } from 'svelte/store'; const STORAGE_KEY = 'editorSettings'; +export type Theme = 'dark' | 'light'; + export interface EditorSettings { fontSize: number; fontFamily: string; @@ -9,6 +11,8 @@ export interface EditorSettings { enableLineWrapping: boolean; tabSize: number; vimMode: boolean; + enableHoverTooltips: boolean; + theme: Theme; } const defaultSettings: EditorSettings = { @@ -17,7 +21,9 @@ const defaultSettings: EditorSettings = { showLineNumbers: true, enableLineWrapping: false, tabSize: 2, - vimMode: false + vimMode: false, + enableHoverTooltips: true, + theme: 'dark' }; export interface EditorSettingsStore { diff --git a/src/lib/stores/uiState.svelte.ts b/src/lib/stores/uiState.svelte.ts index 533ee99..4cbe04a 100644 --- a/src/lib/stores/uiState.svelte.ts +++ b/src/lib/stores/uiState.svelte.ts @@ -30,10 +30,18 @@ export class UIState { this.scopePopupVisible = true; } + toggleScope() { + this.scopePopupVisible = !this.scopePopupVisible; + } + openSpectrogram() { this.spectrogramPopupVisible = true; } + toggleSpectrogram() { + this.spectrogramPopupVisible = !this.spectrogramPopupVisible; + } + showShare(url: string) { this.shareUrl = url; this.sharePopupVisible = true; diff --git a/src/main.ts b/src/main.ts index 664a057..4714619 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,22 @@ import { mount } from 'svelte' +import './theme.css' import './app.css' import App from './App.svelte' +// Apply saved theme before mounting app +const STORAGE_KEY = 'editorSettings'; +const stored = localStorage.getItem(STORAGE_KEY); +if (stored) { + try { + const settings = JSON.parse(stored); + if (settings.theme) { + document.documentElement.dataset.theme = settings.theme; + } + } catch (e) { + console.error('Failed to parse editor settings:', e); + } +} + const app = mount(App, { target: document.getElementById('app')!, }) diff --git a/src/theme.css b/src/theme.css new file mode 100644 index 0000000..369a708 --- /dev/null +++ b/src/theme.css @@ -0,0 +1,169 @@ +/* Theme System */ + +/* Dark Theme - Default */ +:root, +[data-theme="dark"] { + /* Base Colors */ + --color-bg-base: #1a1a1a; + --color-bg-elevated: #242424; + --color-bg-overlay: #2a2a2a; + --color-bg-input: #1e1e1e; + + /* Surface Colors */ + --color-surface-0: #1a1a1a; + --color-surface-1: #202020; + --color-surface-2: #252525; + --color-surface-3: #2a2a2a; + + /* Text Colors */ + --color-text-primary: rgba(255, 255, 255, 0.87); + --color-text-secondary: rgba(255, 255, 255, 0.6); + --color-text-tertiary: rgba(255, 255, 255, 0.4); + --color-text-disabled: rgba(255, 255, 255, 0.25); + + /* Accent Colors */ + --color-accent-primary: #646cff; + --color-accent-primary-hover: #818cf8; + --color-accent-primary-active: #535bf2; + --color-accent-primary-subtle: rgba(100, 108, 255, 0.1); + + /* Semantic Colors */ + --color-success: #10b981; + --color-warning: #fbbf24; + --color-error: #ef4444; + --color-info: #60a5fa; + + /* Border Colors */ + --color-border-subtle: rgba(255, 255, 255, 0.06); + --color-border-default: rgba(255, 255, 255, 0.1); + --color-border-strong: rgba(255, 255, 255, 0.2); + --color-border-accent: var(--color-accent-primary); + + /* Interactive States */ + --color-hover-overlay: rgba(255, 255, 255, 0.05); + --color-active-overlay: rgba(255, 255, 255, 0.08); + --color-selected-overlay: rgba(100, 108, 255, 0.15); + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2); + --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.4); + + /* Component Specific */ + --accordion-header-bg: transparent; + --accordion-header-hover: var(--color-hover-overlay); + --accordion-border: var(--color-border-subtle); + + --input-bg: var(--color-bg-input); + --input-border: var(--color-border-default); + --input-border-focus: var(--color-accent-primary); + + --tooltip-bg: rgba(32, 32, 32, 0.98); + --tooltip-border: var(--color-border-default); + + --code-bg: transparent; + --code-border: var(--color-border-subtle); + --code-text: var(--color-text-secondary); +} + +/* Light Theme */ +[data-theme="light"] { + /* Base Colors */ + --color-bg-base: #fafafa; + --color-bg-elevated: #ffffff; + --color-bg-overlay: #f5f5f5; + --color-bg-input: #ffffff; + + /* Surface Colors */ + --color-surface-0: #fafafa; + --color-surface-1: #ffffff; + --color-surface-2: #f5f5f5; + --color-surface-3: #eeeeee; + + /* Text Colors */ + --color-text-primary: rgba(0, 0, 0, 0.87); + --color-text-secondary: rgba(0, 0, 0, 0.6); + --color-text-tertiary: rgba(0, 0, 0, 0.4); + --color-text-disabled: rgba(0, 0, 0, 0.25); + + /* Accent Colors */ + --color-accent-primary: #5865f2; + --color-accent-primary-hover: #4752c4; + --color-accent-primary-active: #3c45a5; + --color-accent-primary-subtle: rgba(88, 101, 242, 0.1); + + /* Semantic Colors */ + --color-success: #059669; + --color-warning: #d97706; + --color-error: #dc2626; + --color-info: #2563eb; + + /* Border Colors */ + --color-border-subtle: rgba(0, 0, 0, 0.08); + --color-border-default: rgba(0, 0, 0, 0.12); + --color-border-strong: rgba(0, 0, 0, 0.24); + --color-border-accent: var(--color-accent-primary); + + /* Interactive States */ + --color-hover-overlay: rgba(0, 0, 0, 0.04); + --color-active-overlay: rgba(0, 0, 0, 0.08); + --color-selected-overlay: rgba(88, 101, 242, 0.12); + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.08); + --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.12); + --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.15); + + /* Component Specific */ + --accordion-header-bg: transparent; + --accordion-header-hover: var(--color-hover-overlay); + --accordion-border: var(--color-border-subtle); + + --input-bg: var(--color-bg-input); + --input-border: var(--color-border-default); + --input-border-focus: var(--color-accent-primary); + + --tooltip-bg: rgba(255, 255, 255, 0.98); + --tooltip-border: var(--color-border-default); + + --code-bg: transparent; + --code-border: var(--color-border-subtle); + --code-text: var(--color-text-secondary); +} + +/* Shared Variables */ +:root { + /* Spacing Scale */ + --space-xs: 0.25rem; + --space-sm: 0.5rem; + --space-md: 0.75rem; + --space-lg: 1rem; + --space-xl: 1.5rem; + --space-2xl: 2rem; + + /* Border Radius */ + --radius-sm: 2px; + --radius-md: 4px; + --radius-lg: 6px; + + /* Font Sizes */ + --font-xs: 0.6875rem; + --font-sm: 0.75rem; + --font-md: 0.8125rem; + --font-base: 0.875rem; + --font-lg: 1rem; + + /* Font Families */ + --font-mono: 'Courier New', Monaco, Consolas, monospace; + --font-sans: system-ui, -apple-system, sans-serif; + + /* Line Heights */ + --leading-tight: 1.4; + --leading-normal: 1.5; + --leading-relaxed: 1.6; + + /* Transitions */ + --transition-fast: 100ms ease; + --transition-base: 200ms ease; + --transition-slow: 300ms ease; +}