Compare commits
4 Commits
feat/kill-
...
8bed2f75c1
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bed2f75c1 | |||
|
|
1026c762a9 | ||
|
|
50db790f86 | ||
| 1000d053b3 |
@@ -365,7 +365,23 @@ export function registerGiteaIpc(): void {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ---- config (team onboarding) ----
|
// ---- config (team onboarding) ----
|
||||||
ipcMain.handle('config:get', () => publicConfig())
|
// The saved config's public view, or — when running off a .env.local / env
|
||||||
|
// fallback (dev) — a public view of the *resolved* connection, so Settings and
|
||||||
|
// the rail reflect what the app is actually connected to, not just the store.
|
||||||
|
ipcMain.handle('config:get', () => {
|
||||||
|
const saved = publicConfig()
|
||||||
|
if (saved) return saved
|
||||||
|
const c = resolveConfig()
|
||||||
|
if (!c) return null
|
||||||
|
return {
|
||||||
|
baseUrl: c.baseUrl,
|
||||||
|
owner: c.owner,
|
||||||
|
repo: c.repo,
|
||||||
|
pmStateRepo: pmStateRepoName(c),
|
||||||
|
modelUrl: process.env.MODEL_BASE_URL ?? undefined,
|
||||||
|
hasToken: !!c.token,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('config:set', (_event, cfg: AppConfig) => {
|
ipcMain.handle('config:set', (_event, cfg: AppConfig) => {
|
||||||
saveConfig(cfg)
|
saveConfig(cfg)
|
||||||
|
|||||||
@@ -24,10 +24,7 @@ interface Question {
|
|||||||
|
|
||||||
export function CaptureScreen({ onDone }: { onDone: () => void }) {
|
export function CaptureScreen({ onDone }: { onDone: () => void }) {
|
||||||
const [stage, setStage] = React.useState<'dump' | 'interview' | 'review' | 'filed'>('dump')
|
const [stage, setStage] = React.useState<'dump' | 'interview' | 'review' | 'filed'>('dump')
|
||||||
const [dump, setDump] = React.useState(
|
const [dump, setDump] = React.useState('')
|
||||||
'auth is flaky — token refresh dies silently, sometimes session storage goes stale. ' +
|
|
||||||
'also the webhook debounce thing keeps double-firing. and we owe docs for auth setup'
|
|
||||||
)
|
|
||||||
const [qi, setQi] = React.useState(0)
|
const [qi, setQi] = React.useState(0)
|
||||||
const [log, setLog] = React.useState<{ q: string; a: string }[]>([])
|
const [log, setLog] = React.useState<{ q: string; a: string }[]>([])
|
||||||
const [split, setSplit] = React.useState<boolean | null>(null)
|
const [split, setSplit] = React.useState<boolean | null>(null)
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import { EmptyState } from '../shell/states.js'
|
|||||||
import { Badge, Button, Card, IconButton, Tag } from '../ui/index.js'
|
import { Badge, Button, Card, IconButton, Tag } from '../ui/index.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Morning service — the Now/Next/Later focus cards + the burn-up cone.
|
* Morning service — the Now/Next/Later focus cards + the burn-up cone, from the
|
||||||
* `focus` (scheduler) and `forecast` (Monte Carlo) override the demo fixtures
|
* scheduler (`focus`) and Monte Carlo forecast (`forecast`). Both come from the
|
||||||
* when gitea is configured; both fall back to the handoff demo otherwise.
|
* reconciled backlog; with no open work the scheduler has nothing to pour, so we
|
||||||
|
* show the empty state.
|
||||||
*/
|
*/
|
||||||
export function FocusScreen({
|
export function FocusScreen({
|
||||||
onOpenIssue,
|
onOpenIssue,
|
||||||
@@ -66,7 +67,9 @@ export function FocusScreen({
|
|||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!focus) {
|
// The scheduler returns a FocusView even when the backlog has no open work —
|
||||||
|
// treat "no now/next/later" as empty, not just an absent `focus`.
|
||||||
|
if (!focus || (!focus.now && !focus.next && !focus.later)) {
|
||||||
return (
|
return (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon="coffee"
|
icon="coffee"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { PublicConfig } from '../../global.js'
|
import type { PublicConfig } from '../../global.js'
|
||||||
import { Badge, Button, Card, Icon, IconButton, Input, Radio, Select, Switch, Tag } from '../ui/index.js'
|
import { Badge, Button, Card, Icon, Radio, Tag } from '../ui/index.js'
|
||||||
|
|
||||||
// Settings — gitea connection, sync, model roles, labels, rituals, appearance
|
// Settings — gitea connection, sync behaviour, model, label schema, appearance.
|
||||||
export function SettingsScreen({
|
export function SettingsScreen({
|
||||||
dark,
|
dark,
|
||||||
setDark,
|
setDark,
|
||||||
@@ -17,10 +17,20 @@ export function SettingsScreen({
|
|||||||
onReconnect?: () => void
|
onReconnect?: () => void
|
||||||
onDisconnect?: () => void
|
onDisconnect?: () => void
|
||||||
}) {
|
}) {
|
||||||
const [webhooks, setWebhooks] = React.useState(true)
|
const [model, setModel] = React.useState<{ configured: boolean; model: string | null } | null>(null)
|
||||||
const [reconcile, setReconcile] = React.useState(true)
|
|
||||||
const [poll, setPoll] = React.useState(true)
|
React.useEffect(() => {
|
||||||
const [nag, setNag] = React.useState(true)
|
let alive = true
|
||||||
|
window.commitea.model
|
||||||
|
.status()
|
||||||
|
.then((s) => {
|
||||||
|
if (alive) setModel(s)
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
return () => {
|
||||||
|
alive = false
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const Row = ({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) => (
|
const Row = ({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) => (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, ...style }}>{children}</div>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, ...style }}>{children}</div>
|
||||||
@@ -33,128 +43,127 @@ export function SettingsScreen({
|
|||||||
<div style={{ maxWidth: 720, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
<div style={{ maxWidth: 720, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
<header style={{ borderBottom: 'var(--rule-double)', paddingBottom: 14 }}>
|
<header style={{ borderBottom: 'var(--rule-double)', paddingBottom: 14 }}>
|
||||||
<h1 style={{ font: 'var(--text-display)', color: 'var(--ink-1)', margin: 0 }}>Settings</h1>
|
<h1 style={{ font: 'var(--text-display)', color: 'var(--ink-1)', margin: 0 }}>Settings</h1>
|
||||||
<p style={{ font: 'var(--text-data)', color: 'var(--ink-3)', margin: '6px 0 0' }}>config lives in pm-state · versioned, portable</p>
|
<p style={{ font: 'var(--text-data)', color: 'var(--ink-3)', margin: '6px 0 0' }}>
|
||||||
|
connection lives on this machine · the plan lives in gitea
|
||||||
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<Card overline="Gitea" title="Connection">
|
<Card overline="Gitea" title="Connection">
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
{connection ? (
|
{connection ? (
|
||||||
<>
|
<>
|
||||||
<Row style={{ padding: '8px 12px', background: 'var(--paper-0)', border: '1px solid var(--line-1)', borderRadius: 'var(--radius-2)' }}>
|
<Row
|
||||||
|
style={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'var(--paper-0)',
|
||||||
|
border: '1px solid var(--line-1)',
|
||||||
|
borderRadius: 'var(--radius-2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Icon name="git-branch" size={14} style={{ color: 'var(--ink-3)' }} />
|
<Icon name="git-branch" size={14} style={{ color: 'var(--ink-3)' }} />
|
||||||
<span style={{ font: 'var(--text-data)', color: 'var(--ink-1)', flex: 1 }}>{connection.owner}/{connection.repo}</span>
|
<span style={{ font: 'var(--text-data)', color: 'var(--ink-1)', flex: 1 }}>
|
||||||
<Badge tone="ok" dot>connected</Badge>
|
{connection.owner}/{connection.repo}
|
||||||
|
</span>
|
||||||
|
<Badge tone="ok" dot>
|
||||||
|
connected
|
||||||
|
</Badge>
|
||||||
</Row>
|
</Row>
|
||||||
<Row style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', gap: 6 }}>
|
<Row style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', gap: 6 }}>
|
||||||
<Icon name="link" size={12} />{connection.baseUrl}
|
<Icon name="link" size={12} />
|
||||||
|
{connection.baseUrl}
|
||||||
</Row>
|
</Row>
|
||||||
<Row style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', gap: 6 }}>
|
<Row style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', gap: 6 }}>
|
||||||
<Icon name="layers" size={12} />sidecar: {connection.pmStateRepo ?? `${connection.repo}-pm-state`}
|
<Icon name="layers" size={12} />
|
||||||
</Row>
|
sidecar: {connection.pmStateRepo ?? `${connection.repo}-pm-state`}
|
||||||
<Row style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', gap: 6 }}>
|
|
||||||
<Icon name="sparkles" size={12} />model: {connection.modelUrl ?? 'not set — chat off'}
|
|
||||||
</Row>
|
</Row>
|
||||||
<Note>Your token is stored encrypted on this machine. Delete the sidecar and resync — no truth is lost.</Note>
|
<Note>Your token is stored encrypted on this machine. Delete the sidecar and resync — no truth is lost.</Note>
|
||||||
<Row style={{ gap: 8, marginTop: 2 }}>
|
<Row style={{ gap: 8, marginTop: 2 }}>
|
||||||
<Button variant="secondary" size="sm" icon="settings-2" onClick={onReconnect}>Reconfigure</Button>
|
<Button variant="secondary" size="sm" icon="settings-2" onClick={onReconnect}>
|
||||||
<Button variant="ghost" size="sm" onClick={onDisconnect}>Disconnect</Button>
|
Reconfigure
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" onClick={onDisconnect}>
|
||||||
|
Disconnect
|
||||||
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Row style={{ gap: 8 }}>
|
<Row style={{ gap: 8 }}>
|
||||||
<Note>Not connected.</Note>
|
<Note>Not connected.</Note>
|
||||||
<Button variant="secondary" size="sm" onClick={onReconnect}>Connect</Button>
|
<Button variant="secondary" size="sm" onClick={onReconnect}>
|
||||||
|
Connect
|
||||||
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card overline="Sync" title="Staying current">
|
<Card overline="Sync" title="Staying current">
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||||
<Row>
|
<Row style={{ gap: 8 }}>
|
||||||
<Switch label="Webhooks while running" checked={webhooks} onChange={(e) => setWebhooks(e.target.checked)} />
|
<Icon name="refresh-cw" size={14} style={{ color: 'var(--ink-3)' }} />
|
||||||
<span style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-3)', marginLeft: 'auto' }}>endpoint :48731 · healthy</span>
|
<span style={{ font: 'var(--text-body)', color: 'var(--ink-1)' }}>
|
||||||
|
Reconciles on launch and after every write
|
||||||
|
</span>
|
||||||
</Row>
|
</Row>
|
||||||
<Switch label="Full reconcile on launch" checked={reconcile} onChange={(e) => setReconcile(e.target.checked)} />
|
<Note>
|
||||||
<Row>
|
The board is a rebuildable index over gitea — the durable truth. When gitea is unreachable the last
|
||||||
<Switch label="Poll fallback" checked={poll} onChange={(e) => setPoll(e.target.checked)} />
|
snapshot is served from cache (the rail's dot turns red), and the next successful reconcile supersedes it.
|
||||||
<div style={{ marginLeft: 'auto', width: 140 }}>
|
No webhooks, no polling: reads are on demand.
|
||||||
<Select options={[{ value: '2', label: 'every 2 min' }, { value: '5', label: 'every 5 min' }, { value: '15', label: 'every 15 min' }]} defaultValue="5" />
|
</Note>
|
||||||
</div>
|
|
||||||
</Row>
|
|
||||||
<Note>last reconcile 3.2s · 500 issues · nothing lost</Note>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card overline="Models" title="The router">
|
<Card overline="Model" title="Reginald's brain">
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px 14px' }}>
|
{model?.configured ? (
|
||||||
<Select label="Prose & rituals" options={[
|
<Row style={{ gap: 8 }}>
|
||||||
{ value: 'gemma-4b', label: 'gemma-4b · local' },
|
<Icon name="sparkles" size={14} style={{ color: 'var(--ink-3)' }} />
|
||||||
{ value: 'qwen-7b', label: 'qwen-7b · local' },
|
<span style={{ font: 'var(--text-data)', color: 'var(--ink-1)', flex: 1 }}>{model.model}</span>
|
||||||
]} defaultValue="gemma-4b" />
|
<Badge tone="ok" dot>
|
||||||
<Input label="Base URL" mono defaultValue="http://localhost:1234/v1" />
|
reachable
|
||||||
<Select label="Decomposition & negotiation" options={[
|
</Badge>
|
||||||
{ value: 'qwen-72b', label: 'qwen-72b · lm-studio box' },
|
</Row>
|
||||||
{ value: 'gpt-4o', label: 'gpt-4o · OpenAI API' },
|
) : (
|
||||||
]} defaultValue="qwen-72b" />
|
<Row style={{ gap: 8 }}>
|
||||||
<Input label="Base URL" mono defaultValue="http://10.0.0.42:1234/v1" />
|
<Icon name="sparkles" size={14} style={{ color: 'var(--ink-3)' }} />
|
||||||
</div>
|
<span style={{ font: 'var(--text-body)', color: 'var(--ink-2)', flex: 1 }}>
|
||||||
<Row>
|
Chat is off — no model reachable.
|
||||||
<span style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-3)' }}>hot memory ≤ 2k tokens · math is never delegated to either</span>
|
</span>
|
||||||
</Row>
|
<Button variant="secondary" size="sm" onClick={onReconnect}>
|
||||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: 0 }}>
|
Set a Model URL
|
||||||
The small one writes my standup; the large one argues with your estimates. Neither is allowed near the arithmetic.
|
</Button>
|
||||||
</p>
|
</Row>
|
||||||
|
)}
|
||||||
|
<Note>
|
||||||
|
An OpenAI-compatible endpoint (set it in Reconfigure). Reginald names, negotiates and explains; it never
|
||||||
|
does the arithmetic — the scheduler and Monte Carlo do that.
|
||||||
|
</Note>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card overline="Labels" title="Schema">
|
<Card overline="Labels" title="Schema">
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||||
<Row style={{ flexWrap: 'wrap', gap: 6 }}>
|
<Row style={{ flexWrap: 'wrap', gap: 6 }}>
|
||||||
{['est/1d', 'est/2d', 'est/3d', 'est/5d', 'est/8d'].map((l) => <Tag key={l} label={l} />)}
|
{['est/1d', 'est/2d', 'est/3d', 'est/5d', 'est/8d'].map((l) => (
|
||||||
|
<Tag key={l} label={l} />
|
||||||
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
<Row style={{ flexWrap: 'wrap', gap: 6 }}>
|
<Row style={{ flexWrap: 'wrap', gap: 6 }}>
|
||||||
{['p/1', 'p/2', 'p/3', 'p/4'].map((l) => <Tag key={l} label={l} />)}
|
{['p/1', 'p/2', 'p/3', 'p/4'].map((l) => (
|
||||||
|
<Tag key={l} label={l} />
|
||||||
|
))}
|
||||||
<Tag label="deadline/hard" />
|
<Tag label="deadline/hard" />
|
||||||
</Row>
|
</Row>
|
||||||
<Note>Fixed sets, human-meaningful, visible in gitea. Not configurable — that is rather the point.</Note>
|
<Note>Fixed sets, human-meaningful, visible in gitea. Not configurable — that is rather the point.</Note>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card overline="Rituals" title="Reginald's calendar">
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
||||||
<Row>
|
|
||||||
<span style={{ font: 'var(--text-body)', color: 'var(--ink-1)' }}>Morning standup</span>
|
|
||||||
<div style={{ marginLeft: 'auto', width: 120 }}>
|
|
||||||
<Select options={[{ value: '0630', label: '06:30' }, { value: '0700', label: '07:00' }, { value: '0800', label: '08:00' }]} defaultValue="0700" />
|
|
||||||
</div>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<Switch label="Stale-blocker nagging" checked={nag} onChange={(e) => setNag(e.target.checked)} />
|
|
||||||
<div style={{ marginLeft: 'auto', width: 140 }}>
|
|
||||||
<Select options={[{ value: '2', label: 'after 2 days' }, { value: '3', label: 'after 3 days' }, { value: '5', label: 'after 5 days' }]} defaultValue="3" />
|
|
||||||
</div>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card overline="Appearance" title="Service">
|
<Card overline="Appearance" title="Service">
|
||||||
<div style={{ display: 'flex', gap: 20 }}>
|
<div style={{ display: 'flex', gap: 20 }}>
|
||||||
<Radio name="theme" label="Morning (light)" checked={!dark} onChange={() => setDark(false)} />
|
<Radio name="theme" label="Morning (light)" checked={!dark} onChange={() => setDark(false)} />
|
||||||
<Radio name="theme" label="Evening (dark)" checked={dark} onChange={() => setDark(true)} />
|
<Radio name="theme" label="Evening (dark)" checked={dark} onChange={() => setDark(true)} />
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
|
||||||
<Row>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<div style={{ font: 'var(--text-body-strong)', color: 'var(--ink-1)' }}>Forget this gitea</div>
|
|
||||||
<Note>Removes the connection and the local cache. Gitea itself is untouched.</Note>
|
|
||||||
</div>
|
|
||||||
<Button variant="danger">Forget</Button>
|
|
||||||
</Row>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,39 +161,59 @@ export function StandupScreen({
|
|||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section overline="Stale blockers" order={3}>
|
<Section overline="Stale blockers" order={3}>
|
||||||
<div
|
{s.nag.id === 0 ? (
|
||||||
role="button"
|
// Calm sentinel — nothing is over-steeping; render just the reassurance,
|
||||||
tabIndex={0}
|
// no issue chrome, no click target (there's no #0 to open).
|
||||||
onClick={() => onOpenIssue(NAG_REF)}
|
<div
|
||||||
onKeyDown={(e) => {
|
style={{
|
||||||
if (e.key === 'Enter') onOpenIssue(NAG_REF)
|
display: 'flex',
|
||||||
}}
|
gap: 10,
|
||||||
style={{
|
alignItems: 'center',
|
||||||
display: 'flex',
|
background: 'var(--paper-2)',
|
||||||
gap: 10,
|
borderRadius: 'var(--radius-2)',
|
||||||
alignItems: 'flex-start',
|
padding: '12px 14px',
|
||||||
cursor: 'pointer',
|
}}
|
||||||
background: 'var(--warn-tint)',
|
>
|
||||||
borderRadius: 'var(--radius-2)',
|
<span style={{ color: 'var(--ok)', display: 'inline-flex' }}>
|
||||||
padding: '12px 14px',
|
<Icon name="circle-check" size={15} />
|
||||||
}}
|
</span>
|
||||||
>
|
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: 0 }}>{s.nag.text}</p>
|
||||||
<span style={{ color: 'var(--warn)', display: 'inline-flex', marginTop: 2 }}>
|
|
||||||
<Icon name="clock" size={15} />
|
|
||||||
</span>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
|
||||||
<span style={{ font: '500 12.5px var(--font-mono)', color: 'var(--ink-1)' }}>#{s.nag.id}</span>
|
|
||||||
<Badge tone="warn" dot>
|
|
||||||
steeping {s.nag.days}
|
|
||||||
</Badge>
|
|
||||||
<span style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-3)' }}>
|
|
||||||
blocks {s.nag.blocks.join(', ')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: '5px 0 0' }}>{s.nag.text}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => onOpenIssue(NAG_REF)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') onOpenIssue(NAG_REF)
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 10,
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: 'var(--warn-tint)',
|
||||||
|
borderRadius: 'var(--radius-2)',
|
||||||
|
padding: '12px 14px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ color: 'var(--warn)', display: 'inline-flex', marginTop: 2 }}>
|
||||||
|
<Icon name="clock" size={15} />
|
||||||
|
</span>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||||
|
<span style={{ font: '500 12.5px var(--font-mono)', color: 'var(--ink-1)' }}>#{s.nag.id}</span>
|
||||||
|
<Badge tone="warn" dot>
|
||||||
|
steeping {s.nag.days}
|
||||||
|
</Badge>
|
||||||
|
<span style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-3)' }}>
|
||||||
|
blocks {s.nag.blocks.join(', ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: '5px 0 0' }}>{s.nag.text}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<footer
|
<footer
|
||||||
|
|||||||
Reference in New Issue
Block a user