Compare commits
3 Commits
bdae539e9e
...
d00561f75d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d00561f75d | ||
|
|
8d745ad96e | ||
|
|
bcd747721d |
@@ -187,7 +187,7 @@ export function AppLayout() {
|
|||||||
</div>}
|
</div>}
|
||||||
<div className="profile">
|
<div className="profile">
|
||||||
<span className="avatar" title={collapsed ? name : undefined}>{name.charAt(0).toUpperCase()}</span>
|
<span className="avatar" title={collapsed ? name : undefined}>{name.charAt(0).toUpperCase()}</span>
|
||||||
<div><b>{name}</b><small>{user?.primaryEmailAddress?.emailAddress}</small></div>
|
<div><b title={name}>{name}</b><small title={user?.primaryEmailAddress?.emailAddress}>{user?.primaryEmailAddress?.emailAddress}</small></div>
|
||||||
<button className="icon-btn" aria-label={t('layout.signOut')} onClick={() => signOut({ redirectUrl: '/' })}><Icon name="logout" /></button>
|
<button className="icon-btn" aria-label={t('layout.signOut')} onClick={() => signOut({ redirectUrl: '/' })}><Icon name="logout" /></button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const paths: Record<string, string> = {
|
|||||||
sidebar: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16"/>',
|
sidebar: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16"/>',
|
||||||
'chevron-updown': '<path d="m8 9 4-4 4 4M8 15l4 4 4-4"/>',
|
'chevron-updown': '<path d="m8 9 4-4 4 4M8 15l4 4 4-4"/>',
|
||||||
logout:'<path d="M10 4H4v16h6M10 12h11m-4-4 4 4-4 4"/>',
|
logout:'<path d="M10 4H4v16h6M10 12h11m-4-4 4 4-4 4"/>',
|
||||||
|
user: '<circle cx="12" cy="8" r="4"/><path d="M4 21c1-4 4-6 8-6s7 2 8 6"/>',
|
||||||
plus: '<path d="M12 4v16M4 12h16"/>',
|
plus: '<path d="M12 4v16M4 12h16"/>',
|
||||||
trash: '<path d="M3 6h18M9 3h6l1 3M6 6l1 15h10l1-15M10 10v7M14 10v7"/>',
|
trash: '<path d="M3 6h18M9 3h6l1 3M6 6l1 15h10l1-15M10 10v7M14 10v7"/>',
|
||||||
video: '<rect x="3" y="5" width="18" height="14" rx="3"/><path d="m10 9 5 3-5 3z"/>',
|
video: '<rect x="3" y="5" width="18" height="14" rx="3"/><path d="m10 9 5 3-5 3z"/>',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import type { ContentRule, ResearchRule } from './parse'
|
import type { ContentRule, ResearchRule } from './parse'
|
||||||
import { budgetFor, canSchedule, configFor, needsTwoSources, productionError, ruleOf, VIDEO, type ExpressAgent } from './schedules'
|
import { budgetFor, canSchedule, configFor, isMine, needsTwoSources, productionError, ruleOf, VIDEO, type ExpressAgent } from './schedules'
|
||||||
|
|
||||||
const rule = (kind: ContentRule['kind'], count = 2, per: ContentRule['per'] = 'week', topic = 'onboarding', language: ContentRule['language'] = 'English'): ContentRule =>
|
const rule = (kind: ContentRule['kind'], count = 2, per: ContentRule['per'] = 'week', topic = 'onboarding', language: ContentRule['language'] = 'English'): ContentRule =>
|
||||||
({ type: 'content', kind, count, per, topic, language })
|
({ type: 'content', kind, count, per, topic, language })
|
||||||
@@ -9,7 +9,7 @@ const rule = (kind: ContentRule['kind'], count = 2, per: ContentRule['per'] = 'w
|
|||||||
function agentFrom(config: ReturnType<typeof configFor>): ExpressAgent {
|
function agentFrom(config: ReturnType<typeof configFor>): ExpressAgent {
|
||||||
const c = config as Record<string, unknown>
|
const c = config as Record<string, unknown>
|
||||||
return {
|
return {
|
||||||
id: 1, status: 'active', next_run_at: null, created_at: '2026-10-01T00:00:00Z', auto_approve_sources: false,
|
id: 1, status: 'active', next_run_at: null, created_at: '2026-10-01T00:00:00Z', created_by_user_id: 7, auto_approve_sources: false,
|
||||||
name: c.name as string, goal: c.goal as string, instructions: c.instructions as string,
|
name: c.name as string, goal: c.goal as string, instructions: c.instructions as string,
|
||||||
enabled_tools: c.enabled_tools as string[],
|
enabled_tools: c.enabled_tools as string[],
|
||||||
material_outputs: (c.material_outputs as ExpressAgent['material_outputs']) ?? [],
|
material_outputs: (c.material_outputs as ExpressAgent['material_outputs']) ?? [],
|
||||||
@@ -122,3 +122,20 @@ describe('production sources', () => {
|
|||||||
expect(productionError('Something else')).toBe('Something else')
|
expect(productionError('Something else')).toBe('Something else')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('isMine', () => {
|
||||||
|
const owners = { me: 7, names: new Map<number, string>() }
|
||||||
|
const schedule = (by: number | null) => {
|
||||||
|
const agent = { ...agentFrom(configFor(rule('summary'))), created_by_user_id: by }
|
||||||
|
return { agent, rule: ruleOf(agent), active: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
it('keeps only the viewer’s own schedules', () => {
|
||||||
|
expect(isMine(schedule(7), owners)).toBe(true)
|
||||||
|
expect(isMine(schedule(8), owners)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('counts a schedule whose creator was removed as someone else’s', () => {
|
||||||
|
expect(isMine(schedule(null), owners)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export interface ExpressAgent extends Agent {
|
|||||||
run_budget: number
|
run_budget: number
|
||||||
monthly_budget: number
|
monthly_budget: number
|
||||||
created_at: string
|
created_at: string
|
||||||
|
created_by_user_id: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Schedule {
|
export interface Schedule {
|
||||||
@@ -107,6 +108,23 @@ export async function expressAgents({ archived = false } = {}): Promise<Schedule
|
|||||||
|
|
||||||
export const listSchedules = () => expressAgents()
|
export const listSchedules = () => expressAgents()
|
||||||
|
|
||||||
|
// An organization admin sees every schedule in the organization, a member only their own (and any
|
||||||
|
// shared with them), so the Schedule page says who made each one and starts with the viewer's own.
|
||||||
|
export interface Owners { me: number; names: Map<number, string> }
|
||||||
|
|
||||||
|
/** The viewer's engine user id and the organization roster's names. The roster is admin-only;
|
||||||
|
* a member gets no names, which is fine: a member's list holds their own schedules. */
|
||||||
|
export async function scheduleOwners(): Promise<Owners> {
|
||||||
|
const [me, roster] = await Promise.all([
|
||||||
|
request<{ id: number }>('/auth/me'),
|
||||||
|
request<{ members: { user_id: number; name: string | null; email: string | null }[] }>('/organizations/current/dashboard/members')
|
||||||
|
.catch(() => ({ members: [] })),
|
||||||
|
])
|
||||||
|
return { me: me.id, names: new Map(roster.members.map(m => [m.user_id, m.name || m.email || `#${m.user_id}`])) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isMine = (s: Schedule, owners: Owners) => s.agent.created_by_user_id === owners.me
|
||||||
|
|
||||||
/** Schedules made of one engine run per slot (research, video), rather than weekly production plans. */
|
/** Schedules made of one engine run per slot (research, video), rather than weekly production plans. */
|
||||||
export const isRunSchedule = (rule: Rule) => rule.type === 'research' || rule.kind === 'video'
|
export const isRunSchedule = (rule: Rule) => rule.type === 'research' || rule.kind === 'video'
|
||||||
|
|
||||||
|
|||||||
14
src/i18n.ts
14
src/i18n.ts
@@ -206,6 +206,13 @@ const en = {
|
|||||||
'sch.loading': 'Loading schedules…',
|
'sch.loading': 'Loading schedules…',
|
||||||
'sch.loadFailed': 'Could not load schedules',
|
'sch.loadFailed': 'Could not load schedules',
|
||||||
'sch.empty': 'No schedules yet',
|
'sch.empty': 'No schedules yet',
|
||||||
|
'sch.mine': 'Mine',
|
||||||
|
'sch.all': 'Everyone in the organization',
|
||||||
|
'sch.emptyMine': 'You have no schedules yet',
|
||||||
|
'sch.emptyMineCopy': 'Other people in this organization have schedules; see them under “Everyone in the organization”.',
|
||||||
|
'sch.byYou': 'Created by you',
|
||||||
|
'sch.by': 'Created by {name}',
|
||||||
|
'sch.byUnknown': 'Created by someone else',
|
||||||
'sch.emptyCopy': 'Give an instruction like “2 summaries per day about leadership” to start.',
|
'sch.emptyCopy': 'Give an instruction like “2 summaries per day about leadership” to start.',
|
||||||
'sch.nextResearch': 'Next research: {when}',
|
'sch.nextResearch': 'Next research: {when}',
|
||||||
'sch.paused': 'Paused',
|
'sch.paused': 'Paused',
|
||||||
@@ -697,6 +704,13 @@ const id: Record<Key, string> = {
|
|||||||
'sch.loading': 'Memuat jadwal…',
|
'sch.loading': 'Memuat jadwal…',
|
||||||
'sch.loadFailed': 'Jadwal tidak dapat dimuat',
|
'sch.loadFailed': 'Jadwal tidak dapat dimuat',
|
||||||
'sch.empty': 'Belum ada jadwal',
|
'sch.empty': 'Belum ada jadwal',
|
||||||
|
'sch.mine': 'Punya saya',
|
||||||
|
'sch.all': 'Semua di organisasi',
|
||||||
|
'sch.emptyMine': 'Anda belum punya jadwal',
|
||||||
|
'sch.emptyMineCopy': 'Orang lain di organisasi ini punya jadwal; lihat di “Semua di organisasi”.',
|
||||||
|
'sch.byYou': 'Dibuat oleh Anda',
|
||||||
|
'sch.by': 'Dibuat oleh {name}',
|
||||||
|
'sch.byUnknown': 'Dibuat oleh orang lain',
|
||||||
'sch.emptyCopy': 'Beri perintah seperti “2 ringkasan per hari soal leadership” untuk mulai.',
|
'sch.emptyCopy': 'Beri perintah seperti “2 ringkasan per hari soal leadership” untuk mulai.',
|
||||||
'sch.nextResearch': 'Riset berikutnya: {when}',
|
'sch.nextResearch': 'Riset berikutnya: {when}',
|
||||||
'sch.paused': 'Dijeda',
|
'sch.paused': 'Dijeda',
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { toast } from '../components/Toast'
|
|||||||
import { describeRule } from '../express/parse'
|
import { describeRule } from '../express/parse'
|
||||||
import { locale, t, type Key } from '../i18n'
|
import { locale, t, type Key } from '../i18n'
|
||||||
import {
|
import {
|
||||||
archiveSchedule, confirmProduction, isRunSchedule, knowledgeReadiness, listRuns, listSchedules, pauseSchedule, PLAN_ACTIVE, productionStatus, quoteProduction, resumeSchedule,
|
archiveSchedule, confirmProduction, isMine, isRunSchedule, knowledgeReadiness, listRuns, listSchedules, scheduleOwners, pauseSchedule, PLAN_ACTIVE, productionStatus, quoteProduction, resumeSchedule,
|
||||||
planProgress, productionError, runNow as startRun, updateTopic, type ContentPlan, type RunSummary, type Schedule,
|
planProgress, productionError, runNow as startRun, updateTopic, type ContentPlan, type Owners, type RunSummary, type Schedule,
|
||||||
} from '../express/schedules'
|
} from '../express/schedules'
|
||||||
|
|
||||||
const STUDIO_URL: string = import.meta.env.VITE_STUDIO_URL
|
const STUDIO_URL: string = import.meta.env.VITE_STUDIO_URL
|
||||||
@@ -33,25 +33,61 @@ const runError = (code: string) => (RUN_ERRORS.has(code) ? t(`runErr.${code}` as
|
|||||||
const issueReason = (reason: string) => (ISSUE_REASONS.has(reason) ? t(`issue.${reason}` as Key) : reason)
|
const issueReason = (reason: string) => (ISSUE_REASONS.has(reason) ? t(`issue.${reason}` as Key) : reason)
|
||||||
const isNotReady = (r: RunSummary) => r.safe_error_code === 'knowledge_not_ready' || r.has_readiness_issues
|
const isNotReady = (r: RunSummary) => r.safe_error_code === 'knowledge_not_ready' || r.has_readiness_issues
|
||||||
|
|
||||||
|
// Which schedules the page starts with; remembered per device.
|
||||||
|
type Scope = 'mine' | 'all'
|
||||||
|
const SCOPE_KEY = 'express.scheduleScope'
|
||||||
|
const savedScope = (): Scope => {
|
||||||
|
try { return localStorage.getItem(SCOPE_KEY) === 'all' ? 'all' : 'mine' } catch { return 'mine' }
|
||||||
|
}
|
||||||
|
|
||||||
export function SchedulePage() {
|
export function SchedulePage() {
|
||||||
const schedules = useQuery({ queryKey: ['schedules'], queryFn: listSchedules, refetchInterval: 30_000 })
|
const schedules = useQuery({ queryKey: ['schedules'], queryFn: listSchedules, refetchInterval: 30_000 })
|
||||||
|
const owners = useQuery({ queryKey: ['scheduleOwners'], queryFn: scheduleOwners, staleTime: 5 * 60_000 })
|
||||||
|
const [scope, setScope] = useState<Scope>(savedScope)
|
||||||
|
const choose = (next: Scope) => {
|
||||||
|
setScope(next)
|
||||||
|
try { localStorage.setItem(SCOPE_KEY, next) } catch { /* keep it for this visit only */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
const all = schedules.data
|
||||||
|
const mine = all && owners.data ? all.filter(s => isMine(s, owners.data)) : undefined
|
||||||
|
// Members only ever see their own, so the switch appears only when someone else's schedules are listed.
|
||||||
|
const mixed = !!all && !!mine && mine.length < all.length
|
||||||
|
const shown = mixed && scope === 'mine' ? mine : all
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={t('sch.title')} description={t('sch.description')}
|
<PageHead title={t('sch.title')} description={t('sch.description')}
|
||||||
actions={<Link className="btn primary" to="/brain"><Icon name="plus" />{t('sch.newInstruction')}</Link>} />
|
actions={<Link className="btn primary" to="/brain"><Icon name="plus" />{t('sch.newInstruction')}</Link>} />
|
||||||
|
{mixed && (
|
||||||
|
<div className="result-tabs" role="tablist">
|
||||||
|
<button role="tab" aria-selected={scope === 'mine'} className={scope === 'mine' ? 'active' : undefined} onClick={() => choose('mine')}>
|
||||||
|
{t('sch.mine')} <b>{mine.length}</b>
|
||||||
|
</button>
|
||||||
|
<button role="tab" aria-selected={scope === 'all'} className={scope === 'all' ? 'active' : undefined} onClick={() => choose('all')}>
|
||||||
|
{t('sch.all')} <b>{all.length}</b>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-head"><h3>{t('sch.cardTitle')}</h3><Icon name="calendar" /></div>
|
<div className="card-head"><h3>{t('sch.cardTitle')}</h3><Icon name="calendar" /></div>
|
||||||
{schedules.isPending && <SkeletonRows label={t('sch.loading')} />}
|
{schedules.isPending && <SkeletonRows label={t('sch.loading')} />}
|
||||||
{schedules.isError && <Empty icon="info" title={t('sch.loadFailed')}>{schedules.error.message}</Empty>}
|
{schedules.isError && <Empty icon="info" title={t('sch.loadFailed')}>{schedules.error.message}</Empty>}
|
||||||
{schedules.data?.length === 0 && <Empty icon="calendar" title={t('sch.empty')}>{t('sch.emptyCopy')}</Empty>}
|
{all?.length === 0 && <Empty icon="calendar" title={t('sch.empty')}>{t('sch.emptyCopy')}</Empty>}
|
||||||
{!!schedules.data?.length && <div className="result-list">{schedules.data.map(s => <ScheduleRow key={s.agent.id} schedule={s} />)}</div>}
|
{mixed && shown?.length === 0 && <Empty icon="calendar" title={t('sch.emptyMine')}>{t('sch.emptyMineCopy')}</Empty>}
|
||||||
|
{!!shown?.length && <div className="result-list">{shown.map(s => <ScheduleRow key={s.agent.id} schedule={s} owners={owners.data} />)}</div>}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ScheduleRow({ schedule }: { schedule: Schedule }) {
|
function createdBy(schedule: Schedule, owners: Owners) {
|
||||||
|
if (isMine(schedule, owners)) return t('sch.byYou')
|
||||||
|
const name = schedule.agent.created_by_user_id != null ? owners.names.get(schedule.agent.created_by_user_id) : undefined
|
||||||
|
return name ? t('sch.by', { name }) : t('sch.byUnknown')
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScheduleRow({ schedule, owners }: { schedule: Schedule; owners?: Owners }) {
|
||||||
const { agent, rule, active } = schedule
|
const { agent, rule, active } = schedule
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [editing, setEditing] = useState(false)
|
const [editing, setEditing] = useState(false)
|
||||||
@@ -84,6 +120,7 @@ function ScheduleRow({ schedule }: { schedule: Schedule }) {
|
|||||||
<span className="rule-icon"><Icon name={ruleIcon(rule)} /></span>
|
<span className="rule-icon"><Icon name={ruleIcon(rule)} /></span>
|
||||||
<div style={{ minWidth: 0, flex: 1 }}>
|
<div style={{ minWidth: 0, flex: 1 }}>
|
||||||
<b>{describeRule(rule)}</b>
|
<b>{describeRule(rule)}</b>
|
||||||
|
{owners && <div className="schedule-owner"><small><Icon name="user" />{createdBy(schedule, owners)}</small></div>}
|
||||||
<div><small>{production
|
<div><small>{production
|
||||||
? <ProductionLine agentId={agent.id} active={active} />
|
? <ProductionLine agentId={agent.id} active={active} />
|
||||||
: t(video ? 'sch.nextVideo' : 'sch.nextResearch', { when: active ? fmt(agent.next_run_at) : t('sch.paused') })}</small></div>
|
: t(video ? 'sch.nextVideo' : 'sch.nextResearch', { when: active ? fmt(agent.next_run_at) : t('sch.paused') })}</small></div>
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ textarea{resize:vertical;line-height:1.6}
|
|||||||
.rule-outcomes .error{margin:0;font-size:11px}
|
.rule-outcomes .error{margin:0;font-size:11px}
|
||||||
.schedule-row{border-bottom:1px solid var(--line);padding-bottom:14px}
|
.schedule-row{border-bottom:1px solid var(--line);padding-bottom:14px}
|
||||||
.schedule-row:last-child{border-bottom:0}
|
.schedule-row:last-child{border-bottom:0}
|
||||||
|
.schedule-owner small{display:inline-flex;align-items:center;gap:5px;color:var(--muted)}
|
||||||
|
.schedule-owner svg{width:12px;height:12px}
|
||||||
.schedule-row .result-row{border-bottom:0;padding-bottom:10px}
|
.schedule-row .result-row{border-bottom:0;padding-bottom:10px}
|
||||||
.schedule-row .error{display:block;margin-top:6px;font-size:11px}
|
.schedule-row .error{display:block;margin-top:6px;font-size:11px}
|
||||||
.schedule-actions{display:flex;gap:7px;flex-wrap:wrap;padding-left:61px}
|
.schedule-actions{display:flex;gap:7px;flex-wrap:wrap;padding-left:61px}
|
||||||
|
|||||||
@@ -77,8 +77,11 @@ button.workspace:hover,button.workspace[aria-expanded=true]{border-color:var(--s
|
|||||||
.sidebar-help p{font-size:12px}
|
.sidebar-help p{font-size:12px}
|
||||||
.sidebar-help button{color:var(--blue-dark);font-size:12.5px}
|
.sidebar-help button{color:var(--blue-dark);font-size:12.5px}
|
||||||
.profile{border-top:1px solid var(--line);padding:14px 4px 0}
|
.profile{border-top:1px solid var(--line);padding:14px 4px 0}
|
||||||
|
.profile>div{flex:1;min-width:0}
|
||||||
|
.profile b,.profile small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
.profile b{font-size:13px}
|
.profile b{font-size:13px}
|
||||||
.profile small{font-size:11.5px}
|
.profile small{font-size:11.5px}
|
||||||
|
.profile>.icon-btn{flex-shrink:0}
|
||||||
.avatar{background:linear-gradient(140deg,#2e90fa,#1570ef);color:#fff;width:34px;height:34px;font-weight:700;box-shadow:0 0 0 2px #fff,0 0 0 3.5px rgba(46,144,250,.5);flex-shrink:0}
|
.avatar{background:linear-gradient(140deg,#2e90fa,#1570ef);color:#fff;width:34px;height:34px;font-weight:700;box-shadow:0 0 0 2px #fff,0 0 0 3.5px rgba(46,144,250,.5);flex-shrink:0}
|
||||||
|
|
||||||
/* Main column: glass top bar and a soft blue glow at the top */
|
/* Main column: glass top bar and a soft blue glow at the top */
|
||||||
@@ -305,7 +308,7 @@ dialog::backdrop{background:rgba(16,24,40,.45);backdrop-filter:blur(4px)}
|
|||||||
@keyframes live-pulse{70%{box-shadow:0 0 0 8px rgba(74,222,128,0)}100%{box-shadow:0 0 0 0 rgba(74,222,128,0)}}
|
@keyframes live-pulse{70%{box-shadow:0 0 0 8px rgba(74,222,128,0)}100%{box-shadow:0 0 0 0 rgba(74,222,128,0)}}
|
||||||
|
|
||||||
.graph-stage{--g-sources:#6ce9a6;--g-made:#fde272;--g-working:#c3b5fd;
|
.graph-stage{--g-sources:#6ce9a6;--g-made:#fde272;--g-working:#c3b5fd;
|
||||||
position:relative;min-height:640px;border-radius:22px;overflow:hidden;color:#e4e7ec;
|
position:relative;height:640px;max-height:calc(100vh - 260px);max-height:calc(100dvh - 260px);min-height:440px;border-radius:22px;overflow:hidden;color:#e4e7ec;
|
||||||
background:radial-gradient(ellipse at 45% 50%,#0f2a4d 0%,#0a1628 55%,#060d1a 100%);box-shadow:0 24px 60px -28px rgba(16,24,40,.6)}
|
background:radial-gradient(ellipse at 45% 50%,#0f2a4d 0%,#0a1628 55%,#060d1a 100%);box-shadow:0 24px 60px -28px rgba(16,24,40,.6)}
|
||||||
.graph-svg{position:absolute;inset:0;width:calc(100% - 300px);height:100%;display:block}
|
.graph-svg{position:absolute;inset:0;width:calc(100% - 300px);height:100%;display:block}
|
||||||
.graph-loading{position:absolute;inset:0 300px 0 0;display:grid;place-items:center;color:#98a2b3;font-size:13px}
|
.graph-loading{position:absolute;inset:0 300px 0 0;display:grid;place-items:center;color:#98a2b3;font-size:13px}
|
||||||
@@ -378,10 +381,10 @@ dialog::backdrop{background:rgba(16,24,40,.45);backdrop-filter:blur(4px)}
|
|||||||
.graph-fullscreen{position:absolute;top:16px;right:316px;z-index:3;display:grid;place-items:center;width:34px;height:34px;padding:0;border:1px solid rgba(255,255,255,.14);border-radius:10px;background:rgba(10,22,40,.6);backdrop-filter:blur(8px);color:#d0d5dd;cursor:pointer;transition:background .15s,color .15s}
|
.graph-fullscreen{position:absolute;top:16px;right:316px;z-index:3;display:grid;place-items:center;width:34px;height:34px;padding:0;border:1px solid rgba(255,255,255,.14);border-radius:10px;background:rgba(10,22,40,.6);backdrop-filter:blur(8px);color:#d0d5dd;cursor:pointer;transition:background .15s,color .15s}
|
||||||
.graph-fullscreen:hover{background:rgba(46,144,250,.25);color:#fff}
|
.graph-fullscreen:hover{background:rgba(46,144,250,.25);color:#fff}
|
||||||
.graph-fullscreen svg{width:16px;height:16px}
|
.graph-fullscreen svg{width:16px;height:16px}
|
||||||
.graph-stage.full{border-radius:0;min-height:0;height:100vh;height:100dvh;box-shadow:none}
|
.graph-stage.full{border-radius:0;min-height:0;max-height:none;height:100vh;height:100dvh;box-shadow:none}
|
||||||
.graph-stage.pinned{position:fixed;inset:0;z-index:50}
|
.graph-stage.pinned{position:fixed;inset:0;z-index:50}
|
||||||
@media(max-width:900px){
|
@media(max-width:900px){
|
||||||
.graph-stage{min-height:0;display:flex;flex-direction:column}
|
.graph-stage{height:auto;max-height:none;min-height:0;display:flex;flex-direction:column}
|
||||||
.graph-stage.full{overflow:auto}
|
.graph-stage.full{overflow:auto}
|
||||||
.graph-fullscreen{right:16px}
|
.graph-fullscreen{right:16px}
|
||||||
.graph-svg{position:relative;width:100%;height:auto;aspect-ratio:1000/640}
|
.graph-svg{position:relative;width:100%;height:auto;aspect-ratio:1000/640}
|
||||||
|
|||||||
Reference in New Issue
Block a user