Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/swarm-ui/src/ui/table/Table.css
iris 7003560569 swarm-ui: extract Card/MultiselectFilter/SplitView per component-first design
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.
2026-09-11 22:30:41 +02:00

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);
}