console: the page carries what matters, the file holds everything
Fifty-seven fields over thirteen groups sat open on the lead page, which pushed the audit trail — the part people actually read — a screen and a half down. Most of it is reference material: nobody opens a lead to find its source channel. They open it to see who the customer is, what is being insured, what has been attached, and what this stage turns on. So the page carries exactly that — four cards at most, the fourth chosen by the stage the lead is in — and the complete record moves into a drawer behind one click. Nothing is dropped: the drawer is handed the same computed groups the page used to render, so the two cannot disagree about a value. Hiding is only safe while it stays findable, so the trigger names the number it is holding back rather than saying "more", and the drawer brings a filter over labels AND values, because Ctrl-F was how people read the open version and a click breaks that. Long values are clamped in both places. uw_referral_reason and lost_reason run to a paragraph, and a paragraph printed raw in a summary card would rebuild the wall this exists to remove. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
db91daabf8
commit
49d5443c60
65
src/components/LeadFileDialog.css
Normal file
65
src/components/LeadFileDialog.css
Normal file
@ -0,0 +1,65 @@
|
||||
/* Wide and tall: this is the whole record, and squeezing thirteen groups into
|
||||
a prose-width dialog would only move the crowding rather than remove it. */
|
||||
.lfd { width: min(1180px, 100%); max-height: min(90vh, 940px); }
|
||||
|
||||
.lfd__sub {
|
||||
margin: 4px 0 0;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.5;
|
||||
color: var(--zk-muted);
|
||||
}
|
||||
|
||||
.lfd__tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid var(--zk-line-soft);
|
||||
background: var(--zk-tint);
|
||||
}
|
||||
|
||||
/* Ctrl-F was how people read the open version. Behind a click that stops
|
||||
working, so the drawer brings its own. */
|
||||
.lfd__find {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 12px;
|
||||
background: var(--zk-white);
|
||||
border: 1px solid var(--zk-line);
|
||||
border-radius: var(--r-pill);
|
||||
}
|
||||
|
||||
.lfd__find:focus-within { border-color: var(--zk-blue); box-shadow: 0 0 0 3px var(--zk-tint-blue); }
|
||||
.lfd__find svg { width: 14px; height: 14px; flex: none; color: var(--zk-grey); }
|
||||
|
||||
.lfd__find input {
|
||||
flex: 1;
|
||||
border: 0;
|
||||
outline: none;
|
||||
background: none;
|
||||
font: inherit;
|
||||
font-size: 0.84rem;
|
||||
color: var(--zk-ink);
|
||||
}
|
||||
|
||||
.lfd__find input::-webkit-search-cancel-button { cursor: pointer; }
|
||||
|
||||
.lfd__count {
|
||||
flex: none;
|
||||
font-size: 0.74rem;
|
||||
color: var(--zk-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.lfd__body { overflow-y: auto; }
|
||||
.lfd__body .file360 { padding: 18px 22px 24px; }
|
||||
|
||||
.lfd__none {
|
||||
margin: 0;
|
||||
padding: 44px 24px;
|
||||
text-align: center;
|
||||
font-size: 0.86rem;
|
||||
color: var(--zk-grey);
|
||||
}
|
||||
163
src/components/LeadFileDialog.jsx
Normal file
163
src/components/LeadFileDialog.jsx
Normal file
@ -0,0 +1,163 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import ClampText from './ClampText.jsx'
|
||||
import './LeadFileDialog.css'
|
||||
|
||||
/**
|
||||
* Everything on the lead, on demand.
|
||||
*
|
||||
* This used to sit open on the page. Fifty-seven fields across thirteen groups
|
||||
* pushed the audit trail — the part anyone actually reads — a screen and a half
|
||||
* down, and most of those fields are reference material: nobody opens a lead to
|
||||
* find out its source channel. The page now carries the handful that matter for
|
||||
* where the lead IS, and the complete record lives one click away in here.
|
||||
*
|
||||
* NOTHING IS LOST AND NOTHING IS BURIED. The groups, the order and the values
|
||||
* are the same ones the page rendered before — this receives them already
|
||||
* computed, so the two can never disagree. The trigger says how many fields are
|
||||
* behind it, because a control that hides fifty-seven things should say so.
|
||||
*
|
||||
* The filter is the point of doing this as a drawer rather than a collapse.
|
||||
* Ctrl-F is what people were using on the open version; with the record behind
|
||||
* a click that stops working, so the drawer brings its own. It matches the
|
||||
* label AND the value, so "KA01" finds the vehicle and "POSP" finds the
|
||||
* partner, and it says how many groups it hid rather than silently emptying.
|
||||
*/
|
||||
export default function LeadFileDialog({ groups, labels, label, money, fileHref, longAt, onClose }) {
|
||||
const ref = useRef(null)
|
||||
const restore = useRef(null)
|
||||
const [q, setQ] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
restore.current = document.activeElement
|
||||
ref.current?.focus()
|
||||
const onKey = (e) => { if (e.key === 'Escape') onClose() }
|
||||
document.addEventListener('keydown', onKey)
|
||||
const prev = document.body.style.overflow
|
||||
document.body.style.overflow = 'hidden'
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKey)
|
||||
document.body.style.overflow = prev
|
||||
if (restore.current instanceof HTMLElement) restore.current.focus()
|
||||
}
|
||||
}, [onClose])
|
||||
|
||||
const total = groups.reduce((n, [, shown]) => n + shown.length, 0)
|
||||
|
||||
const shownGroups = useMemo(() => {
|
||||
const term = q.trim().toLowerCase()
|
||||
if (!term) return groups
|
||||
return groups
|
||||
.map(([title, rows]) => [
|
||||
title,
|
||||
rows.filter(([k, v]) => {
|
||||
const l = String(labels[k] ?? label(k)).toLowerCase()
|
||||
return l.includes(term) || String(v).toLowerCase().includes(term)
|
||||
}),
|
||||
])
|
||||
.filter(([, rows]) => rows.length)
|
||||
}, [groups, q, labels, label])
|
||||
|
||||
const found = shownGroups.reduce((n, [, rows]) => n + rows.length, 0)
|
||||
|
||||
return (
|
||||
<div className="prose__scrim" onClick={onClose} role="presentation">
|
||||
<div
|
||||
className="prose__dlg lfd"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Lead file"
|
||||
tabIndex={-1}
|
||||
ref={ref}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="prose__head">
|
||||
<div>
|
||||
<h3>Lead file</h3>
|
||||
<p className="lfd__sub">
|
||||
Everything captured, grouped by the step that captured it.
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" onClick={onClose} aria-label="Close">
|
||||
<svg viewBox="0 0 14 14" aria-hidden="true">
|
||||
<path d="M3.5 3.5l7 7M10.5 3.5l-7 7" fill="none" stroke="currentColor"
|
||||
strokeWidth="1.6" strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="lfd__tools">
|
||||
<label className="lfd__find">
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<circle cx="7" cy="7" r="4.2" fill="none" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path d="M10.2 10.2 13.5 13.5" fill="none" stroke="currentColor"
|
||||
strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
<input
|
||||
type="search"
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
placeholder="Find a field or a value…"
|
||||
aria-label="Filter the lead file"
|
||||
/>
|
||||
</label>
|
||||
<span className="lfd__count">
|
||||
{q.trim() ? `${found} of ${total} fields` : `${total} fields`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="lfd__body">
|
||||
{shownGroups.length ? (
|
||||
<div className="file360">
|
||||
{shownGroups.map(([title, shown]) => {
|
||||
const facts = shown.filter(([, v, files]) => files.length || v.length <= longAt)
|
||||
const notes = shown.filter(([, v, files]) => !files.length && v.length > longAt)
|
||||
return (
|
||||
<section className="fgroup" key={title}>
|
||||
<h3>{title}<span>{shown.length}</span></h3>
|
||||
|
||||
{facts.length ? (
|
||||
<dl className="props">
|
||||
{facts.map(([k, v, files]) => (
|
||||
<div className="prop" key={k}>
|
||||
<dt>{labels[k] ?? label(k)}</dt>
|
||||
{files.length ? (
|
||||
<dd className="prop__files">
|
||||
{files.map((f) => (
|
||||
<a key={f.uuid} className="prop__file" href={fileHref(f)}
|
||||
target="_blank" rel="noreferrer">
|
||||
{f.original_name || 'Document'}
|
||||
</a>
|
||||
))}
|
||||
</dd>
|
||||
) : (
|
||||
<dd className={money.has(k) ? 'prop__num' : undefined}>{v}</dd>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
) : null}
|
||||
|
||||
{notes.length ? (
|
||||
<div className="fnotes">
|
||||
{notes.map(([k, v]) => (
|
||||
<div className="fnote" key={k}>
|
||||
<span className="fnote__l">{labels[k] ?? label(k)}</span>
|
||||
<ClampText text={v} title={labels[k] ?? label(k)} lines={2} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="lfd__none">
|
||||
Nothing on this lead matches “{q.trim()}”.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -7,9 +7,10 @@ import AgentChip from '../components/AgentChip.jsx'
|
||||
import CallTranscript from '../components/CallTranscript.jsx'
|
||||
import ClampText from '../components/ClampText.jsx'
|
||||
import Conversation from '../components/Conversation.jsx'
|
||||
import LeadFileDialog from '../components/LeadFileDialog.jsx'
|
||||
import StageRail from '../components/StageRail.jsx'
|
||||
import Timeline from '../components/Timeline.jsx'
|
||||
import { APP_ID, DV_LEAD, PRODUCTS, STAGES, STALL_AFTER_MS, blockedOn, nudgeFor, phaseOf } from '../api/config.js'
|
||||
import { APP_ID, DOC_SLOTS, DV_LEAD, PRODUCTS, STAGES, STALL_AFTER_MS, blockedOn, nudgeFor, phaseOf } from '../api/config.js'
|
||||
import { AGENTS } from '../api/agents.js'
|
||||
import { actionsFor, rolesOf } from '../api/permissions.js'
|
||||
import { describeError } from '../api/errors.js'
|
||||
@ -87,6 +88,44 @@ const GROUPS = [
|
||||
/** Past this, a value is prose and gets folded rather than printed in full. */
|
||||
const LONG = 150
|
||||
|
||||
/**
|
||||
* WHAT THE PAGE SHOWS WITHOUT BEING ASKED.
|
||||
*
|
||||
* Three things are always true of a lead — who the customer is, what is being
|
||||
* insured, and what has been attached — so those are always here. The fourth
|
||||
* section depends on where the lead has got to: a lead in Quoted needs the
|
||||
* cover and the quote's expiry; the same lead in Onboarded needs the policy
|
||||
* number and when it renews. Everything else is in the full file.
|
||||
*
|
||||
* THIS IS A MIRROR, in the same sense as STAGES, ACTIONS and DOC_SLOTS, and it
|
||||
* carries the same hazard: a stage not listed here simply gets no fourth
|
||||
* section, and a field renamed in the workflow quietly stops appearing. That is
|
||||
* the safe direction to fail — the full file is generated from the data and
|
||||
* still holds everything — but it is a mirror, not a source.
|
||||
*
|
||||
* The metrics strip at the top of the page already carries premium, commission,
|
||||
* product, renewal date, lead age and AI confidence. Nothing is repeated here.
|
||||
*/
|
||||
const GLANCE_STAGE = {
|
||||
'zk-state-new': ['lead_score', 'eligibility_outcome'],
|
||||
'zk-state-qualified': ['lead_score', 'eligibility_outcome', 'outreach_window'],
|
||||
'zk-state-contacted': ['contact_outcome', 'outreach_window'],
|
||||
'zk-state-docs': ['documents_status', 'motor_prev_expiry'],
|
||||
'zk-state-quoted': ['ai_recommended_cover', 'quote_valid_till', 'acceptance_ref'],
|
||||
'zk-state-payment': ['payment_ref', 'quote_valid_till'],
|
||||
'zk-state-issued': ['policy_no', 'policy_issued_at', 'payment_ref'],
|
||||
'zk-state-onboarded': ['policy_no', 'policy_issued_at', 'renewal_due', 'payout_status'],
|
||||
'zk-state-referred': ['uw_outcome', 'uw_referral_reason'],
|
||||
'zk-state-parked': ['contact_outcome', 'outreach_window'],
|
||||
'zk-state-lost': ['lost_reason', 'contact_outcome'],
|
||||
}
|
||||
|
||||
/** The risk, in whichever line's vocabulary this lead is written. */
|
||||
const GLANCE_RISK = {
|
||||
motor: ['motor_reg_no', 'motor_make_model', 'motor_mfg_year', 'motor_idv', 'motor_ncb_pct'],
|
||||
sme: ['sme_product_variant', 'sme_occupancy', 'sme_value_at_risk'],
|
||||
}
|
||||
|
||||
/**
|
||||
* Field ids are snake_case and several carry acronyms, which sentence-casing
|
||||
* turns into "Pan" and "Gstin". Only the words that need it are listed; every
|
||||
@ -154,6 +193,8 @@ export default function Lead() {
|
||||
const [showChat, setShowChat] = useState(false)
|
||||
const [openAgent, setOpenAgent] = useState(null)
|
||||
const [showCall, setShowCall] = useState(false)
|
||||
// The full record, on demand — see the panel comment below.
|
||||
const [showFile, setShowFile] = useState(false)
|
||||
// A clock in state rather than Date.now() in the render body. Reading the
|
||||
// wall clock while rendering is impure — React may render twice and get two
|
||||
// answers — and it also means the "nothing for 6 minutes" counter would only
|
||||
@ -258,6 +299,27 @@ export default function Lead() {
|
||||
// anyway; showing them a row of buttons that all 403 reads as a broken app
|
||||
// rather than as a control.
|
||||
const actions = actionsFor(roles, stage?.uid)
|
||||
const fieldCount = groups.reduce((n, [, shown]) => n + shown.length, 0)
|
||||
|
||||
// Built from the SAME rows the full file renders, so the two can never
|
||||
// disagree about a value. A section with nothing in it is dropped rather
|
||||
// than rendered empty — an SME lead has no registration number, and a
|
||||
// heading over a blank box reads as a bug.
|
||||
const pick = (keys) => keys
|
||||
.map((k) => [k, fmt(k, row[k]), filesOf(row[k])])
|
||||
.filter(([, v]) => v !== null)
|
||||
|
||||
const docKeys = Object.keys(DOC_SLOTS).filter((k) => filesOf(row[k]).length)
|
||||
const line = row.product_line === 'sme_package' ? 'sme' : 'motor'
|
||||
|
||||
const glance = [
|
||||
['Customer', pick(['customer_name', 'entity_name', 'mobile', 'email'])],
|
||||
[line === 'sme' ? 'The risk' : 'The vehicle', pick(GLANCE_RISK[line])],
|
||||
['Documents', pick(docKeys)],
|
||||
['This stage', pick(GLANCE_STAGE[stage?.uid] || [])],
|
||||
].filter(([, rows]) => rows.length)
|
||||
|
||||
|
||||
// Two different reasons for an empty action list, and they must not read the
|
||||
// same. A terminal lead is finished; a live one you cannot act on belongs to
|
||||
// somebody else, and saying "closed" about it would be a lie.
|
||||
@ -609,55 +671,38 @@ export default function Lead() {
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* EVERYTHING AT ONCE.
|
||||
This was tabs — thirteen of them, one group visible at a time —
|
||||
so "what do we know about this lead?" meant clicking thirteen
|
||||
times and holding the answer in your head. It is a lead FILE; a
|
||||
file you can only read one page of is a filing cabinet.
|
||||
Now every group is a card, laid out in columns that reflow, so
|
||||
the whole 360 is one scroll and Ctrl-F finds anything. */}
|
||||
{groups.length ? (
|
||||
{/* WHAT MATTERS HERE, NOT EVERYTHING THERE IS.
|
||||
The whole record used to sit open on the page: fifty-seven
|
||||
fields over thirteen groups, which pushed the audit trail — the
|
||||
part people actually read — a screen and a half down. Most of it
|
||||
is reference material. Nobody opens a lead to find its source
|
||||
channel; they open it to see who the customer is, what is being
|
||||
insured, what has been attached, and what this stage turns on.
|
||||
|
||||
So the page carries that, and the complete file is one click
|
||||
away with a filter on it. Hiding is only safe when it stays
|
||||
findable, which is why the trigger below names the number it is
|
||||
holding back rather than saying "more". */}
|
||||
{glance.length ? (
|
||||
<div className="panel">
|
||||
<div className="panel__head">
|
||||
<div>
|
||||
<h2 className="panel__title">Lead file</h2>
|
||||
<h2 className="panel__title">At a glance</h2>
|
||||
<p className="panel__sub">
|
||||
Everything captured, grouped by the step that captured it.
|
||||
The customer, the risk, and what this stage turns on.
|
||||
</p>
|
||||
</div>
|
||||
<span className="panel__meta">
|
||||
{groups.reduce((n, [, shown]) => n + shown.length, 0)} fields
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="file360">
|
||||
{groups.map(([title, shown]) => {
|
||||
/* SHORT FACTS FIRST, PROSE AFTER.
|
||||
Seven of these fields are AI paragraphs — the attribution
|
||||
reason, the rationale, the call transcript — and rendering
|
||||
them in the same flow as "Mobile" made a card that was
|
||||
mostly tinted boxes with three key/value pairs lost among
|
||||
them. The facts are what people scan; the reasoning is what
|
||||
they occasionally open, and it is already in the trail
|
||||
alongside the step that wrote it. So the pairs group at the
|
||||
top where they can be read as a column, and the prose sits
|
||||
under a rule as a line and a link. */
|
||||
const facts = shown.filter(([, v, files]) => files.length || v.length <= LONG)
|
||||
const notes = shown.filter(([, v, files]) => !files.length && v.length > LONG)
|
||||
return (
|
||||
<div className="glance">
|
||||
{glance.map(([title, rows]) => (
|
||||
<section className="fgroup" key={title}>
|
||||
<h3>{title}<span>{shown.length}</span></h3>
|
||||
|
||||
{facts.length ? (
|
||||
<h3>{title}<span>{rows.length}</span></h3>
|
||||
<dl className="props">
|
||||
{facts.map(([k, v, files]) => (
|
||||
{rows.map(([k, v, files]) => (
|
||||
<div className="prop" key={k}>
|
||||
<dt>{labels[k] ?? label(k)}</dt>
|
||||
{files.length ? (
|
||||
/* An attached document is a thing to open, not
|
||||
a filename to read. The preview route is
|
||||
app-scoped and public, so a plain link needs
|
||||
no token. */
|
||||
<dd className="prop__files">
|
||||
{files.map((f) => (
|
||||
<a
|
||||
@ -670,28 +715,32 @@ export default function Lead() {
|
||||
</a>
|
||||
))}
|
||||
</dd>
|
||||
) : v.length > LONG ? (
|
||||
/* A stage reason can run to a paragraph —
|
||||
uw_referral_reason and lost_reason both do — and
|
||||
a paragraph printed raw here would rebuild the
|
||||
wall this panel exists to remove. */
|
||||
<dd><ClampText text={v} title={labels[k] ?? label(k)} lines={2} /></dd>
|
||||
) : (
|
||||
<dd className={MONEY.has(k) ? 'prop__num' : undefined}>{v}</dd>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
) : null}
|
||||
|
||||
{notes.length ? (
|
||||
<div className="fnotes">
|
||||
{notes.map(([k, v]) => (
|
||||
<div className="fnote" key={k}>
|
||||
<span className="fnote__l">{labels[k] ?? label(k)}</span>
|
||||
<ClampText text={v} title={labels[k] ?? label(k)} lines={2} />
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{fieldCount ? (
|
||||
<button type="button" className="glance__all" onClick={() => setShowFile(true)}>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M3 3.5h10M3 8h10M3 12.5h6" fill="none" stroke="currentColor"
|
||||
strokeWidth="1.4" strokeLinecap="round" />
|
||||
</svg>
|
||||
Open the full lead file
|
||||
<b>{fieldCount} fields</b>
|
||||
</button>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@ -750,6 +799,18 @@ export default function Lead() {
|
||||
{showCall ? (
|
||||
<CallTranscript text={row.call_transcript} name={row.customer_name} onClose={() => setShowCall(false)} />
|
||||
) : null}
|
||||
|
||||
{showFile ? (
|
||||
<LeadFileDialog
|
||||
groups={groups}
|
||||
labels={labels}
|
||||
label={label}
|
||||
money={MONEY}
|
||||
longAt={LONG}
|
||||
fileHref={(f) => `${client.baseUrl}/app/${APP_ID}/view/files/${f.uuid}/preview`}
|
||||
onClose={() => setShowFile(false)}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1487,6 +1487,87 @@
|
||||
|
||||
.file360 .clamp__more { margin-top: 3px; font-size: 0.7rem; }
|
||||
|
||||
/* ── at a glance ──────────────────────────────────────────────────────────
|
||||
The same card as the full file uses, so opening the drawer feels like more
|
||||
of the thing you were already reading rather than a different screen. Four
|
||||
sections at most, so a fixed auto-fit grid never leaves a lone card on a
|
||||
row of its own the way the thirteen-group version did. */
|
||||
.glance {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 14px;
|
||||
padding: 4px 22px 18px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.glance .props { display: block; padding: 0; }
|
||||
|
||||
.glance .prop {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 42%) minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: baseline;
|
||||
margin: 0;
|
||||
padding: 3px 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.glance .prop:hover { background: transparent; }
|
||||
.glance .prop dt { font-size: 0.71rem; line-height: 1.5; color: var(--zk-grey); margin: 0; }
|
||||
.glance .prop dd { font-size: 0.8rem; line-height: 1.5; color: var(--zk-ink); overflow-wrap: anywhere; }
|
||||
|
||||
/* Same de-weighting as the full file: at a glance a reason is one fact among
|
||||
a dozen, not the entry it is in the trail. */
|
||||
.glance .clamp__lead,
|
||||
.glance .clamp__text {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: var(--zk-muted);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.glance .clamp__more { margin-top: 3px; font-size: 0.7rem; }
|
||||
|
||||
/* HIDING IS ONLY SAFE WHILE IT STAYS FINDABLE, so this names the number it is
|
||||
holding rather than saying "more". Full width and on the panel's own floor:
|
||||
a link tucked into the header would be the discoverability problem that
|
||||
sinks progressive disclosure. */
|
||||
.glance__all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
width: 100%;
|
||||
padding: 13px 22px;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--zk-line-soft);
|
||||
border-radius: 0 0 var(--r-lg) var(--r-lg);
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
color: var(--zk-blue-dark);
|
||||
transition: background var(--t-fast);
|
||||
}
|
||||
|
||||
.glance__all:hover { background: var(--zk-tint); }
|
||||
.glance__all:focus-visible { outline: 2px solid var(--zk-blue); outline-offset: -2px; }
|
||||
.glance__all svg { width: 15px; height: 15px; flex: none; }
|
||||
|
||||
.glance__all b {
|
||||
margin-left: auto;
|
||||
font-weight: 400;
|
||||
font-size: 0.74rem;
|
||||
color: var(--zk-grey);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.panel__meta {
|
||||
flex: none;
|
||||
font-size: 0.74rem;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user