This commit is contained in:
2025-11-14 14:02:28 +01:00
parent 5a388e7f14
commit 93205f3f5b
8 changed files with 554 additions and 136 deletions

View File

@ -1,6 +1,9 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import TopBar from "./lib/components/ui/TopBar.svelte";
import MenuBar from "./lib/components/ui/MenuBar.svelte";
import MenuItem from "./lib/components/ui/MenuItem.svelte";
import MenuAction from "./lib/components/ui/MenuAction.svelte";
import AboutDialog from "./lib/components/ui/AboutDialog.svelte";
import EditorWithLogs from "./lib/components/editor/EditorWithLogs.svelte";
import EditorSettings from "./lib/components/editor/EditorSettings.svelte";
import FileBrowser from "./lib/components/ui/FileBrowser.svelte";
@ -261,93 +264,76 @@
{/snippet}
<div class="app-container">
<TopBar title="OldBoy">
{#snippet leftActions()}
<button
onclick={handleStop}
class="icon-button stop-button"
disabled={!$running}
title="Stop audio (Ctrl+.)"
>
<CircleStop size={18} />
</button>
<button
class="icon-button"
<MenuBar onLogoClick={() => uiState.showAboutDialog()}>
<MenuItem label="File">
<MenuAction
label="New File"
icon={FileStack}
onclick={handleNewFile}
title="New file"
>
<FileStack size={18} />
</button>
<button
class="icon-button"
onclick={handleSave}
/>
<MenuAction
label="Save"
icon={Save}
shortcut={navigator.platform.includes("Mac") ? "Cmd+S" : "Ctrl+S"}
disabled={!editorState.hasUnsavedChanges}
title="Save (Ctrl+S){editorState.hasUnsavedChanges
? ' - unsaved changes'
: ''}"
class:has-changes={editorState.hasUnsavedChanges}
>
<Save size={18} />
</button>
<button
onclick={handleShare}
class="icon-button"
onclick={handleSave}
/>
<MenuAction
label="Share..."
icon={Share2}
disabled={!editorState.currentFileId}
title="Share current file"
>
<Share2 size={18} />
</button>
{/snippet}
<button
onclick={handleExecuteFile}
class="icon-button evaluate-button"
onclick={handleShare}
/>
</MenuItem>
<MenuItem label="Audio">
<MenuAction
label="Run File"
icon={Play}
shortcut={navigator.platform.includes("Mac") ? "Cmd+R" : "Ctrl+R"}
disabled={!$initialized}
class:is-running={$running}
title="Run File (Cmd-R)"
>
<Play size={18} />
</button>
<button
onclick={() => uiState.toggleScope()}
class="icon-button"
title="Toggle audio scope"
>
<Activity size={18} />
</button>
<button
onclick={() => uiState.toggleSpectrogram()}
class="icon-button"
title="Toggle spectrogram"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="18" height="18" rx="2" />
<path d="M3 9h18" />
<path d="M3 15h18" />
<path d="M9 3v18" />
<path d="M15 3v18" />
</svg>
</button>
{#if !uiState.sidePanelVisible}
<button
class="icon-button"
onclick={handleExecuteFile}
/>
<MenuAction
label="Stop"
icon={CircleStop}
shortcut={navigator.platform.includes("Mac") ? "Cmd+." : "Ctrl+."}
disabled={!$running}
onclick={handleStop}
/>
</MenuItem>
<MenuItem label="Visuals">
<MenuAction
label="Toggle Side Panel"
icon={uiState.sidePanelPosition === "left" ? PanelLeftOpen : PanelRightOpen}
onclick={() => uiState.toggleSidePanel()}
title="Open side panel"
>
{#if uiState.sidePanelPosition === "left"}
<PanelLeftOpen size={18} />
{:else}
<PanelRightOpen size={18} />
{/if}
</button>
{/if}
</TopBar>
/>
<MenuAction
label="Audio Scope"
icon={Activity}
onclick={() => uiState.toggleScope()}
/>
<MenuAction
label="Spectrogram"
onclick={() => uiState.toggleSpectrogram()}
/>
</MenuItem>
<MenuItem label="Options">
<MenuAction
label="Editor Settings"
onclick={() => {}}
/>
</MenuItem>
<MenuItem label="About">
<MenuAction
label="About OldBoy..."
onclick={() => uiState.showAboutDialog()}
/>
</MenuItem>
</MenuBar>
<div class="main-content">
{#if uiState.sidePanelPosition === "left"}
@ -473,6 +459,11 @@
{/snippet}
</ResizablePopup>
<AboutDialog
isOpen={uiState.aboutDialogVisible}
onClose={() => uiState.hideAboutDialog()}
/>
</div>
<style>
@ -495,49 +486,6 @@
overflow: hidden;
}
.icon-button {
padding: var(--space-sm);
background-color: var(--border-color);
color: var(--text-color);
border: 1px solid var(--border-color);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color var(--transition-base);
}
.icon-button:hover:not(:disabled) {
background-color: var(--button-hover);
border-color: var(--accent-color);
}
.icon-button:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.icon-button.has-changes {
color: var(--accent-color);
}
.icon-button.stop-button:not(:disabled) {
color: var(--danger-color);
}
.icon-button.stop-button:hover:not(:disabled) {
color: var(--danger-hover);
border-color: var(--danger-hover);
}
.icon-button.evaluate-button.is-running {
color: var(--accent-color);
}
.icon-button.evaluate-button.is-running:hover:not(:disabled) {
border-color: var(--accent-color);
}
h3 {
margin-top: 0;
color: var(--text-color);

View File

@ -0,0 +1,165 @@
<script lang="ts">
import ResizablePopup from './ResizablePopup.svelte';
import { Code2 } from 'lucide-svelte';
interface Props {
isOpen: boolean;
onClose: () => void;
}
let { isOpen, onClose }: Props = $props();
const version = '0.0.0';
const author = 'Raphaël Maurice Forment';
const license = 'MIT License';
const website = 'https://raphaelforment.fr';
const livecoding = 'https://livecoding.fr';
</script>
{#if isOpen}
<ResizablePopup
bind:visible={isOpen}
title="About OldBoy"
x={typeof window !== 'undefined' ? window.innerWidth / 2 - 250 : 250}
y={typeof window !== 'undefined' ? window.innerHeight / 2 - 250 : 250}
width={500}
height={500}
minWidth={500}
minHeight={500}
closable={true}
onClose={onClose}
>
{#snippet children()}
<div class="about-content">
<div class="about-logo">
<Code2 size={64} strokeWidth={1.5} />
</div>
<div class="about-info">
<h1 class="app-name">OldBoy</h1>
<div class="description">
<p>
A modern web-based Csound editor and development environment. Write,
execute, and experiment with Csound code directly in your browser.
</p>
</div>
<div class="author-section">
<div class="links">
<a href={website} target="_blank" rel="noopener noreferrer">
raphaelforment.fr
</a>
<a href={livecoding} target="_blank" rel="noopener noreferrer">
livecoding.fr
</a>
</div>
</div>
<div class="footer">
<p>&copy; 2025 {author} · {license}</p>
</div>
</div>
</div>
{/snippet}
</ResizablePopup>
{/if}
<style>
.about-content {
padding: 32px;
overflow-y: auto;
height: 100%;
}
.about-logo {
display: flex;
justify-content: center;
margin-bottom: 24px;
color: var(--accent-color);
}
.about-info {
text-align: center;
}
.app-name {
font-size: 28px;
margin: 0 0 8px 0;
color: var(--text-color);
font-weight: 600;
}
.version {
font-size: var(--font-sm);
color: var(--text-secondary);
margin: 0 0 24px 0;
}
.description {
margin-bottom: 32px;
text-align: center;
padding: 0 16px;
}
.description p {
color: var(--text-color);
line-height: 1.6;
margin: 0;
}
.author-section {
margin-bottom: 32px;
text-align: center;
}
.author-section h3 {
font-size: var(--font-base);
color: var(--text-color);
margin: 0 0 12px 0;
font-weight: 600;
}
.author-name {
color: var(--text-color);
font-size: var(--font-base);
margin: 0 0 12px 0;
}
.links {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
max-width: 400px;
margin: 0 auto;
}
.links a {
color: var(--accent-color);
text-decoration: none;
font-size: var(--font-sm);
transition: opacity var(--transition-fast);
text-align: center;
padding: 8px 16px;
border: 1px solid var(--border-color);
background-color: var(--surface-color);
}
.links a:hover {
opacity: 0.8;
border-color: var(--accent-color);
}
.footer {
margin-top: 32px;
padding-top: 16px;
border-top: 1px solid var(--border-color);
text-align: center;
}
.footer p {
font-size: var(--font-xs);
color: var(--text-secondary);
margin: 0;
}
</style>

View File

@ -0,0 +1,82 @@
<script lang="ts">
import type { ComponentType } from 'svelte';
interface Props {
label: string;
icon?: ComponentType;
shortcut?: string;
disabled?: boolean;
onclick?: () => void;
}
let { label, icon, shortcut, disabled = false, onclick }: Props = $props();
</script>
<button
class="menu-action"
class:disabled
{disabled}
onclick={() => !disabled && onclick?.()}
type="button"
>
<span class="menu-action-content">
{#if icon}
{@const Icon = icon}
<Icon size={16} />
{/if}
<span class="menu-action-label">{label}</span>
</span>
{#if shortcut}
<span class="menu-action-shortcut">{shortcut}</span>
{/if}
</button>
<style>
.menu-action {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 6px 12px;
background: transparent;
border: none;
color: var(--text-color);
font-size: var(--font-sm);
cursor: pointer;
transition: background-color var(--transition-fast);
text-align: left;
gap: 24px;
}
.menu-action:hover:not(.disabled) {
background-color: var(--accent-color);
color: var(--accent-text);
}
.menu-action.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.menu-action-content {
display: flex;
align-items: center;
gap: 8px;
}
.menu-action-label {
white-space: nowrap;
}
.menu-action-shortcut {
font-size: var(--font-xs);
color: var(--text-secondary);
opacity: 0.7;
white-space: nowrap;
}
.menu-action:hover:not(.disabled) .menu-action-shortcut {
color: var(--accent-text);
opacity: 0.9;
}
</style>

View File

@ -0,0 +1,65 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { Code2 } from 'lucide-svelte';
interface Props {
children: Snippet;
onLogoClick?: () => void;
}
let { children, onLogoClick }: Props = $props();
</script>
<div class="menu-bar">
<div class="menu-bar-left">
<button class="logo-button" onclick={onLogoClick} type="button">
<Code2 size={20} />
<span class="logo-text">OldBoy</span>
</button>
{@render children()}
</div>
</div>
<style>
.menu-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 48px;
background-color: var(--bg-color);
border-bottom: 1px solid var(--border-color);
padding: 0;
flex-shrink: 0;
}
.menu-bar-left {
display: flex;
align-items: center;
height: 100%;
}
.logo-button {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: transparent;
border: none;
color: var(--text-color);
font-size: var(--font-base);
font-weight: 600;
cursor: pointer;
transition: background-color var(--transition-fast);
height: 100%;
border-right: 1px solid var(--border-color);
}
.logo-button:hover {
background-color: var(--surface-color);
color: var(--accent-color);
}
.logo-text {
user-select: none;
}
</style>

View File

@ -0,0 +1,79 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { Snippet } from 'svelte';
interface Props {
isOpen: boolean;
onClose: () => void;
children: Snippet;
anchorElement?: HTMLElement;
}
let { isOpen = $bindable(), onClose, children, anchorElement }: Props = $props();
let dropdownElement: HTMLDivElement;
function handleClickOutside(event: MouseEvent) {
if (
isOpen &&
dropdownElement &&
!dropdownElement.contains(event.target as Node) &&
anchorElement &&
!anchorElement.contains(event.target as Node)
) {
onClose();
}
}
function handleEscapeKey(event: KeyboardEvent) {
if (isOpen && event.key === 'Escape') {
onClose();
}
}
$effect(() => {
if (isOpen) {
document.addEventListener('click', handleClickOutside, true);
document.addEventListener('keydown', handleEscapeKey);
return () => {
document.removeEventListener('click', handleClickOutside, true);
document.removeEventListener('keydown', handleEscapeKey);
};
}
});
function positionDropdown() {
if (!dropdownElement || !anchorElement) return;
const anchorRect = anchorElement.getBoundingClientRect();
dropdownElement.style.top = `${anchorRect.bottom}px`;
dropdownElement.style.left = `${anchorRect.left}px`;
}
$effect(() => {
if (isOpen && anchorElement) {
positionDropdown();
}
});
</script>
{#if isOpen}
<div class="menu-dropdown" bind:this={dropdownElement}>
{@render children()}
</div>
{/if}
<style>
.menu-dropdown {
position: fixed;
z-index: 1000;
background-color: var(--surface-color);
border: 1px solid var(--border-color);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
min-width: 200px;
max-width: 300px;
padding: 4px 0;
display: flex;
flex-direction: column;
}
</style>

View File

@ -0,0 +1,67 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import MenuDropdown from './MenuDropdown.svelte';
interface Props {
label: string;
children: Snippet;
}
let { label, children }: Props = $props();
let isOpen = $state(false);
let buttonElement: HTMLButtonElement;
function toggleMenu() {
isOpen = !isOpen;
}
function closeMenu() {
isOpen = false;
}
</script>
<div class="menu-item">
<button
bind:this={buttonElement}
class="menu-item-trigger"
class:active={isOpen}
onclick={toggleMenu}
type="button"
>
{label}
</button>
<MenuDropdown {isOpen} onClose={closeMenu} anchorElement={buttonElement}>
<div onclick={closeMenu}>
{@render children()}
</div>
</MenuDropdown>
</div>
<style>
.menu-item {
position: relative;
}
.menu-item-trigger {
background: transparent;
border: none;
color: var(--text-color);
font-size: var(--font-sm);
padding: 12px 16px;
cursor: pointer;
transition: background-color var(--transition-fast);
height: 100%;
display: flex;
align-items: center;
}
.menu-item-trigger:hover,
.menu-item-trigger.active {
background-color: var(--surface-color);
}
.menu-item-trigger.active {
color: var(--accent-color);
}
</style>

View File

@ -1,5 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { X } from 'lucide-svelte';
interface Props {
title?: string;
@ -126,7 +127,9 @@
<div class="popup-header" onmousedown={handleDragStart}>
<span class="popup-title">{title}</span>
{#if closable}
<button class="close-button" onclick={handleClose}>×</button>
<button class="close-button" onclick={handleClose}>
<X size={16} />
</button>
{/if}
</div>
<div class="popup-content" class:no-padding={noPadding}>
@ -159,11 +162,12 @@
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-md) var(--space-lg);
padding: 8px 16px;
background-color: var(--surface-color);
border-bottom: 1px solid var(--border-color);
cursor: move;
user-select: none;
min-height: 36px;
}
.popup-title {
@ -173,23 +177,22 @@
}
.close-button {
background: none;
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 1.5rem;
line-height: 1;
padding: 0;
padding: 4px;
cursor: pointer;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: color var(--transition-base);
transition: all var(--transition-fast);
color: var(--text-secondary);
}
.close-button:hover {
color: var(--text-color);
background-color: var(--accent-color);
color: var(--accent-text);
}
.popup-content {

View File

@ -9,6 +9,7 @@ export class UIState {
sharePopupVisible = $state(false);
audioPermissionPopupVisible = $state(true);
unsavedChangesDialogVisible = $state(false);
aboutDialogVisible = $state(false);
shareUrl = $state('');
@ -56,4 +57,12 @@ export class UIState {
hideUnsavedChangesDialog() {
this.unsavedChangesDialogVisible = false;
}
showAboutDialog() {
this.aboutDialogVisible = true;
}
hideAboutDialog() {
this.aboutDialogVisible = false;
}
}