Mara: "result looks like the shape i am looking for, but the code does not. you did not follow component first principle" - the card's clickable/selectable mechanics, the card-view filter trigger+popover, and the list+detail split layout were all one-off page-local JSX in AgentsPage.tsx instead of docs/web-ui/design-guide.md's "Component-first design" primitives. Three new ui/ components, each with a same-day /components demo section per that doc's own rule: - ui/card/Card.tsx - the role=button/keyboard-activation/selected mechanics AgentCard now wraps agent-specific content around, instead of owning them itself. - ui/multiselect-filter/MultiselectFilter.tsx - the checkbox-list trigger+popover control. This was also a straight duplicate of Table's own inline popover content once AgentsPage's filter toolbar needed the identical thing; Table now renders the same MultiselectFilterOptions piece too (keeping its own th-anchored trigger and fixed+portal positioning, which are genuinely table-specific), not a second copy. - ui/split-view/SplitView.tsx - the list+detail flex-wrap layout, no opinion on what's inside either pane. AgentsPage.tsx's own CSS shrinks to just the agent-specific content inside these primitives (card line/message layout, detail-panel field grid, the name-search input) - the container/positioning rules moved to each component's own colocated CSS. No behavior change for any other Table caller (HivesPage, IssueReportPage, the components demo's own Table samples) - the popover's visual output is identical, just sourced from the shared component instead of inline JSX. Verified: typecheck/build clean, real screenshots of both AgentsPage (pixel-identical to before) and the three new /components sections, plus a live click confirming MultiselectFilter's popover opens correctly on the demo page too.
183 lines
5.7 KiB
CSS
183 lines
5.7 KiB
CSS
/* Scrolls horizontally within its own box on a narrow viewport instead
|
|
of forcing the whole page to overflow — see Table.tsx's comment. */
|
|
.ui-table-scroll {
|
|
overflow-x: auto;
|
|
}
|
|
.ui-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
.ui-table th {
|
|
text-align: left;
|
|
font-weight: 600;
|
|
color: var(--muted);
|
|
padding: 0.5em 0.75em;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.ui-table td {
|
|
padding: 0.5em 0.75em;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.ui-table tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
.ui-table-empty {
|
|
color: var(--muted);
|
|
text-align: center;
|
|
padding: 1.25em 0.75em;
|
|
}
|
|
|
|
/* A cell holding free-form prose (an operator- or agent-provided string,
|
|
not a short discrete label) rather than open-ended growth that drags
|
|
the whole table wider — caps the column and wraps within it instead. */
|
|
.ui-table-prose {
|
|
max-width: 32em;
|
|
white-space: normal;
|
|
word-break: break-word;
|
|
}
|
|
|
|
/* Sortable header — the whole header cell is the click target, not a
|
|
separate icon-only button next to it, same "control lives on the
|
|
thing it affects" reasoning as `Badge`'s own click-to-toggle shape. */
|
|
.ui-table-sort-button {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.35em;
|
|
background: none;
|
|
border: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
font: inherit;
|
|
font-weight: 600;
|
|
color: inherit;
|
|
cursor: pointer;
|
|
}
|
|
.ui-table-sort-button:hover {
|
|
color: var(--fg);
|
|
}
|
|
.ui-table-sort-glyph {
|
|
opacity: 0.6;
|
|
font-size: 0.85em;
|
|
}
|
|
.ui-table-sort-button:hover .ui-table-sort-glyph {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* `.ui-table-th-filterable` (set on a filterable column's `<th>`, no
|
|
rule of its own) exists purely as a `:hover`/`:focus-within` selector
|
|
target for the fade below — nothing to do with positioning the
|
|
popover itself, which is computed in JS and rendered via a portal
|
|
instead (see Table.tsx's own comment on `popoverPos` for why it
|
|
isn't just `position: absolute` under this `<th>`).
|
|
|
|
The filter-icon trigger — invisible by default, `opacity` transition
|
|
rather than a hard show/hide (mara: "fade in out animation"). Shown
|
|
on three signals, matching Table.tsx's own comment on `filterValue`:
|
|
hovering/focusing the header cell (so it's discoverable without a
|
|
filter already set), the popover being open (still visible while
|
|
you're using it, even if the pointer has moved off the header), or
|
|
`.ui-table-filter-icon-active` — a filter is *set* on this column,
|
|
so the icon stays put as a persistent "this column is filtered"
|
|
indicator rather than disappearing the moment the header isn't
|
|
hovered (mara: "when a filter is set, the filter icon does not
|
|
disappear"). `:focus-within` (not just `:hover`) so a keyboard user
|
|
tabbing to the icon sees it appear too, not just a mouse hovering. */
|
|
.ui-table-filter-icon {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-left: 0.35em;
|
|
padding: 0.15em;
|
|
background: none;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: var(--muted);
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
transition: opacity 0.15s ease;
|
|
}
|
|
.ui-table-th-filterable:hover .ui-table-filter-icon,
|
|
.ui-table-th-filterable:focus-within .ui-table-filter-icon,
|
|
.ui-table-filter-icon-active {
|
|
opacity: 1;
|
|
}
|
|
.ui-table-filter-icon:hover {
|
|
color: var(--fg);
|
|
background: var(--bg);
|
|
}
|
|
/* A filter is live on this column — `--purple`, theme.css's own "active
|
|
tabs, links, highlights" accent, so an operator scanning the header
|
|
row reads it the same way anything else marked active does. */
|
|
.ui-table-filter-icon-active {
|
|
color: var(--purple);
|
|
}
|
|
|
|
/* `position`/`top`/`left` are inline styles, not here — Table.tsx
|
|
computes them from the anchor `<th>`'s own `getBoundingClientRect()`
|
|
and renders this via a portal onto `document.body` (see its own
|
|
`popoverPos` comment for the full reasoning: escaping
|
|
`.ui-table-scroll`'s clip). `z-index: 10` matches the existing
|
|
overlay-menu convention (`LinksMenu`/`UserMenu`), now that this
|
|
sits alongside them as a `document.body`-level sibling rather than
|
|
nested inside the table. */
|
|
.ui-table-filter-popover {
|
|
z-index: 10;
|
|
min-width: 12em;
|
|
padding: 0.5em;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
|
}
|
|
.ui-table-filter-input {
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 0.25em 0.5em;
|
|
font: inherit;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
/* `filterMode: "multiselect"`'s actual checkbox-list content is
|
|
`MultiselectFilterOptions` (`ui/multiselect-filter/`) now — its own
|
|
colocated CSS, not here. */
|
|
|
|
/* The negate toggle sits under every filter mode's own control (mara:
|
|
"you should be able to change if the filter is negated or not") —
|
|
a hairline rule separates it from the control above so "exclude"
|
|
doesn't read as one more option belonging to that control. */
|
|
.ui-table-filter-negate {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.35em;
|
|
margin-top: 0.5em;
|
|
padding-top: 0.5em;
|
|
border-top: 1px solid var(--border);
|
|
color: var(--muted);
|
|
font-size: 0.85em;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* One button clears every column's filter on this table at once (mara:
|
|
"have a small reset filters btn") — text-button styling matching
|
|
`.ui-table-sort-button`'s "no chrome, just a clickable label"
|
|
treatment, not a full `Badge`/bordered-button — this is a quiet
|
|
secondary action, not a primary one. */
|
|
.ui-table-reset-filters {
|
|
display: inline-block;
|
|
background: none;
|
|
border: none;
|
|
padding: 0;
|
|
margin: 0 0 0.5em;
|
|
font: inherit;
|
|
font-size: 0.85em;
|
|
color: var(--muted);
|
|
text-decoration: underline;
|
|
cursor: pointer;
|
|
}
|
|
.ui-table-reset-filters:hover {
|
|
color: var(--fg);
|
|
}
|