Skip to content

Commit bdad615

Browse files
committed
feat: add audio preferences management and integrate presence sound features
1 parent 4685c32 commit bdad615

9 files changed

Lines changed: 521 additions & 121 deletions

File tree

src/App.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useServerStatus } from './hooks/useServerStatus';
88
import { useToast } from './hooks/useToast';
99
import { usePeerPresenceToasts } from './hooks/usePeerPresenceToasts';
1010
import { useFollowCollaborator } from './hooks/useFollowCollaborator';
11+
import { useAudioPreferences } from './hooks/useAudioPreferences';
1112
import { useVirtualFS } from './hooks/useVirtualFS';
1213
import { useWorkspaceImport } from './hooks/useWorkspaceImport';
1314
import { useWorkspaceLayout } from './hooks/useWorkspaceLayout';
@@ -64,6 +65,12 @@ function AppContent({
6465
const [dismissedServerBannerKey, setDismissedServerBannerKey] = useState<string | null>(null);
6566
const [helpInitialTab, setHelpInitialTab] = useState<HelpModalTab>('about');
6667
const { toasts, pushToast, dismissToast } = useToast();
68+
const {
69+
presenceSoundsEnabled,
70+
presenceSoundVolume,
71+
setPresenceSoundsEnabled,
72+
setPresenceSoundVolume,
73+
} = useAudioPreferences();
6774
const layout = useWorkspaceLayout({
6875
fs,
6976
editorRef,
@@ -160,6 +167,8 @@ function AppContent({
160167
awareness,
161168
connected,
162169
pushToast,
170+
presenceSoundsEnabled,
171+
presenceSoundVolume,
163172
});
164173

165174
const serverStatus = useServerStatus({ syncStatus: connectionStatus });
@@ -244,6 +253,10 @@ function AppContent({
244253
onSaveAll={handleSaveAll}
245254
onFontSizeUp={layout.handleFontSizeUp}
246255
onFontSizeDown={layout.handleFontSizeDown}
256+
presenceSoundsEnabled={presenceSoundsEnabled}
257+
presenceSoundVolume={presenceSoundVolume}
258+
onPresenceSoundsEnabledChange={setPresenceSoundsEnabled}
259+
onPresenceSoundVolumeChange={setPresenceSoundVolume}
247260
onHelpOpen={() => openHelp('about')}
248261
/>
249262

src/components/Icons.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,26 @@ export function MoonIcon(props: IconProps) {
103103

104104
// ── Terminal / Prompt ──
105105

106+
export function Volume2Icon(props: IconProps) {
107+
return (
108+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" {...props}>
109+
<path d="M11 5 6 9H3v6h3l5 4V5Z" strokeLinecap="round" strokeLinejoin="round" />
110+
<path d="M15.5 8.5a5 5 0 0 1 0 7" strokeLinecap="round" />
111+
<path d="M18.5 6a8.5 8.5 0 0 1 0 12" strokeLinecap="round" />
112+
</svg>
113+
);
114+
}
115+
116+
export function VolumeXIcon(props: IconProps) {
117+
return (
118+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" {...props}>
119+
<path d="M11 5 6 9H3v6h3l5 4V5Z" strokeLinecap="round" strokeLinejoin="round" />
120+
<path d="m16 9 5 5" strokeLinecap="round" />
121+
<path d="m21 9-5 5" strokeLinecap="round" />
122+
</svg>
123+
);
124+
}
125+
106126
export function TerminalIcon(props: IconProps) {
107127
return (
108128
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" {...props}>

src/components/ThemePicker.css

Lines changed: 142 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,175 @@
1-
.cc-theme-option {
1+
.cc-theme-picker {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 0.55rem;
5+
min-width: 0;
6+
}
7+
8+
.cc-theme-picker-compact {
9+
flex-direction: row;
10+
align-items: center;
11+
gap: 0.45rem;
12+
}
13+
14+
.cc-theme-switch {
15+
display: grid;
16+
grid-template-columns: repeat(2, minmax(0, 1fr));
17+
gap: 0.2rem;
18+
padding: 0.22rem;
19+
border: 1px solid var(--cc-border);
20+
border-radius: 0.95rem;
21+
background: color-mix(in srgb, var(--cc-bg-elevated) 88%, transparent);
22+
min-width: 0;
23+
}
24+
25+
.cc-theme-choice {
26+
display: inline-flex;
27+
align-items: center;
28+
justify-content: center;
29+
gap: 0.45rem;
30+
border: 0;
31+
border-radius: 0.75rem;
32+
background: transparent;
33+
color: var(--cc-text-muted);
34+
padding: 0.48rem 0.72rem;
35+
font-size: 0.74rem;
36+
font-weight: 600;
37+
cursor: pointer;
238
transition:
339
background-color 140ms ease,
4-
border-color 140ms ease,
540
color 140ms ease,
641
transform 140ms ease,
7-
opacity 140ms ease;
42+
box-shadow 140ms ease;
843
}
944

10-
.cc-theme-picker {
11-
display: inline-flex;
12-
flex-wrap: wrap;
13-
align-items: center;
14-
gap: 0.35rem;
45+
.cc-theme-choice:hover {
46+
color: var(--cc-text-primary);
1547
}
1648

17-
.cc-theme-picker-compact {
18-
gap: 0.25rem;
49+
.cc-theme-choice-active {
50+
background: color-mix(in srgb, var(--cc-bg-selection) 78%, var(--cc-bg-elevated) 22%);
51+
color: var(--cc-text-primary);
52+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cc-accent) 32%, transparent);
1953
}
2054

21-
.cc-theme-option {
55+
.cc-theme-picker-compact .cc-theme-choice {
56+
min-width: 2.15rem;
57+
padding: 0.4rem 0.55rem;
58+
}
59+
60+
.cc-theme-choice-icon {
61+
height: 0.85rem;
62+
width: 0.85rem;
63+
flex: none;
64+
}
65+
66+
.cc-theme-choice-label {
67+
min-width: 0;
68+
white-space: nowrap;
69+
}
70+
71+
.cc-theme-auto {
72+
position: relative;
2273
display: inline-flex;
2374
align-items: center;
24-
gap: 0.45rem;
75+
gap: 0.55rem;
2576
border: 1px solid var(--cc-border);
26-
background: color-mix(in srgb, var(--cc-bg-elevated) 90%, transparent);
77+
border-radius: 0.95rem;
78+
background: color-mix(in srgb, var(--cc-bg-elevated) 92%, transparent);
2779
color: var(--cc-text-secondary);
28-
border-radius: 999px;
29-
padding: 0.35rem 0.65rem;
30-
font-size: 0.72rem;
31-
font-weight: 600;
80+
padding: 0.55rem 0.75rem;
3281
cursor: pointer;
82+
min-width: 0;
83+
transition:
84+
border-color 140ms ease,
85+
background-color 140ms ease,
86+
color 140ms ease,
87+
transform 140ms ease;
3388
}
3489

35-
.cc-theme-option:hover {
36-
background: var(--cc-bg-hover);
90+
.cc-theme-auto:hover {
3791
color: var(--cc-text-primary);
92+
background: var(--cc-bg-hover);
3893
}
3994

40-
.cc-theme-option-active {
41-
border-color: var(--cc-accent);
42-
background: var(--cc-bg-selection);
95+
.cc-theme-auto-active {
96+
border-color: color-mix(in srgb, var(--cc-accent) 40%, transparent);
97+
background: color-mix(in srgb, var(--cc-bg-selection) 76%, var(--cc-bg-elevated) 24%);
4398
color: var(--cc-text-primary);
4499
}
45100

46-
.cc-theme-picker-compact .cc-theme-option {
47-
padding: 0.3rem;
101+
.cc-theme-picker-compact .cc-theme-auto {
102+
flex: none;
103+
gap: 0.45rem;
104+
padding: 0.4rem 0.6rem;
105+
}
106+
107+
.cc-theme-auto-input {
108+
position: absolute;
109+
inset: 0;
110+
opacity: 0;
111+
cursor: pointer;
48112
}
49113

50-
.cc-theme-icon-wrap {
114+
.cc-theme-auto-box {
51115
display: inline-flex;
52116
align-items: center;
53117
justify-content: center;
54-
height: 1.15rem;
55-
width: 1.15rem;
56-
border-radius: 999px;
57-
background: color-mix(in srgb, currentColor 12%, transparent);
118+
width: 1rem;
119+
height: 1rem;
120+
flex: none;
121+
border: 1px solid var(--cc-border-strong);
122+
border-radius: 0.32rem;
123+
background: color-mix(in srgb, var(--cc-bg-panel) 84%, transparent);
124+
color: var(--cc-accent-contrast);
125+
transition:
126+
border-color 140ms ease,
127+
background-color 140ms ease;
128+
}
129+
130+
.cc-theme-auto-box-checked {
131+
border-color: var(--cc-accent);
132+
background: var(--cc-accent);
133+
}
134+
135+
.cc-theme-auto-check {
136+
width: 0.72rem;
137+
height: 0.72rem;
138+
}
139+
140+
.cc-theme-auto-icon {
141+
width: 0.85rem;
142+
height: 0.85rem;
143+
flex: none;
144+
}
145+
146+
.cc-theme-auto-copy {
147+
display: flex;
148+
flex-direction: column;
149+
min-width: 0;
150+
}
151+
152+
.cc-theme-auto-title {
153+
font-size: 0.72rem;
154+
font-weight: 600;
155+
line-height: 1.1;
156+
color: var(--cc-text-primary);
157+
white-space: nowrap;
158+
}
159+
160+
.cc-theme-auto-subtitle {
161+
font-size: 0.64rem;
162+
line-height: 1.15;
163+
color: var(--cc-text-muted);
58164
}
59165

60-
.cc-theme-option-active .cc-theme-icon-wrap {
61-
background: color-mix(in srgb, var(--cc-accent) 18%, transparent);
166+
.cc-theme-picker-compact .cc-theme-auto-title {
167+
font-size: 0.68rem;
62168
}
63169

64-
.cc-theme-icon {
65-
height: 0.8rem;
66-
width: 0.8rem;
170+
.cc-theme-status {
171+
padding-left: 0.1rem;
172+
font-size: 0.65rem;
173+
line-height: 1.3;
174+
color: var(--cc-text-muted);
67175
}

src/components/ThemePicker.tsx

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import { useTheme } from '../theme/ThemeProvider';
22
import type { AppearancePreference } from '../theme/themes';
3-
import { MonitorIcon, MoonIcon, SunIcon } from './Icons';
3+
import { CheckIcon, MonitorIcon, MoonIcon, SunIcon } from './Icons';
44
import './ThemePicker.css';
55

66
interface ThemePickerProps {
77
compact?: boolean;
88
className?: string;
99
}
1010

11-
const appearanceOptions = [
12-
{ id: 'system', label: 'Auto', Icon: MonitorIcon },
11+
const themeChoices = [
1312
{ id: 'light', label: 'Light', Icon: SunIcon },
1413
{ id: 'dark', label: 'Dark', Icon: MoonIcon },
1514
] as const satisfies ReadonlyArray<{
16-
id: AppearancePreference;
15+
id: Exclude<AppearancePreference, 'system'>;
1716
label: string;
1817
Icon: typeof SunIcon;
1918
}>;
@@ -22,33 +21,75 @@ export default function ThemePicker({
2221
compact = false,
2322
className = '',
2423
}: ThemePickerProps) {
25-
const { appearance, setAppearance } = useTheme();
24+
const { appearance, theme, setAppearance } = useTheme();
25+
const autoEnabled = appearance === 'system';
26+
const effectiveAppearance: Exclude<AppearancePreference, 'system'> = autoEnabled
27+
? theme.colorScheme
28+
: appearance;
29+
30+
const handleAutoChange = (enabled: boolean) => {
31+
if (enabled) {
32+
setAppearance('system');
33+
return;
34+
}
35+
36+
setAppearance(effectiveAppearance);
37+
};
2638

2739
return (
2840
<div
2941
className={`cc-theme-picker ${compact ? 'cc-theme-picker-compact' : ''} ${className}`.trim()}
30-
role="group"
31-
aria-label="Select appearance"
3242
>
33-
{appearanceOptions.map((option) => {
34-
const isActive = option.id === appearance;
35-
36-
return (
37-
<button
38-
key={option.id}
39-
type="button"
40-
onClick={() => setAppearance(option.id)}
41-
title={`Use ${option.label} appearance`}
42-
aria-pressed={isActive}
43-
className={`cc-theme-option ${isActive ? 'cc-theme-option-active' : ''}`}
44-
>
45-
<span className="cc-theme-icon-wrap" aria-hidden="true">
46-
<option.Icon className="cc-theme-icon" />
47-
</span>
48-
{!compact && <span className="truncate">{option.label}</span>}
49-
</button>
50-
);
51-
})}
43+
<div className="cc-theme-switch" role="radiogroup" aria-label="Select appearance">
44+
{themeChoices.map((option) => {
45+
const isActive = option.id === effectiveAppearance;
46+
47+
return (
48+
<button
49+
key={option.id}
50+
type="button"
51+
role="radio"
52+
aria-checked={isActive}
53+
title={`Use ${option.label} appearance`}
54+
onClick={() => setAppearance(option.id)}
55+
className={`cc-theme-choice ${isActive ? 'cc-theme-choice-active' : ''}`}
56+
>
57+
<option.Icon className="cc-theme-choice-icon" />
58+
{!compact && <span className="cc-theme-choice-label">{option.label}</span>}
59+
</button>
60+
);
61+
})}
62+
</div>
63+
64+
<label className={`cc-theme-auto ${autoEnabled ? 'cc-theme-auto-active' : ''}`}>
65+
<input
66+
type="checkbox"
67+
checked={autoEnabled}
68+
onChange={(event) => handleAutoChange(event.target.checked)}
69+
className="cc-theme-auto-input"
70+
/>
71+
<span
72+
className={`cc-theme-auto-box ${autoEnabled ? 'cc-theme-auto-box-checked' : ''}`}
73+
aria-hidden="true"
74+
>
75+
{autoEnabled && <CheckIcon className="cc-theme-auto-check" />}
76+
</span>
77+
<MonitorIcon className="cc-theme-auto-icon" />
78+
{compact ? (
79+
<span className="cc-theme-auto-title">Auto</span>
80+
) : (
81+
<span className="cc-theme-auto-copy">
82+
<span className="cc-theme-auto-title">Auto</span>
83+
<span className="cc-theme-auto-subtitle">Follow your system theme</span>
84+
</span>
85+
)}
86+
</label>
87+
88+
{!compact && (
89+
<p className="cc-theme-status" aria-live="polite">
90+
{autoEnabled ? `Currently following ${theme.colorScheme} mode.` : `Locked to ${effectiveAppearance} mode.`}
91+
</p>
92+
)}
5293
</div>
5394
);
5495
}

0 commit comments

Comments
 (0)