mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
exp(orbit): start modal, paste-to-join, suggest track from context menu
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Play, ListPlus, Radio, Heart, Download, ChevronRight, User, Disc3, ListMusic, Plus, Info, Sparkles, Star, Trash2, HeartCrack, Share2 } from 'lucide-react';
|
import { Play, ListPlus, Radio, Heart, Download, ChevronRight, User, Disc3, ListMusic, Plus, Info, Sparkles, Star, Trash2, HeartCrack, Share2, Orbit as OrbitIcon } from 'lucide-react';
|
||||||
|
import { useOrbitStore } from '../store/orbitStore';
|
||||||
|
import { suggestOrbitTrack } from '../utils/orbit';
|
||||||
import LastfmIcon from './LastfmIcon';
|
import LastfmIcon from './LastfmIcon';
|
||||||
import StarRating from './StarRating';
|
import StarRating from './StarRating';
|
||||||
import { lastfmLoveTrack, lastfmUnloveTrack } from '../api/lastfm';
|
import { lastfmLoveTrack, lastfmUnloveTrack } from '../api/lastfm';
|
||||||
@@ -962,6 +964,7 @@ function MultiPlaylistToPlaylistSubmenu({ playlists, onDone, triggerId }: { play
|
|||||||
|
|
||||||
export default function ContextMenu() {
|
export default function ContextMenu() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const orbitRole = useOrbitStore(s => s.role);
|
||||||
const { contextMenu, closeContextMenu, playTrack, enqueue, queue, currentTrack, removeTrack, lastfmLovedCache, setLastfmLovedForSong, starredOverrides, setStarredOverride, openSongInfo, userRatingOverrides, setUserRatingOverride } = usePlayerStore(
|
const { contextMenu, closeContextMenu, playTrack, enqueue, queue, currentTrack, removeTrack, lastfmLovedCache, setLastfmLovedForSong, starredOverrides, setStarredOverride, openSongInfo, userRatingOverrides, setUserRatingOverride } = usePlayerStore(
|
||||||
useShallow(s => ({
|
useShallow(s => ({
|
||||||
contextMenu: s.contextMenu,
|
contextMenu: s.contextMenu,
|
||||||
@@ -1443,6 +1446,15 @@ export default function ContextMenu() {
|
|||||||
<div className="context-menu-item" onClick={() => handleAction(() => enqueue([song]))}>
|
<div className="context-menu-item" onClick={() => handleAction(() => enqueue([song]))}>
|
||||||
<ListPlus size={14} /> {t('contextMenu.addToQueue')}
|
<ListPlus size={14} /> {t('contextMenu.addToQueue')}
|
||||||
</div>
|
</div>
|
||||||
|
{orbitRole === 'guest' && (
|
||||||
|
<div className="context-menu-item" onClick={() => handleAction(() => {
|
||||||
|
suggestOrbitTrack(song.id)
|
||||||
|
.then(() => showToast('Suggested to session', 2200, 'info'))
|
||||||
|
.catch(() => showToast('Couldn\'t suggest — not joined', 3000, 'error'));
|
||||||
|
})}>
|
||||||
|
<OrbitIcon size={14} /> Add to session
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === song.id ? 'active' : ''}`}
|
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === song.id ? 'active' : ''}`}
|
||||||
data-playlist-trigger-id={song.id}
|
data-playlist-trigger-id={song.id}
|
||||||
@@ -1575,6 +1587,15 @@ export default function ContextMenu() {
|
|||||||
<div className="context-menu-item" onClick={() => handleAction(() => enqueue([song]))}>
|
<div className="context-menu-item" onClick={() => handleAction(() => enqueue([song]))}>
|
||||||
<ListPlus size={14} /> {t('contextMenu.addToQueue')}
|
<ListPlus size={14} /> {t('contextMenu.addToQueue')}
|
||||||
</div>
|
</div>
|
||||||
|
{orbitRole === 'guest' && (
|
||||||
|
<div className="context-menu-item" onClick={() => handleAction(() => {
|
||||||
|
suggestOrbitTrack(song.id)
|
||||||
|
.then(() => showToast('Suggested to session', 2200, 'info'))
|
||||||
|
.catch(() => showToast('Couldn\'t suggest — not joined', 3000, 'error'));
|
||||||
|
})}>
|
||||||
|
<OrbitIcon size={14} /> Add to session
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === song.id ? 'active' : ''}`}
|
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === song.id ? 'active' : ''}`}
|
||||||
data-playlist-trigger-id={song.id}
|
data-playlist-trigger-id={song.id}
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { X, Check, Copy } from 'lucide-react';
|
||||||
|
import { startOrbitSession, buildOrbitShareLink } from '../utils/orbit';
|
||||||
|
import { useAuthStore } from '../store/authStore';
|
||||||
|
import { useOrbitStore } from '../store/orbitStore';
|
||||||
|
import { ORBIT_DEFAULT_MAX_USERS } from '../api/orbit';
|
||||||
|
|
||||||
|
interface Props { onClose: () => void; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orbit — start-session modal.
|
||||||
|
*
|
||||||
|
* Two-step: host picks a name + max participants and presses "Start".
|
||||||
|
* Once the session is created we swap the form for the share link + a
|
||||||
|
* copy button, then the host closes the modal manually.
|
||||||
|
*/
|
||||||
|
export default function OrbitStartModal({ onClose }: Props) {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [maxUsers, setMaxUsers] = useState(ORBIT_DEFAULT_MAX_USERS);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [shareLink, setShareLink] = useState<string | null>(null);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const onStart = async () => {
|
||||||
|
setError(null);
|
||||||
|
const trimmed = name.trim();
|
||||||
|
if (!trimmed) { setError('Name required'); return; }
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
const state = await startOrbitSession({ name: trimmed, maxUsers });
|
||||||
|
const server = useAuthStore.getState().getActiveServer();
|
||||||
|
const base = server?.url ?? '';
|
||||||
|
setShareLink(buildOrbitShareLink(base, state.sid));
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : 'Start failed');
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCopy = async () => {
|
||||||
|
if (!shareLink) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(shareLink);
|
||||||
|
setCopied(true);
|
||||||
|
window.setTimeout(() => setCopied(false), 1500);
|
||||||
|
} catch { /* silent */ }
|
||||||
|
};
|
||||||
|
|
||||||
|
const inStartedState = !!shareLink;
|
||||||
|
const bindingActive = useOrbitStore.getState().phase === 'active';
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
className="modal-overlay orbit-start-overlay"
|
||||||
|
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="orbit-start-title"
|
||||||
|
>
|
||||||
|
<div className="modal-content orbit-start-modal">
|
||||||
|
<button type="button" className="modal-close" onClick={onClose} aria-label="Close">
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{!inStartedState && (
|
||||||
|
<>
|
||||||
|
<h3 id="orbit-start-title" className="orbit-start-modal__title">Start a session</h3>
|
||||||
|
<p className="orbit-start-modal__sub">Anyone you share the link with can join from this server.</p>
|
||||||
|
|
||||||
|
<label className="orbit-start-modal__label">
|
||||||
|
Name
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
placeholder="Friday night"
|
||||||
|
maxLength={40}
|
||||||
|
className="orbit-start-modal__input"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="orbit-start-modal__label">
|
||||||
|
Max guests: <strong>{maxUsers}</strong>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={1}
|
||||||
|
max={32}
|
||||||
|
value={maxUsers}
|
||||||
|
onChange={e => setMaxUsers(Number(e.target.value))}
|
||||||
|
className="orbit-start-modal__range"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{error && <div className="orbit-start-modal__error">{error}</div>}
|
||||||
|
|
||||||
|
<div className="orbit-start-modal__actions">
|
||||||
|
<button type="button" className="btn btn-surface" onClick={onClose}>Cancel</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={onStart}
|
||||||
|
disabled={busy || !name.trim()}
|
||||||
|
>
|
||||||
|
{busy ? 'Starting…' : 'Start'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{inStartedState && (
|
||||||
|
<>
|
||||||
|
<h3 id="orbit-start-title" className="orbit-start-modal__title">Session live{bindingActive ? '' : '…'}</h3>
|
||||||
|
<p className="orbit-start-modal__sub">Share this with anyone on this server:</p>
|
||||||
|
|
||||||
|
<div className="orbit-start-modal__link">
|
||||||
|
<code>{shareLink}</code>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="orbit-start-modal__copy"
|
||||||
|
onClick={onCopy}
|
||||||
|
data-tooltip={copied ? 'Copied' : 'Copy'}
|
||||||
|
aria-label="Copy share link"
|
||||||
|
>
|
||||||
|
{copied ? <Check size={14} /> : <Copy size={14} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="orbit-start-modal__actions">
|
||||||
|
<button type="button" className="btn btn-primary" onClick={onClose}>Done</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { decodeSharePayloadFromText } from '../utils/shareLink';
|
|||||||
import { decodeServerMagicStringFromText } from '../utils/serverMagicString';
|
import { decodeServerMagicStringFromText } from '../utils/serverMagicString';
|
||||||
import { applySharePastePayload } from '../utils/applySharePaste';
|
import { applySharePastePayload } from '../utils/applySharePaste';
|
||||||
import { showToast } from '../utils/toast';
|
import { showToast } from '../utils/toast';
|
||||||
|
import { parseOrbitShareLink, joinOrbitSession, OrbitJoinError } from '../utils/orbit';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global paste: library share links (`psysonic2-`) and server invites (`psysonic1-`)
|
* Global paste: library share links (`psysonic2-`) and server invites (`psysonic1-`)
|
||||||
@@ -30,6 +31,43 @@ export default function PasteClipboardHandler() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const text = e.clipboardData?.getData('text/plain') ?? '';
|
const text = e.clipboardData?.getData('text/plain') ?? '';
|
||||||
|
|
||||||
|
// Orbit share link — handled before library shares.
|
||||||
|
const orbit = parseOrbitShareLink(text.trim());
|
||||||
|
if (orbit) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!isLoggedIn) { showToast('Log in before joining a session', 4000, 'info'); return; }
|
||||||
|
const active = useAuthStore.getState().getActiveServer();
|
||||||
|
const activeUrl = (active?.url ?? '').replace(/\/+$/, '');
|
||||||
|
const wantUrl = orbit.serverBase.replace(/\/+$/, '');
|
||||||
|
if (activeUrl !== wantUrl) {
|
||||||
|
showToast(`Switch to ${wantUrl} first, then paste again`, 5000, 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (busy.current) return;
|
||||||
|
busy.current = true;
|
||||||
|
joinOrbitSession(orbit.sid)
|
||||||
|
.then(() => showToast('Joined session', 2500, 'info'))
|
||||||
|
.catch(err => {
|
||||||
|
if (err instanceof OrbitJoinError) {
|
||||||
|
const msg: Record<string, string> = {
|
||||||
|
'not-found': 'Session not found',
|
||||||
|
'ended': 'Session has ended',
|
||||||
|
'full': 'Session is full',
|
||||||
|
'kicked': 'You can\'t rejoin this session',
|
||||||
|
'no-user': 'No active server',
|
||||||
|
'server-error': 'Couldn\'t join',
|
||||||
|
};
|
||||||
|
showToast(msg[err.reason] ?? err.message, 4000, 'error');
|
||||||
|
} else {
|
||||||
|
showToast('Couldn\'t join session', 4000, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => { busy.current = false; });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const share = decodeSharePayloadFromText(text);
|
const share = decodeSharePayloadFromText(text);
|
||||||
if (share) {
|
if (share) {
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useState, useRef, useMemo, useEffect } from 'react';
|
import React, { useState, useRef, useMemo, useEffect } from 'react';
|
||||||
import { Track, usePlayerStore, songToTrack } from '../store/playerStore';
|
import { Track, usePlayerStore, songToTrack } from '../store/playerStore';
|
||||||
import { Play, Music, Star, X, Trash2, Save, FolderOpen, Shuffle, Infinity, Waves, MicVocal, ListMusic, Check, ListPlus, ArrowUpToLine, Radio, HardDrive, ChevronDown, Info, Share2 } from 'lucide-react';
|
import { Play, Music, Star, X, Trash2, Save, FolderOpen, Shuffle, Infinity, Waves, MicVocal, ListMusic, Check, ListPlus, ArrowUpToLine, Radio, HardDrive, ChevronDown, Info, Share2, Orbit as OrbitIcon } from 'lucide-react';
|
||||||
|
import OrbitStartModal from './OrbitStartModal';
|
||||||
|
import { useOrbitStore } from '../store/orbitStore';
|
||||||
import { buildCoverArtUrl, coverArtCacheKey, getAlbum, getPlaylists, getPlaylist, updatePlaylist, deletePlaylist, SubsonicPlaylist } from '../api/subsonic';
|
import { buildCoverArtUrl, coverArtCacheKey, getAlbum, getPlaylists, getPlaylist, updatePlaylist, deletePlaylist, SubsonicPlaylist } from '../api/subsonic';
|
||||||
import { usePlaylistStore } from '../store/playlistStore';
|
import { usePlaylistStore } from '../store/playlistStore';
|
||||||
import { useCachedUrl } from './CachedImage';
|
import { useCachedUrl } from './CachedImage';
|
||||||
@@ -199,6 +201,9 @@ function QueueHeader({ queue, queueIndex, showRemainingTime, setShowRemainingTim
|
|||||||
|
|
||||||
const dur = showRemainingTime ? `-${fmt(Math.floor(remainingSecs))}` : fmt(Math.floor(totalSecs));
|
const dur = showRemainingTime ? `-${fmt(Math.floor(remainingSecs))}` : fmt(Math.floor(totalSecs));
|
||||||
|
|
||||||
|
const orbitRole = useOrbitStore(s => s.role);
|
||||||
|
const [startOpen, setStartOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="queue-header">
|
<div className="queue-header">
|
||||||
<div style={{ display: "flex", flexDirection: "column", minWidth: 0, flex: 1 }}>
|
<div style={{ display: "flex", flexDirection: "column", minWidth: 0, flex: 1 }}>
|
||||||
@@ -225,6 +230,18 @@ function QueueHeader({ queue, queueIndex, showRemainingTime, setShowRemainingTim
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{orbitRole === null && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="queue-header-orbit-btn"
|
||||||
|
onClick={() => setStartOpen(true)}
|
||||||
|
data-tooltip="Start a session"
|
||||||
|
aria-label="Start a session"
|
||||||
|
>
|
||||||
|
<OrbitIcon size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{startOpen && <OrbitStartModal onClose={() => setStartOpen(false)} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11443,3 +11443,148 @@ html[data-app-hidden="true"] *::after {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Queue-header "start session" trigger ──────────────────────── */
|
||||||
|
.queue-header-orbit-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--accent) 32%, transparent);
|
||||||
|
color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 150ms ease, border-color 150ms ease, transform 160ms ease;
|
||||||
|
}
|
||||||
|
.queue-header-orbit-btn:hover {
|
||||||
|
background: color-mix(in srgb, var(--accent) 22%, transparent);
|
||||||
|
border-color: color-mix(in srgb, var(--accent) 52%, transparent);
|
||||||
|
transform: rotate(30deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Start-session modal ────────────────────────────────────────── */
|
||||||
|
.orbit-start-overlay {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orbit-start-modal {
|
||||||
|
max-width: 420px;
|
||||||
|
width: min(420px, calc(100vw - 32px));
|
||||||
|
padding: 28px 26px 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orbit-start-modal__title {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
padding-right: 2rem;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orbit-start-modal__sub {
|
||||||
|
margin: 0 0 18px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orbit-start-modal__label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.orbit-start-modal__label strong {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orbit-start-modal__input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
background: var(--ctp-base);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.orbit-start-modal__input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orbit-start-modal__range {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orbit-start-modal__error {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: color-mix(in srgb, var(--ctp-red, #f38ba8) 14%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--ctp-red, #f38ba8) 35%, transparent);
|
||||||
|
color: var(--ctp-red, #f38ba8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orbit-start-modal__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orbit-start-modal__link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--ctp-base);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-family: var(--font-mono, ui-monospace, 'Courier New', monospace);
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--accent);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.orbit-start-modal__link code {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.orbit-start-modal__copy {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
|
||||||
|
color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 150ms ease, border-color 150ms ease;
|
||||||
|
}
|
||||||
|
.orbit-start-modal__copy:hover {
|
||||||
|
background: color-mix(in srgb, var(--accent) 26%, transparent);
|
||||||
|
border-color: color-mix(in srgb, var(--accent) 50%, transparent);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user