diff --git a/frontend/packages/shared/src/icons.tsx b/frontend/packages/shared/src/icons.tsx
index 01b2b3eb..6a72fabd 100644
--- a/frontend/packages/shared/src/icons.tsx
+++ b/frontend/packages/shared/src/icons.tsx
@@ -56,26 +56,3 @@ export function LinkIcon() {
);
}
-
-// A funnel, the standard "filter" glyph — smaller than the other two
-// (1em not 1.3em) since its first caller sits inside a table header
-// cell, not a nav trigger button; scale via the caller's own
-// `font-size` like the others rather than a hardcoded size prop, same
-// reasoning as `GearIcon`/`LinkIcon` above (`currentColor` stroke).
-export function FilterIcon() {
- return (
-
- );
-}
diff --git a/frontend/packages/swarm-ui/src/ui/table/Table.css b/frontend/packages/swarm-ui/src/ui/table/Table.css
index 9f434e8c..30fb1f81 100644
--- a/frontend/packages/swarm-ui/src/ui/table/Table.css
+++ b/frontend/packages/swarm-ui/src/ui/table/Table.css
@@ -63,71 +63,12 @@
opacity: 1;
}
-/* `.ui-table-th-filterable` (set on a filterable column's `
`, 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 `
`).
-
- 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 `
`'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);
+/* The per-column filter inputs' row — same cell padding as a header row,
+ no bottom border of its own (the header row above already has one). */
+.ui-table-filter-row th {
+ padding-top: 0;
+ padding-bottom: 0.5em;
+ font-weight: 400;
}
.ui-table-filter-input,
.ui-table-filter-select {
diff --git a/frontend/packages/swarm-ui/src/ui/table/Table.tsx b/frontend/packages/swarm-ui/src/ui/table/Table.tsx
index 5d988750..9a1c6887 100644
--- a/frontend/packages/swarm-ui/src/ui/table/Table.tsx
+++ b/frontend/packages/swarm-ui/src/ui/table/Table.tsx
@@ -9,10 +9,8 @@
// a `
` doesn't shrink below its content's natural width on its
// own, so without this the page itself would break, not just look
// cramped.
-import { useEffect, useMemo, useRef, useState } from "preact/hooks";
-import { createPortal } from "preact/compat";
+import { useMemo, useState } from "preact/hooks";
import type { ComponentChildren } from "preact";
-import { FilterIcon } from "@hive/shared/icons.js";
import "./Table.css";
export interface TableColumn {
@@ -53,16 +51,11 @@ export interface TableColumn {
*/
sortBy?: (row: T) => string | number;
/**
- * Column is filterable (a filter-icon button in its own header cell,
- * opening a small popover with the actual control) when present —
- * extracts the string to filter this column against. Same
- * one-signal reasoning as `sortBy`: no separate `filterable` flag,
- * the extractor's presence is the flag. How the extracted string is
- * matched against the operator's input is `filterMode`. The icon
- * fades in on header hover/focus, or stays visible outright once
- * this column has an active filter (mara: "when a filter is set,
- * the filter icon does not disappear") — see `Table.css`'s
- * `.ui-table-filter-icon` for the actual rule.
+ * Column is filterable (a control in its own row under the headers)
+ * when present — extracts the string to filter this column against.
+ * Same one-signal reasoning as `sortBy`: no separate `filterable`
+ * flag, the extractor's presence is the flag. How the extracted
+ * string is matched against the operator's input is `filterMode`.
*/
filterValue?: (row: T) => string;
/**
@@ -115,92 +108,10 @@ export function Table({
// Filter text per filterable column key, only populated for a column
// whose input the operator has actually typed into.
const [filters, setFilters] = useState>({});
- // Which column's filter popover is open, if any — at most one at a
- // time (opening a second closes the first) so the header row never
- // shows more than one panel at once.
- const [openFilterKey, setOpenFilterKey] = useState(null);
- // Viewport coordinates for the open popover, or null while closed.
- // Recomputed on open and on scroll/resize below — see the portal
- // rendering further down for why this exists at all: `.ui-table-scroll`
- // sets `overflow-x: auto`, and per the CSS overflow spec an axis left
- // unset computes to `auto` too once the *other* axis isn't `visible`,
- // so this box silently clips vertically as well. A popover positioned
- // `absolute`/`top: 100%` under its header — the first shape this
- // shipped with — gets cut off by that clip on any table whose height
- // is shorter than header-plus-popover (argus's review: reproduced,
- // not speculative, on a 1-row table). `position: fixed` computed from
- // the anchor `
`'s own `getBoundingClientRect()`, rendered via a
- // portal outside `.ui-table-scroll`'s subtree entirely, escapes that
- // clip the same way any `position: fixed` element escapes an
- // ancestor's `overflow` (unless that ancestor establishes a new
- // containing block via `transform`/`filter`/`will-change` — neither
- // `.ui-table-scroll` nor `.ui-table` do).
- const [popoverPos, setPopoverPos] = useState<{
- top: number;
- left: number;
- } | null>(null);
- // One `
` ref per filterable column key, so the scroll/resize
- // effect below can recompute the *currently open* column's position
- // without needing the click that opened it to have happened again.
- const thRefs = useRef