@crispy-seed/data-table 0.10.2 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ import { checkmark } from '@seed-design/css/recipes/checkmark';
17
17
  import { Checkbox, useCheckboxContext } from '@seed-design/react-checkbox';
18
18
  import { useScaleFeedback } from '@seed-design/react-scale-feedback';
19
19
  import { checkboxGroup } from '@seed-design/css/recipes/checkbox-group';
20
+ import { createPortal } from 'react-dom';
20
21
  import { Image } from '@seed-design/react-image';
21
22
  import { avatar } from '@seed-design/css/recipes/avatar';
22
23
  import '@seed-design/css/recipes/avatar-stack';
@@ -2559,6 +2560,50 @@ const Badge = /*#__PURE__*/ forwardRef(({ className, children, ...props }, ref)=
2559
2560
  });
2560
2561
  Badge.displayName = "Badge";
2561
2562
 
2563
+ // Feature B rework (#8, chip-list display) -- the pure "how many chips fit" decision,
2564
+ // isolated from the DOM measuring that drives it (`cells.tsx`'s `ChipListCell`) the same
2565
+ // way `truncate.ts`/`filtering.ts` already split a pure calculation out of the styled
2566
+ // layer: this file touches no DOM, so the actual fitting algorithm is unit-testable with
2567
+ // plain numbers instead of a real laid-out browser.
2568
+ //
2569
+ // Attio's own chip-list cell (e.g. Categories: "Internet", "Broadcasting", "+7") shows as
2570
+ // many WHOLE chips as fit left-to-right, then one "+N" badge for the rest -- never a
2571
+ // chip clipped mid-render, never a second wrapped line. `ChipListCell` measures every
2572
+ // chip's real rendered width (font/padding-dependent, so it can't be estimated from
2573
+ // character count) plus the container's current width, then asks this function how many
2574
+ // of those chips it can show before the running total (chips + gaps + the badge itself)
2575
+ // would exceed the container.
2576
+ /**
2577
+ * `overflowBadgeWidth` is the WORST-CASE badge width the caller measured (e.g. the
2578
+ * width of a badge showing every tag: `+<chipWidths.length>`) rather than the width of
2579
+ * the badge for whatever count this call actually returns -- the real badge (fewer
2580
+ * digits once some chips DO fit) is never wider than that, so reserving the worst case
2581
+ * up front avoids a second measuring pass and always yields a result that fits. It may
2582
+ * slightly under-fill the row by the odd chip in a boundary case (e.g. "+9" vs "+10" --
2583
+ * one fewer digit than reserved), which is the deliberate trade against re-measuring per
2584
+ * candidate count.
2585
+ */ function computeVisibleChipCount(chipWidths, containerWidth, gap, overflowBadgeWidth) {
2586
+ // An unmeasured/zero-width container (not yet laid out, e.g. `display: none` or a
2587
+ // pre-hydration frame) has no meaningful "fits" answer -- show everything rather than
2588
+ // collapsing every chip behind "+N" for a frame it'll never actually paint like that.
2589
+ if (containerWidth <= 0) return chipWidths.length;
2590
+ let used = 0;
2591
+ let count = 0;
2592
+ for(let index = 0; index < chipWidths.length; index++){
2593
+ const withGap = count > 0 ? gap : 0;
2594
+ const usedIfShown = used + withGap + chipWidths[index];
2595
+ const remainingAfterThis = chipWidths.length - (index + 1);
2596
+ // Every chip except the very last one needs room left over for the overflow badge
2597
+ // TOO (if it's going to exist at all) -- otherwise this loop would happily show
2598
+ // every chip except the last, then have nowhere left to put "+1".
2599
+ const badgeReserve = remainingAfterThis > 0 ? gap + overflowBadgeWidth : 0;
2600
+ if (usedIfShown + badgeReserve > containerWidth) break;
2601
+ used = usedIfShown;
2602
+ count++;
2603
+ }
2604
+ return count;
2605
+ }
2606
+
2562
2607
  // Attio cell-type renderers, built from seed's own components -- not exported from
2563
2608
  // the package's public surface (kept alongside DataTable.tsx as an implementation
2564
2609
  // detail, the way Select keeps `InternalIcon` under `components/private`). Each
@@ -2594,29 +2639,56 @@ function toneFor(value) {
2594
2639
  }
2595
2640
  return BADGE_TONES[hash % BADGE_TONES.length];
2596
2641
  }
2597
- // Feature A/B (#9/#8) -- Attio's own flat 12-swatch chip palette, ported from
2598
- // crisp's `tokens/palette.ts` (`tagSwatch1..12`, measured live off Attio) rather
2599
- // than seed's own 6-tone `Badge` (`BADGE_TONES` above): a swatch, unlike a Badge
2600
- // tone, has no ring/border and its ink is a FLAT black on every swatch (crisp's own
2601
- // `tagSwatchInk`, `#000000` -- every swatch background is already a pale tint, so
2602
- // one dark foreground reads on all 12), not a per-tone contrast color. No seed token
2603
- // lands on any of these 12 hex values, so -- same footnote as the qvism recipe's own
2604
- // `ATTIO_BRAND_BLUE`/`RATING_FILLED` literals -- they stay literal here too.
2605
2642
  const CHIP_SWATCHES = [
2606
- "#fff3cc",
2607
- "#f4fbcb",
2608
- "#fdf7c4",
2609
- "#feecf3",
2610
- "#e0fced",
2611
- "#ddf7ff",
2612
- "#f6f7f7",
2613
- "#ffebeb",
2614
- "#feeee1",
2615
- "#e5eeff",
2616
- "#f5f0ff",
2617
- "#fdedff"
2643
+ {
2644
+ background: "#dbeafe",
2645
+ color: "#1e40af"
2646
+ },
2647
+ {
2648
+ background: "#dcfce7",
2649
+ color: "#166534"
2650
+ },
2651
+ {
2652
+ background: "#ede9fe",
2653
+ color: "#5b21b6"
2654
+ },
2655
+ {
2656
+ background: "#fef3c7",
2657
+ color: "#92400e"
2658
+ },
2659
+ {
2660
+ background: "#fee2e2",
2661
+ color: "#991b1b"
2662
+ },
2663
+ {
2664
+ background: "#ccfbf1",
2665
+ color: "#115e59"
2666
+ },
2667
+ {
2668
+ background: "#fce7f3",
2669
+ color: "#9d174d"
2670
+ },
2671
+ {
2672
+ background: "#e0e7ff",
2673
+ color: "#3730a3"
2674
+ },
2675
+ {
2676
+ background: "#cffafe",
2677
+ color: "#155e75"
2678
+ },
2679
+ {
2680
+ background: "#ecfccb",
2681
+ color: "#3f6212"
2682
+ },
2683
+ {
2684
+ background: "#ffe4e6",
2685
+ color: "#9f1239"
2686
+ },
2687
+ {
2688
+ background: "#e2e8f0",
2689
+ color: "#334155"
2690
+ }
2618
2691
  ];
2619
- const CHIP_SWATCH_INK = "#000000";
2620
2692
  /** Same stable multiply-and-add hash `toneFor` above already uses over seed's 6
2621
2693
  * Badge tones, just over this file's own 12-swatch palette instead -- same value,
2622
2694
  * same swatch, every render (crisp's own `toneFor`, `datatable.tsx`). */ function swatchIndexFor(value) {
@@ -2627,7 +2699,13 @@ const CHIP_SWATCH_INK = "#000000";
2627
2699
  return hash % CHIP_SWATCHES.length;
2628
2700
  }
2629
2701
  function swatchColorFor(value) {
2630
- return CHIP_SWATCHES[swatchIndexFor(value)];
2702
+ return CHIP_SWATCHES[swatchIndexFor(value)].background;
2703
+ }
2704
+ /** The per-hue dark text paired with `swatchColorFor`'s background, at the SAME
2705
+ * index (`swatchIndexFor`) -- replaces the old single flat `CHIP_SWATCH_INK`
2706
+ * (`#000000`) now that each background is saturated enough that one foreground no
2707
+ * longer reads well on all 12. */ function swatchInkFor(value) {
2708
+ return CHIP_SWATCHES[swatchIndexFor(value)].color;
2631
2709
  }
2632
2710
  function initialsFor(name) {
2633
2711
  const parts = name.trim().split(/\s+/);
@@ -2744,7 +2822,7 @@ function StatusCell({ value }) {
2744
2822
  });
2745
2823
  }
2746
2824
  /** One swatch chip -- `style` carries the per-value background/ink (computed by
2747
- * `swatchColorFor`/`CHIP_SWATCH_INK` above) since neither is a recipe token, the
2825
+ * `swatchColorFor`/`swatchInkFor` above) since neither is a recipe token, the
2748
2826
  * same "literal color as a computed prop" call `RatingCell`'s star fill already
2749
2827
  * makes. Shared by `ChipCell` (Feature A, single value) and `ChipListCell`/
2750
2828
  * editors.tsx's `ChipListEditor` (Feature B, multi value) so all three chip
@@ -2753,7 +2831,7 @@ function StatusCell({ value }) {
2753
2831
  className: className,
2754
2832
  style: {
2755
2833
  backgroundColor: swatchColorFor(value),
2756
- color: CHIP_SWATCH_INK
2834
+ color: swatchInkFor(value)
2757
2835
  },
2758
2836
  children: [
2759
2837
  value,
@@ -2773,19 +2851,125 @@ function StatusCell({ value }) {
2773
2851
  className: className
2774
2852
  });
2775
2853
  }
2776
- /** `type: "chip-list"`, READ-ONLY (Feature B, #8) -- multiple swatch chips, no add/
2777
- * remove affordances. The editable case is editors.tsx's `ChipListEditor`, an
2778
- * always-live widget (interacting with it IS the commit, the same `switch`/
2779
- * `rating`/`checkbox` shape `body-cells.tsx`'s `isAlwaysLiveEditable` already
2780
- * covers) -- this component only ever renders the REST display. */ function ChipListCell({ value, className, chipClassName }) {
2854
+ // Feature B rework (#8) -- the chip-list DISPLAY row's own gap, mirrored from the
2855
+ // qvism recipe's `chipList` slot (`packages/qvism-preset/src/recipes/data-table.ts`'s
2856
+ // `CHIP_GAP`, measured live off Attio at 3px between chips). Kept as a literal here
2857
+ // too, same footnote as `CHIP_SWATCHES` above: this file and the recipe are two
2858
+ // independent sources for numbers that have to agree, not one importing the other
2859
+ // (react/AGENTS.md -- style stays in the recipe; this is the one JS-side computation,
2860
+ // `computeVisibleChipCount`, that needs the SAME number to size against).
2861
+ const CHIP_LIST_GAP_PX = 3;
2862
+ /** `type: "chip-list"` display -- every editable/non-editable chip-list cell renders
2863
+ * through this component for its REST state (editable: when its click-to-edit
2864
+ * popover, editors.tsx's `ChipListEditor`, isn't open; read-only: always). Attio's
2865
+ * own model (its Categories cell: "Internet", "Broadcasting", "+7") -- show as many
2866
+ * WHOLE chips as fit the cell's current width, then one "+N" badge for the rest,
2867
+ * never a chip clipped mid-render, never a second wrapped line, never overflowing
2868
+ * into a neighboring (possibly sticky) column. `body-cells.tsx`'s `isAlwaysLiveEditable`
2869
+ * no longer lists `chip-list` -- the always-live inline editor this replaced is what
2870
+ * produced exactly that overflow (crispy-seed's #1094).
2871
+ *
2872
+ * The fitting decision itself (`computeVisibleChipCount`, `chip-overflow.ts`) is pure
2873
+ * and unit-tested on its own; this component's only job is feeding it real measured
2874
+ * widths. Two widths need measuring that can't be estimated from character count
2875
+ * (font/padding-dependent): every chip's own rendered width, and the "+N" badge's
2876
+ * worst-case width (reserved for the FULL tag count, so the real badge -- fewer
2877
+ * digits once some chips fit -- never overflows what was reserved for it; see
2878
+ * `computeVisibleChipCount`'s own doc comment). Both are measured off a permanently
2879
+ * mounted, invisible copy (`visibility: hidden`, absolutely positioned so it never
2880
+ * affects layout -- `position: relative` on the `chipList` slot's own recipe rule
2881
+ * makes THIS element, not the `<td>` two levels up, its containing block, and the
2882
+ * `chipList` slot's own `overflow: hidden` clips it there too) -- kept mounted at ALL
2883
+ * times, not just while a `ResizeObserver` fires, because widening the column back
2884
+ * out needs to know the width of chips currently hidden behind "+N" to decide
2885
+ * whether they now fit again.
2886
+ */ function ChipListCell({ value, className, chipClassName, overflowClassName }) {
2781
2887
  const tags = Array.isArray(value) ? value.filter((tag)=>!!tag) : [];
2888
+ const containerRef = React.useRef(null);
2889
+ const measureRef = React.useRef(null);
2890
+ const overflowMeasureRef = React.useRef(null);
2891
+ const [visibleCount, setVisibleCount] = React.useState(tags.length);
2892
+ // Every tag joined into one string -- a stable primitive dependency for the effect
2893
+ // below, so it only re-measures when the actual tag SET changes, not on every
2894
+ // unrelated re-render of the row (a new `tags` array identity from a fresh
2895
+ // `getColumnValue` call on every render would otherwise re-run this every time).
2896
+ const tagsKey = tags.join("\u0000");
2897
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `tagsKey` is a deliberate serialized proxy for `tags` (a fresh array identity every render would otherwise re-run this effect on every unrelated re-render) -- `tags` itself is intentionally excluded, and `tags.length` (referenced below, in the early-return branch) is already covered by `tagsKey` changing whenever the tag SET does, including a same-length rename/reorder that `tags.length` alone would miss.
2898
+ React.useLayoutEffect(()=>{
2899
+ const container = containerRef.current;
2900
+ const measure = measureRef.current;
2901
+ // No real layout to measure against (SSR, or a test environment with no layout
2902
+ // engine, e.g. this package's own `bun test` -- happy-dom lays out nothing, every
2903
+ // rect comes back zero) -- show every chip rather than a permanently-wrong "+N"
2904
+ // for a width this environment can never actually report.
2905
+ if (!container || !measure || typeof ResizeObserver === "undefined") {
2906
+ setVisibleCount(tags.length);
2907
+ return;
2908
+ }
2909
+ // Rebound to non-null consts (rather than a `!` assertion inside `recompute`
2910
+ // below) -- `container`/`measure` are already narrowed non-null here, and these
2911
+ // two bindings carry that narrowing into the closure without re-deriving it.
2912
+ const measuredContainer = container;
2913
+ const measuredList = measure;
2914
+ function recompute() {
2915
+ const chipWidths = Array.from(measuredList.children, (el)=>el.getBoundingClientRect().width);
2916
+ const overflowWidth = overflowMeasureRef.current?.getBoundingClientRect().width ?? 0;
2917
+ setVisibleCount(computeVisibleChipCount(chipWidths, measuredContainer.clientWidth, CHIP_LIST_GAP_PX, overflowWidth));
2918
+ }
2919
+ recompute();
2920
+ const observer = new ResizeObserver(recompute);
2921
+ observer.observe(container);
2922
+ return ()=>observer.disconnect();
2923
+ }, [
2924
+ tagsKey
2925
+ ]);
2782
2926
  if (!tags.length) return null;
2783
- return /*#__PURE__*/ jsx("span", {
2927
+ const clampedVisible = Math.min(visibleCount, tags.length);
2928
+ const overflowCount = tags.length - clampedVisible;
2929
+ return /*#__PURE__*/ jsxs("span", {
2784
2930
  className: className,
2785
- children: tags.map((tag)=>/*#__PURE__*/ jsx(SwatchChip, {
2786
- value: tag,
2787
- className: chipClassName
2788
- }, tag))
2931
+ ref: containerRef,
2932
+ children: [
2933
+ tags.slice(0, clampedVisible).map((tag)=>/*#__PURE__*/ jsx(SwatchChip, {
2934
+ value: tag,
2935
+ className: chipClassName
2936
+ }, tag)),
2937
+ overflowCount > 0 && /*#__PURE__*/ jsxs("span", {
2938
+ className: overflowClassName,
2939
+ children: [
2940
+ "+",
2941
+ overflowCount
2942
+ ]
2943
+ }),
2944
+ /*#__PURE__*/ jsx("span", {
2945
+ ref: measureRef,
2946
+ "aria-hidden": "true",
2947
+ className: className,
2948
+ style: {
2949
+ position: "absolute",
2950
+ visibility: "hidden",
2951
+ pointerEvents: "none"
2952
+ },
2953
+ children: tags.map((tag)=>/*#__PURE__*/ jsx(SwatchChip, {
2954
+ value: tag,
2955
+ className: chipClassName
2956
+ }, tag))
2957
+ }),
2958
+ /*#__PURE__*/ jsxs("span", {
2959
+ ref: overflowMeasureRef,
2960
+ "aria-hidden": "true",
2961
+ className: overflowClassName,
2962
+ style: {
2963
+ position: "absolute",
2964
+ visibility: "hidden",
2965
+ pointerEvents: "none"
2966
+ },
2967
+ children: [
2968
+ "+",
2969
+ tags.length
2970
+ ]
2971
+ })
2972
+ ]
2789
2973
  });
2790
2974
  }
2791
2975
  /**
@@ -2951,8 +3135,28 @@ function RatingCell({ value, maxRating = 5 }) {
2951
3135
  let cursor = selectable ? 40 : 0;
2952
3136
  let pinnedCount = 0;
2953
3137
  const entries = ordered.map((key)=>byKey.get(key)).filter((column)=>Boolean(column)).map((column)=>{
2954
- const width = getColumnWidth(column.key) ?? column.width;
3138
+ const rawWidth = getColumnWidth(column.key) ?? column.width;
2955
3139
  const pinned = pinnedSet.has(column.key);
3140
+ // Fix (0.10.4, screenshot-confirmed pinned-column overlay): a pinned column
3141
+ // with no explicit/resized width used to keep `width` as `undefined` here,
3142
+ // which `DataTableColGroup` (DataTable.tsx) renders as an AUTO `<col>` --
3143
+ // under `table-layout: fixed`, the browser then sizes it from whatever
3144
+ // remaining space is left (anywhere from its full share down to 0, e.g. once
3145
+ // the OTHER explicit-width columns already exceed the container). Meanwhile
3146
+ // `cursor` below used `DEFAULT_PINNED_COLUMN_WIDTH` (180) as its OWN guess for
3147
+ // that same column's width, to compute every LATER pinned column's
3148
+ // `insetInlineStart`. Those two numbers -- the browser's real rendered width
3149
+ // and this file's 180px guess -- had no reason to agree, and whenever they
3150
+ // didn't, every pinned/unpinned column after the mismatched one landed at the
3151
+ // wrong screen position: the next pinned column's sticky box (positioned at
3152
+ // the WRONG, JS-assumed offset) then visually overlapped whatever unpinned
3153
+ // content actually continues from the browser's REAL (different) offset.
3154
+ // Forcing the SAME fallback into the `<col>` itself (not just the cursor
3155
+ // math) is what makes the two agree by construction -- a pinned column
3156
+ // without its own width now always renders at exactly
3157
+ // `DEFAULT_PINNED_COLUMN_WIDTH`, matching what every other pinned column's
3158
+ // offset already assumed it would be.
3159
+ const width = pinned && rawWidth === undefined ? DEFAULT_PINNED_COLUMN_WIDTH : rawWidth;
2956
3160
  const pinOffset = pinned ? cursor : 0;
2957
3161
  if (pinned) {
2958
3162
  cursor += width ?? DEFAULT_PINNED_COLUMN_WIDTH;
@@ -3357,7 +3561,7 @@ const TextFieldTextarea = /*#__PURE__*/ forwardRef(({ className, autoresize = tr
3357
3561
  });
3358
3562
  TextFieldTextarea.displayName = "TextFieldTextarea";
3359
3563
 
3360
- __insertCSS(".seed-data-table__root{box-sizing:border-box;display:flex;flex-direction:column;width:100%;border-width:1px;border-style:solid;font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}.seed-data-table__table{width:100%;border-collapse:separate;border-spacing:0;table-layout:fixed}.seed-data-table__headerCell{box-sizing:border-box;position:sticky;top:0;z-index:2;vertical-align:middle;text-align:start;padding:0;height:var(--seed-dimension-x10);background-color:var(--seed-color-bg-layer-default);border-top:1px solid var(--seed-color-stroke-neutral-muted);border-bottom:1px solid var(--seed-color-stroke-neutral-muted);border-inline-end:1px solid var(--seed-color-stroke-neutral-muted);border-inline-start:none;color:var(--seed-color-fg-neutral);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);font-weight:var(--seed-font-weight-medium);letter-spacing:-.14px}.seed-data-table__headerCell:last-child{border-inline-end:none}.seed-data-table__headerCell[data-sortable]{cursor:pointer;user-select:none}.seed-data-table__headerCell [data-chevron]{opacity:0;transition:opacity .12s}.seed-data-table__headerCell[data-sortable]:hover [data-chevron]{opacity:.6}.seed-data-table__headerCell[data-sorted] [data-chevron]{opacity:1;color:var(--seed-color-fg-neutral)}.seed-data-table__headerCell:hover [data-column-menu]{opacity:1}.seed-data-table__headerCell [data-column-menu]:focus-visible,.seed-data-table__headerCell [data-column-menu][data-open]{opacity:1}.seed-data-table__headerCell[data-dragging]{opacity:.5}.seed-data-table__headerCell[data-sticky]{position:sticky;z-index:3;background-color:var(--seed-color-bg-layer-default)}.seed-data-table__headerCell{outline:var(--seed-dimension-x0_5) solid transparent;outline-offset:calc(var(--seed-dimension-x0_5) * -1)}.seed-data-table__headerCell:is(:focus-visible,[data-focus-visible]){outline:var(--seed-dimension-x0_5) solid var(--seed-color-stroke-focus-ring);outline-offset:calc(var(--seed-dimension-x0_5) * -1)}.seed-data-table__headerCell .seed-data-table-resize-handle{position:absolute;top:0;inset-inline-end:calc(7px / -2);width:7px;height:100%;z-index:4;display:flex;align-items:center;justify-content:center;cursor:col-resize;touch-action:none}.seed-data-table__headerCell .seed-data-table-resize-handle-inner{width:3px;height:100%;border-radius:3px;background-color:#0298d9;opacity:0;transition:opacity .1s}.seed-data-table__headerCell .seed-data-table-resize-handle:hover .seed-data-table-resize-handle-inner,.seed-data-table__headerCell .seed-data-table-resize-handle[data-resizing] .seed-data-table-resize-handle-inner{opacity:1}.seed-data-table__headerCellInner{box-sizing:border-box;display:flex;align-items:center;width:100%;height:100%;min-width:0;gap:var(--seed-dimension-x1_5);padding-inline:var(--seed-dimension-x3)}.seed-data-table__headerCellLabel{min-width:0;flex:1 1 0%;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__headerCellIcon{flex-shrink:0;margin-inline-start:auto;display:inline-flex;align-items:center;justify-content:center;width:var(--seed-dimension-x4);height:var(--seed-dimension-x4);color:var(--seed-color-fg-neutral-subtle);border:none;background:0 0;padding:0;font:inherit;cursor:pointer}.seed-data-table__headerCellIcon[data-active]{color:var(--seed-color-fg-neutral)}.seed-data-table__headerCellIcon[data-column-menu]{opacity:0;transition:opacity .12s}.seed-data-table__row{position:relative;background-color:var(--seed-color-bg-layer-default);--seed-data-table-row-bg:var(--seed-color-bg-layer-default)}.seed-data-table__row[data-clickable]{cursor:pointer}.seed-data-table__row:is([aria-selected=true],[data-selected]){background-color:color-mix(in srgb,#0298d9 10%,var(--seed-color-bg-layer-default));--seed-data-table-row-bg:color-mix(in srgb, #0298d9 10%, var(--seed-color-bg-layer-default))}.seed-data-table__row:is([aria-selected=true],[data-selected]):is(:hover,[data-hover]){background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 3%,color-mix(in srgb,#0298d9 10%,var(--seed-color-bg-layer-default)));--seed-data-table-row-bg:color-mix(in srgb, var(--seed-color-fg-neutral) 3%, color-mix(in srgb, #0298d9 10%, var(--seed-color-bg-layer-default)))}.seed-data-table__cell{box-sizing:border-box;position:relative;vertical-align:middle;text-align:start;height:var(--seed-dimension-x9);border-bottom:1px solid var(--seed-color-stroke-neutral-muted);border-inline-end:1px solid var(--seed-color-stroke-neutral-muted);border-top:none;border-inline-start:none;padding-block:0;padding-inline:var(--seed-dimension-x3);color:var(--seed-color-fg-neutral);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);font-weight:var(--seed-font-weight-regular);letter-spacing:-.14px;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__cell:last-child{border-inline-end:none}tbody tr:last-child .seed-data-table__cell{border-bottom:none}.seed-data-table__cell[data-numeric]{text-align:end;font-variant-numeric:tabular-nums}.seed-data-table__cell[data-sticky]{position:sticky;z-index:1;background-color:var(--seed-data-table-row-bg)}.seed-data-table__cell[data-editable]{cursor:default}.seed-data-table__cell .seed-data-table-edit-picker{display:flex;align-items:center;gap:var(--seed-dimension-x1_5);min-width:0}.seed-data-table__cell .seed-data-table-edit-pending{flex-shrink:0;color:var(--seed-color-fg-neutral-subtle);animation:rotate .6s linear infinite}.seed-data-table__cell .seed-data-table-edit-error{position:absolute;top:100%;inset-inline-start:var(--seed-dimension-x3);inset-inline-end:var(--seed-dimension-x3);z-index:2;padding-top:2px;font-size:12px;line-height:1.3;color:var(--seed-color-fg-critical);overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__entity{display:flex;align-items:center;min-width:0;gap:var(--seed-dimension-x1_5)}.seed-data-table__entityName{min-width:0;font-weight:var(--seed-font-weight-medium);overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__tagList{display:flex;align-items:center;gap:var(--seed-dimension-x1_5);overflow:hidden;min-width:0}.seed-data-table__entityActions{display:flex;align-items:center;gap:6px;margin-inline-start:auto;flex-shrink:0;opacity:0;transition:opacity .15s}tr:hover .seed-data-table__entityActions{opacity:1}.seed-data-table__entityActions:focus-within{opacity:1}.seed-data-table__entityAction{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin:0;padding:2px;border:none;border-radius:6px;background:0 0;font-family:inherit;color:color-mix(in srgb,var(--seed-color-fg-neutral) 55%,transparent);cursor:pointer;flex-shrink:0;transition:background-color .2s,color .2s,box-shadow .2s}.seed-data-table__entityAction:hover{background:color-mix(in srgb,var(--seed-color-fg-neutral) 4%,transparent);color:var(--seed-color-fg-neutral)}.seed-data-table__link{color:var(--seed-color-fg-neutral);text-decoration:none;min-width:0;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__link:is(:hover,[data-hover]){text-decoration:underline}.seed-data-table__chip{box-sizing:border-box;display:inline-flex;align-items:center;flex-shrink:0;max-width:100%;height:22px;padding:0 4px;border-radius:7px;font-size:10px;font-weight:var(--seed-font-weight-regular);line-height:22px;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__chipList{display:flex;align-items:center;flex-wrap:nowrap;gap:var(--seed-dimension-x1_5);overflow:hidden;min-width:0;width:100%}.seed-data-table__chipRemove{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:12px;height:12px;margin:0;margin-inline-start:2px;padding:0;border:none;border-radius:999px;background:0 0;color:inherit;opacity:.6;cursor:pointer;transition:opacity .12s,background-color .12s}.seed-data-table__chipRemove:hover{opacity:1;background-color:rgba(0,0,0,.08)}.seed-data-table__chipInput{flex:1 1 auto;min-width:40px;height:22px;margin:0;padding:0;border:none;outline:0;background:0 0;font-family:inherit;font-size:var(--seed-font-size-t4);color:var(--seed-color-fg-neutral);letter-spacing:-.14px}.seed-data-table__expandOverlay{box-sizing:border-box;position:absolute;top:-1px;inset-inline-start:-1px;inset-inline-end:-1px;z-index:10;min-height:calc(100% + 2px);display:flex;align-items:flex-start;padding-inline:var(--seed-dimension-x3);padding-block:var(--seed-dimension-x3);background-color:var(--seed-color-bg-layer-floating);border-radius:var(--seed-radius-r5);box-shadow:inset 0 0 0 1px var(--seed-color-stroke-neutral-weak),var(--seed-shadow-s3);color:var(--seed-color-fg-neutral);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);letter-spacing:-.14px;cursor:default}.seed-data-table__expandOverlayText{width:100%;min-width:0;white-space:normal;word-break:break-word}.seed-data-table__cellSuffixRow{display:flex;align-items:center;gap:4px;min-width:0;width:100%}.seed-data-table__cellSuffixText{flex:1 1 0%;min-width:0;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__cellSuffixIcon{flex-shrink:0;display:inline-flex;align-items:center;color:color-mix(in srgb,var(--seed-color-fg-neutral) 55%,transparent)}.seed-data-table__toolbar{box-sizing:border-box;display:flex;align-items:center;width:100%;flex-shrink:0;min-height:49px;gap:var(--seed-dimension-x2);padding-inline:var(--seed-dimension-x3);border-top:1px solid var(--seed-color-stroke-neutral-muted)}.seed-data-table__search{position:relative;display:inline-flex;align-items:center;flex:1 1 0%;max-width:280px}.seed-data-table__search svg{position:absolute;inset-inline-start:var(--seed-dimension-x2);width:var(--seed-dimension-x4);height:var(--seed-dimension-x4);color:var(--seed-color-fg-neutral-muted);pointer-events:none}.seed-data-table__search input{width:100%;height:var(--seed-dimension-x8);padding-inline-start:var(--seed-dimension-x7);padding-inline-end:var(--seed-dimension-x2);border:none;border-radius:var(--seed-radius-r2);background-color:var(--seed-color-bg-layer-default);box-shadow:inset 0 0 0 1px var(--seed-color-stroke-neutral-weak);font-family:inherit;font-size:var(--seed-font-size-t4);color:var(--seed-color-fg-neutral);outline:0;transition:box-shadow .1s var(--seed-timing-function-easing)}.seed-data-table__search input::placeholder{color:var(--seed-color-fg-neutral-subtle)}.seed-data-table__search input:focus{box-shadow:inset 0 0 0 2px var(--seed-color-stroke-neutral-contrast)}.seed-data-table__count{flex-shrink:0;color:var(--seed-color-fg-neutral-subtle);font-size:var(--seed-font-size-t3);font-variant-numeric:tabular-nums}.seed-data-table__toolbarEnd{flex-shrink:0;margin-inline-start:auto;display:inline-flex;align-items:center;gap:var(--seed-dimension-x2)}.seed-data-table__scrollArea{position:relative;display:flex;flex-direction:column;width:100%;min-width:0;flex:1 1 auto;min-height:0;overflow-x:auto;overflow-y:auto}.seed-data-table__scrollArea[data-scrolled-start] [data-sticky-edge]{clip-path:inset(0 -38px 0 0);box-shadow:6px 0 16px 4px rgba(0,0,0,.12)}.seed-data-table__scrollArea[data-scrolled-end] [data-sticky-edge-end]{clip-path:inset(0 0 0 -38px);box-shadow:-6px 0 16px 4px rgba(0,0,0,.12)}.seed-data-table__footerCell{box-sizing:border-box;position:sticky;bottom:0;z-index:2;vertical-align:middle;padding:0;height:var(--seed-dimension-x9);background-color:var(--seed-color-bg-layer-default);border-top:1px solid var(--seed-color-stroke-neutral-muted);border-inline-end:1px solid var(--seed-color-stroke-neutral-muted);border-inline-start:none}.seed-data-table__footerCell:last-child{border-inline-end:none}.seed-data-table__footerCell .seed-data-table-column-calc-trigger{display:flex;align-items:center;gap:var(--seed-dimension-x1_5);width:100%;height:100%;min-width:0;margin:0;padding:0 var(--seed-dimension-x3);border:none;background:0 0;color:var(--seed-color-fg-neutral-subtle);font-family:inherit;font-size:var(--seed-font-size-t4);font-weight:var(--seed-font-weight-medium);white-space:nowrap;overflow:clip;cursor:pointer;transition:background-color .12s,color .12s}.seed-data-table__footerCell .seed-data-table-column-calc-trigger:hover{background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 4%,transparent);color:var(--seed-color-fg-neutral)}.seed-data-table__footerCell .seed-data-table-column-calc-trigger[data-align=right]{justify-content:flex-end}.seed-data-table__footerCell .seed-data-table-column-calc-trigger svg{flex-shrink:0;opacity:.7}.seed-data-table__footerCell .seed-data-table-column-calc-placeholder{overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__footerCell .seed-data-table-column-calc-value{min-width:0;flex-shrink:1;color:var(--seed-color-fg-neutral);font-variant-numeric:tabular-nums;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__footerCell .seed-data-table-column-calc-op{min-width:0;flex-shrink:1;color:color-mix(in srgb,var(--seed-color-fg-neutral) 63%,transparent);overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__emptyState{box-sizing:border-box;text-align:center;padding-block:var(--seed-dimension-x8);padding-inline:var(--seed-dimension-x3);color:var(--seed-color-fg-neutral-subtle);font-size:var(--seed-font-size-t4)}.seed-data-table__loadingState{box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:10px;padding-block:var(--seed-dimension-x8);padding-inline:var(--seed-dimension-x3);color:color-mix(in srgb,var(--seed-color-fg-neutral) 63%,transparent);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);font-weight:var(--seed-font-weight-medium)}.seed-data-table__loadingState[data-inline]{padding-block:0;padding-inline:0;height:var(--seed-dimension-x9)}.seed-data-table__loadingState .seed-data-table-loading-spinner{flex-shrink:0;color:color-mix(in srgb,var(--seed-color-fg-neutral) 63%,transparent);animation:rotate .6s linear infinite}.seed-data-table__addRowCell{box-sizing:border-box;padding:0;border-top:none}.seed-data-table__addRow{box-sizing:border-box;display:flex;align-items:center;width:100%;margin:0;border:none;border-top:1px solid var(--seed-color-stroke-neutral-muted);background-color:var(--seed-color-bg-layer-default);height:var(--seed-dimension-x9);gap:var(--seed-dimension-x2);padding-inline:var(--seed-dimension-x2);color:var(--seed-color-fg-neutral-subtle);font-family:inherit;font-size:var(--seed-font-size-t4);cursor:pointer;transition:background-color .12s,color .12s}.seed-data-table__addRow:hover{background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 4%,var(--seed-color-bg-layer-default));color:var(--seed-color-fg-neutral)}.seed-data-table__addRow svg{flex-shrink:0}.seed-data-table__selectionCell{text-align:center;padding:0}.seed-data-table__root--surface_card{border-color:var(--seed-color-stroke-neutral-muted);background-color:var(--seed-color-bg-layer-default);border-radius:var(--seed-radius-r3)}.seed-data-table__root--surface_bleed{border-color:var(--seed-color-bg-transparent);background-color:var(--seed-color-bg-transparent)}.seed-data-table__row--interactive_true{transition:background-color .12s}.seed-data-table__row--interactive_true:is(:hover,[data-hover]){background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 3%,transparent);--seed-data-table-row-bg:color-mix(in srgb, var(--seed-color-fg-neutral) 3%, var(--seed-color-bg-layer-default))}");
3564
+ __insertCSS(".seed-data-table__root{box-sizing:border-box;display:flex;flex-direction:column;width:100%;border-width:1px;border-style:solid;font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}.seed-data-table__table{width:100%;border-collapse:separate;border-spacing:0;table-layout:fixed}.seed-data-table__headerCell{box-sizing:border-box;position:sticky;top:0;z-index:2;vertical-align:middle;text-align:start;padding:0;height:var(--seed-dimension-x10);background-color:var(--seed-color-bg-layer-default);border-top:1px solid var(--seed-color-stroke-neutral-muted);border-bottom:1px solid var(--seed-color-stroke-neutral-muted);border-inline-end:1px solid var(--seed-color-stroke-neutral-muted);border-inline-start:none;color:var(--seed-color-fg-neutral);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);font-weight:var(--seed-font-weight-medium);letter-spacing:-.14px}.seed-data-table__headerCell:last-child{border-inline-end:none}.seed-data-table__headerCell[data-sortable]{cursor:pointer;user-select:none}.seed-data-table__headerCell [data-chevron]{opacity:0;transition:opacity .12s}.seed-data-table__headerCell[data-sortable]:hover [data-chevron]{opacity:.6}.seed-data-table__headerCell[data-sorted] [data-chevron]{opacity:1;color:var(--seed-color-fg-neutral)}.seed-data-table__headerCell:hover [data-column-menu]{opacity:1}.seed-data-table__headerCell [data-column-menu]:focus-visible,.seed-data-table__headerCell [data-column-menu][data-open]{opacity:1}.seed-data-table__headerCell[data-dragging]{opacity:.5}.seed-data-table__headerCell[data-sticky]{position:sticky;z-index:3;background-color:var(--seed-color-bg-layer-default)}.seed-data-table__headerCell{outline:var(--seed-dimension-x0_5) solid transparent;outline-offset:calc(var(--seed-dimension-x0_5) * -1)}.seed-data-table__headerCell:is(:focus-visible,[data-focus-visible]){outline:var(--seed-dimension-x0_5) solid var(--seed-color-stroke-focus-ring);outline-offset:calc(var(--seed-dimension-x0_5) * -1)}.seed-data-table__headerCell .seed-data-table-resize-handle{position:absolute;top:0;inset-inline-end:calc(7px / -2);width:7px;height:100%;z-index:4;display:flex;align-items:center;justify-content:center;cursor:col-resize;touch-action:none}.seed-data-table__headerCell .seed-data-table-resize-handle-inner{width:3px;height:100%;border-radius:3px;background-color:#0298d9;opacity:0;transition:opacity .1s}.seed-data-table__headerCell .seed-data-table-resize-handle:hover .seed-data-table-resize-handle-inner,.seed-data-table__headerCell .seed-data-table-resize-handle[data-resizing] .seed-data-table-resize-handle-inner{opacity:1}.seed-data-table__headerCellInner{box-sizing:border-box;display:flex;align-items:center;width:100%;height:100%;min-width:0;gap:var(--seed-dimension-x1_5);padding-inline:var(--seed-dimension-x3)}.seed-data-table__headerCellLabel{min-width:0;flex:1 1 0%;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__headerCellIcon{flex-shrink:0;margin-inline-start:auto;display:inline-flex;align-items:center;justify-content:center;width:var(--seed-dimension-x4);height:var(--seed-dimension-x4);color:var(--seed-color-fg-neutral-subtle);border:none;background:0 0;padding:0;font:inherit;cursor:pointer}.seed-data-table__headerCellIcon[data-active]{color:var(--seed-color-fg-neutral)}.seed-data-table__headerCellIcon[data-column-menu]{opacity:0;transition:opacity .12s}.seed-data-table__row{position:relative;background-color:var(--seed-color-bg-layer-default);--seed-data-table-row-bg:var(--seed-color-bg-layer-default)}.seed-data-table__row[data-clickable]{cursor:pointer}.seed-data-table__row:is([aria-selected=true],[data-selected]){background-color:color-mix(in srgb,#0298d9 10%,var(--seed-color-bg-layer-default));--seed-data-table-row-bg:color-mix(in srgb, #0298d9 10%, var(--seed-color-bg-layer-default))}.seed-data-table__row:is([aria-selected=true],[data-selected]):is(:hover,[data-hover]){background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 3%,color-mix(in srgb,#0298d9 10%,var(--seed-color-bg-layer-default)));--seed-data-table-row-bg:color-mix(in srgb, var(--seed-color-fg-neutral) 3%, color-mix(in srgb, #0298d9 10%, var(--seed-color-bg-layer-default)))}.seed-data-table__cell{box-sizing:border-box;position:relative;vertical-align:middle;text-align:start;height:var(--seed-dimension-x9);border-bottom:1px solid var(--seed-color-stroke-neutral-muted);border-inline-end:1px solid var(--seed-color-stroke-neutral-muted);border-top:none;border-inline-start:none;padding-block:0;padding-inline:var(--seed-dimension-x3);color:var(--seed-color-fg-neutral);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);font-weight:var(--seed-font-weight-regular);letter-spacing:-.14px;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__cell:not([data-editable]){user-select:none}.seed-data-table__cell:last-child{border-inline-end:none}tbody tr:last-child .seed-data-table__cell{border-bottom:none}.seed-data-table__cell[data-numeric]{text-align:end;font-variant-numeric:tabular-nums}.seed-data-table__cell[data-sticky]{position:sticky;z-index:1;background-color:var(--seed-data-table-row-bg)}.seed-data-table__cell[data-overlay-open]{overflow:visible;z-index:20}.seed-data-table__cell[data-editable]{cursor:default}.seed-data-table__cell .seed-data-table-edit-picker{display:flex;align-items:center;gap:var(--seed-dimension-x1_5);min-width:0}.seed-data-table__cell .seed-data-table-edit-pending{flex-shrink:0;color:var(--seed-color-fg-neutral-subtle);animation:rotate .6s linear infinite}.seed-data-table__cell .seed-data-table-edit-error{position:absolute;top:100%;inset-inline-start:var(--seed-dimension-x3);inset-inline-end:var(--seed-dimension-x3);z-index:2;padding-top:2px;font-size:12px;line-height:1.3;color:var(--seed-color-fg-critical);overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__entity{display:flex;align-items:center;min-width:0;gap:var(--seed-dimension-x1_5)}.seed-data-table__entityName{min-width:0;font-weight:var(--seed-font-weight-medium);overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__tagList{display:flex;align-items:center;gap:var(--seed-dimension-x1_5);overflow:hidden;min-width:0}.seed-data-table__entityActions{display:flex;align-items:center;gap:6px;margin-inline-start:auto;flex-shrink:0;opacity:0;transition:opacity .15s}tr:hover .seed-data-table__entityActions{opacity:1}.seed-data-table__entityActions:focus-within{opacity:1}.seed-data-table__entityAction{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin:0;padding:2px;border:none;border-radius:6px;background:0 0;font-family:inherit;color:color-mix(in srgb,var(--seed-color-fg-neutral) 55%,transparent);cursor:pointer;flex-shrink:0;transition:background-color .2s,color .2s,box-shadow .2s}.seed-data-table__entityAction:hover{background:color-mix(in srgb,var(--seed-color-fg-neutral) 4%,transparent);color:var(--seed-color-fg-neutral)}.seed-data-table__link{color:var(--seed-color-fg-neutral);text-decoration:none;min-width:0;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__link:is(:hover,[data-hover]){text-decoration:underline}.seed-data-table__chip{box-sizing:border-box;display:inline-flex;align-items:center;flex-shrink:0;max-width:100%;height:22px;padding:0 4px;border-radius:7px;font-size:10px;font-weight:var(--seed-font-weight-regular);line-height:15px;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__chipList{position:relative;display:flex;align-items:center;flex-wrap:nowrap;gap:3px;overflow:hidden;min-width:0;width:100%}.seed-data-table__chipOverflow{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;height:20px;padding:0 4px;border-radius:6px;background-color:#f6f7f7;color:#000;font-size:10px;font-weight:var(--seed-font-weight-regular);line-height:15px;white-space:nowrap}.seed-data-table__chipListEditor{display:flex;align-items:center;flex-wrap:wrap;gap:3px;min-width:0;width:100%}.seed-data-table__chipRemove{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:12px;height:12px;margin:0;margin-inline-start:2px;padding:0;border:none;border-radius:999px;background:0 0;color:inherit;opacity:.6;cursor:pointer;transition:opacity .12s,background-color .12s}.seed-data-table__chipRemove:hover{opacity:1;background-color:rgba(0,0,0,.08)}.seed-data-table__chipInput{flex:1 1 auto;min-width:40px;height:22px;margin:0;padding:0;border:none;outline:0;background:0 0;font-family:inherit;font-size:var(--seed-font-size-t4);color:var(--seed-color-fg-neutral);letter-spacing:-.14px}.seed-data-table__expandOverlay{box-sizing:border-box;position:absolute;top:-1px;inset-inline-start:-1px;inset-inline-end:-1px;z-index:10;min-height:calc(100% + 2px);display:flex;align-items:flex-start;padding-inline:var(--seed-dimension-x3);padding-block:var(--seed-dimension-x3);background-color:var(--seed-color-bg-layer-floating);border-radius:4px;box-shadow:none;border:1px solid #266df0;color:var(--seed-color-fg-neutral);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);letter-spacing:-.14px;cursor:default}.seed-data-table__expandOverlayText{width:100%;min-width:0;white-space:normal;word-break:break-word}.seed-data-table__cellSuffixRow{display:flex;align-items:center;gap:4px;min-width:0;width:100%}.seed-data-table__cellSuffixText{flex:1 1 0%;min-width:0;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__cellSuffixIcon{flex-shrink:0;display:inline-flex;align-items:center;color:color-mix(in srgb,var(--seed-color-fg-neutral) 55%,transparent)}.seed-data-table__toolbar{box-sizing:border-box;display:flex;align-items:center;width:100%;flex-shrink:0;min-height:49px;gap:var(--seed-dimension-x2);padding-inline:var(--seed-dimension-x3);border-top:1px solid var(--seed-color-stroke-neutral-muted)}.seed-data-table__search{position:relative;display:inline-flex;align-items:center;flex:1 1 0%;max-width:280px}.seed-data-table__search svg{position:absolute;inset-inline-start:var(--seed-dimension-x2);width:var(--seed-dimension-x4);height:var(--seed-dimension-x4);color:var(--seed-color-fg-neutral-muted);pointer-events:none}.seed-data-table__search input{width:100%;height:var(--seed-dimension-x8);padding-inline-start:var(--seed-dimension-x7);padding-inline-end:var(--seed-dimension-x2);border:none;border-radius:var(--seed-radius-r2);background-color:var(--seed-color-bg-layer-default);box-shadow:inset 0 0 0 1px var(--seed-color-stroke-neutral-weak);font-family:inherit;font-size:var(--seed-font-size-t4);color:var(--seed-color-fg-neutral);outline:0;transition:box-shadow .1s var(--seed-timing-function-easing)}.seed-data-table__search input::placeholder{color:var(--seed-color-fg-neutral-subtle)}.seed-data-table__search input:focus{box-shadow:inset 0 0 0 2px var(--seed-color-stroke-neutral-contrast)}.seed-data-table__count{flex-shrink:0;color:var(--seed-color-fg-neutral-subtle);font-size:var(--seed-font-size-t3);font-variant-numeric:tabular-nums}.seed-data-table__toolbarEnd{flex-shrink:0;margin-inline-start:auto;display:inline-flex;align-items:center;gap:var(--seed-dimension-x2)}.seed-data-table__scrollArea{position:relative;display:flex;flex-direction:column;width:100%;min-width:0;flex:1 1 auto;min-height:0;overflow-x:auto;overflow-y:auto}.seed-data-table__scrollArea[data-scrolled-start] [data-sticky-edge]{clip-path:inset(0 -38px 0 0);box-shadow:6px 0 16px 4px rgba(0,0,0,.12)}.seed-data-table__scrollArea[data-scrolled-end] [data-sticky-edge-end]{clip-path:inset(0 0 0 -38px);box-shadow:-6px 0 16px 4px rgba(0,0,0,.12)}.seed-data-table__footerCell{box-sizing:border-box;position:sticky;bottom:0;z-index:2;vertical-align:middle;padding:0;height:var(--seed-dimension-x9);background-color:var(--seed-color-bg-layer-default);border-top:1px solid var(--seed-color-stroke-neutral-muted);border-inline-end:1px solid var(--seed-color-stroke-neutral-muted);border-inline-start:none}.seed-data-table__footerCell:last-child{border-inline-end:none}.seed-data-table__footerCell .seed-data-table-column-calc-trigger{display:flex;align-items:center;gap:var(--seed-dimension-x1_5);width:100%;height:100%;min-width:0;margin:0;padding:0 var(--seed-dimension-x3);border:none;background:0 0;color:var(--seed-color-fg-neutral-subtle);font-family:inherit;font-size:var(--seed-font-size-t4);font-weight:var(--seed-font-weight-medium);white-space:nowrap;overflow:clip;cursor:pointer;transition:background-color .12s,color .12s}.seed-data-table__footerCell .seed-data-table-column-calc-trigger:hover{background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 4%,transparent);color:var(--seed-color-fg-neutral)}.seed-data-table__footerCell .seed-data-table-column-calc-trigger[data-align=right]{justify-content:flex-end}.seed-data-table__footerCell .seed-data-table-column-calc-trigger svg{flex-shrink:0;opacity:.7}.seed-data-table__footerCell .seed-data-table-column-calc-placeholder{overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__footerCell .seed-data-table-column-calc-value{min-width:0;flex-shrink:1;color:var(--seed-color-fg-neutral);font-variant-numeric:tabular-nums;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__footerCell .seed-data-table-column-calc-op{min-width:0;flex-shrink:1;color:color-mix(in srgb,var(--seed-color-fg-neutral) 63%,transparent);overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__emptyState{box-sizing:border-box;text-align:center;padding-block:var(--seed-dimension-x8);padding-inline:var(--seed-dimension-x3);color:var(--seed-color-fg-neutral-subtle);font-size:var(--seed-font-size-t4)}.seed-data-table__loadingState{box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:10px;padding-block:var(--seed-dimension-x8);padding-inline:var(--seed-dimension-x3);color:color-mix(in srgb,var(--seed-color-fg-neutral) 63%,transparent);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);font-weight:var(--seed-font-weight-medium)}.seed-data-table__loadingState[data-inline]{padding-block:0;padding-inline:0;height:var(--seed-dimension-x9)}.seed-data-table__loadingState .seed-data-table-loading-spinner{flex-shrink:0;color:color-mix(in srgb,var(--seed-color-fg-neutral) 63%,transparent);animation:rotate .6s linear infinite}.seed-data-table__addRowCell{box-sizing:border-box;padding:0;border-top:none}.seed-data-table__addRow{box-sizing:border-box;display:flex;align-items:center;width:100%;margin:0;border:none;border-top:1px solid var(--seed-color-stroke-neutral-muted);background-color:var(--seed-color-bg-layer-default);height:var(--seed-dimension-x9);gap:var(--seed-dimension-x2);padding-inline:var(--seed-dimension-x2);color:var(--seed-color-fg-neutral-subtle);font-family:inherit;font-size:var(--seed-font-size-t4);cursor:pointer;transition:background-color .12s,color .12s}.seed-data-table__addRow:hover{background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 4%,var(--seed-color-bg-layer-default));color:var(--seed-color-fg-neutral)}.seed-data-table__addRow svg{flex-shrink:0}.seed-data-table__selectionCell{text-align:center;padding:0}.seed-data-table__root--surface_card{border-color:var(--seed-color-stroke-neutral-muted);background-color:var(--seed-color-bg-layer-default);border-radius:var(--seed-radius-r3)}.seed-data-table__root--surface_bleed{border-color:var(--seed-color-bg-transparent);background-color:var(--seed-color-bg-transparent)}.seed-data-table__row--interactive_true{transition:background-color .12s}.seed-data-table__row--interactive_true:is(:hover,[data-hover]){background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 3%,transparent);--seed-data-table-row-bg:color-mix(in srgb, var(--seed-color-fg-neutral) 3%, var(--seed-color-bg-layer-default))}");
3361
3565
 
3362
3566
  const createClassName = (className, variants, compoundVariants = [])=>{
3363
3567
  const variantKeys = Object.keys(variants);
@@ -3460,6 +3664,14 @@ const dataTableSlotNames = [
3460
3664
  "chipList",
3461
3665
  "seed-data-table__chipList"
3462
3666
  ],
3667
+ [
3668
+ "chipOverflow",
3669
+ "seed-data-table__chipOverflow"
3670
+ ],
3671
+ [
3672
+ "chipListEditor",
3673
+ "seed-data-table__chipListEditor"
3674
+ ],
3463
3675
  [
3464
3676
  "chipRemove",
3465
3677
  "seed-data-table__chipRemove"
@@ -4063,20 +4275,31 @@ function RatingEditor({ column, value, pending, onCommit }) {
4063
4275
  });
4064
4276
  }
4065
4277
  ////////////////////////////////////////////////////////////////////////////////////
4066
- // -- editable: "chip-list" (Feature B, #8) -- always-live, the same shape
4067
- // `SwitchEditor`/`CheckboxEditor`/`RatingEditor` above already establish
4068
- // (interacting with the widget IS the commit, no double-click gate, no separate
4069
- // rest display -- `body-cells.tsx`'s `isAlwaysLiveEditable` covers it alongside
4070
- // them). A gated open/close state (like `SelectEditor`'s) would fight adding
4071
- // MULTIPLE tags in one sitting: `useDataTableEdit`'s `commitEdit` closes editing
4072
- // the instant a SYNC commit resolves, so a gated chip editor would close itself
4073
- // after the very first tag. Always-live sidesteps that entirely -- there is no
4074
- // `editingCell` to close, the widget is simply always the cell's own content.
4075
- // Every add/remove commits the FULL next array through the same one-string
4278
+ // -- editable: "chip-list" (Feature B rework, #8) -- click-to-edit, Attio's own model
4279
+ // rather than the always-live widget this replaced (crispy-seed#1094: every row
4280
+ // rendering a live add-tag editor at once overflowed the cell and collided with
4281
+ // sticky columns). `body-cells.tsx`'s `DataTableBodyCell` opens this INSIDE the
4282
+ // same anchored `CellExpandOverlay` Feature C already built for a gated editor's
4283
+ // expanded state, on a plain single click (no truncation gate, unlike every other
4284
+ // read-only cell's peek) -- clicking a short 2-tag cell must open the editor just
4285
+ // as readily as a 10-tag one. It stays open across MULTIPLE add/remove commits
4286
+ // (closes only on blur/outside-click/Escape, `DataTableBodyCell`'s own effect) by
4287
+ // NOT going through `useDataTableEdit`'s `isEditing` gate the way `SelectEditor`/
4288
+ // `TextEditor` do -- `commitEdit` closes `editingCell` the instant a SYNC commit
4289
+ // resolves (its own doc comment), which would have closed a gated chip editor
4290
+ // after the very first tag. `DataTableBodyCell` instead tracks THIS editor's own
4291
+ // open/closed state in its local `overlayOpen`, independent of `editingCell`.
4292
+ // Every add/remove still commits the FULL next array through the same one-string
4076
4293
  // contract every other editor already uses (`useDataTableEdit`'s own doc
4077
- // comment): `JSON.stringify(nextTags)`, which the consumer's `onEdit` parses
4078
- // back -- crisp/GRC's contract stays "one string in, no typed union", not a
4079
- // second commit shape invented for just this column.
4294
+ // comment): `JSON.stringify(nextTags)`, which the consumer's `onEdit` parses back
4295
+ // -- crisp/GRC's contract stays "one string in, no typed union", not a second
4296
+ // commit shape invented for just this column.
4297
+ //
4298
+ // Renders through `classNames.chipListEditor`, NOT the read-only `chipList` slot
4299
+ // (cells.tsx's `ChipListCell`) -- the display row clips to one line with a "+N"
4300
+ // badge; this editor instead WRAPS onto as many lines as it needs inside the
4301
+ // overlay's own grow-in-height box, since every tag (plus the add-input) has to
4302
+ // stay visible and editable at once.
4080
4303
  function ChipListEditor({ column, value, pending, onCommit, classNames }) {
4081
4304
  const tags = Array.isArray(value) ? value.filter((tag)=>!!tag) : [];
4082
4305
  const [draft, setDraft] = React.useState("");
@@ -4097,7 +4320,7 @@ function ChipListEditor({ column, value, pending, onCommit, classNames }) {
4097
4320
  }
4098
4321
  return(// biome-ignore lint/a11y/noStaticElementInteractions: not a widget -- this span never responds to interaction on ITSELF, only stops propagation of events that already occurred on a real interactive descendant (the remove buttons, the add-tag input). No role fits it because it isn't a control.
4099
4322
  /*#__PURE__*/ jsxs("span", {
4100
- className: classNames.chipList,
4323
+ className: classNames.chipListEditor,
4101
4324
  onClick: (event)=>event.stopPropagation(),
4102
4325
  children: [
4103
4326
  tags.map((tag)=>/*#__PURE__*/ jsx(SwatchChip, {
@@ -4115,6 +4338,8 @@ function ChipListEditor({ column, value, pending, onCommit, classNames }) {
4115
4338
  })
4116
4339
  }, tag)),
4117
4340
  /*#__PURE__*/ jsx("input", {
4341
+ // biome-ignore lint/a11y/noAutofocus: this editor only ever mounts in response to an explicit click that just opened it (never on page load), matching `SelectEditor`'s own `open` (controlled, always-open-on-mount) -- the same UX every gated editor in this file already gives via a component the linter can't see through (`TextFieldInput`/`SelectSearchField`), just on a bare `<input>` here since `chipInput` has no such wrapper.
4342
+ autoFocus: true,
4118
4343
  className: classNames.chipInput,
4119
4344
  value: draft,
4120
4345
  disabled: pending,
@@ -4122,14 +4347,19 @@ function ChipListEditor({ column, value, pending, onCommit, classNames }) {
4122
4347
  "aria-label": `Add tag to ${column.key}`,
4123
4348
  onChange: (event)=>setDraft(event.target.value),
4124
4349
  onKeyDown: (event)=>{
4350
+ // Escape is the ONE key this input must NOT stop propagation for: there is
4351
+ // no in-progress draft state worth clearing locally (every add/remove
4352
+ // already commits immediately), so it needs to bubble all the way to
4353
+ // `document` -- `DataTableBodyCell`'s own Escape listener is what actually
4354
+ // closes this popover. Every other key stops here, same as before, so it
4355
+ // never reaches the row's own click/keyboard handling.
4356
+ if (event.key === "Escape") return;
4125
4357
  event.stopPropagation();
4126
4358
  if (event.key === "Enter") {
4127
4359
  event.preventDefault();
4128
4360
  addTag();
4129
4361
  } else if (event.key === "Backspace" && draft === "" && tags.length > 0) {
4130
4362
  removeTag(tags[tags.length - 1]);
4131
- } else if (event.key === "Escape") {
4132
- setDraft("");
4133
4363
  }
4134
4364
  }
4135
4365
  }),
@@ -4234,12 +4464,41 @@ function isCellTruncated(cell) {
4234
4464
  /** `editable` + one of these shapes needs no double-click gate and no separate rest
4235
4465
  * display -- interacting with the widget itself IS the commit gesture (crisp's own
4236
4466
  * #453/#760/#761: `switch`/`rating`/`checkbox` never had a meaningfully different
4237
- * "rest" state to show). `chip-list` (Feature B, #8) joins them for a different
4238
- * reason -- see `ChipListEditor`'s own doc comment (editors.tsx): a GATED chip
4239
- * editor would close itself the instant the first tag committed. Every other
4240
- * `editable` column opens on double-click. */ function isAlwaysLiveEditable(column) {
4467
+ * "rest" state to show). `chip-list` used to join them here (Feature B, #8) but no
4468
+ * longer does -- see `isChipListEditable` below, its own separate "click opens a
4469
+ * popover, closed the rest of the time" category, and `ChipListEditor`'s own doc
4470
+ * comment (editors.tsx) for why it isn't just the ordinary GATED (double-click)
4471
+ * shape either. Every other `editable` column opens on double-click. */ function isAlwaysLiveEditable(column) {
4241
4472
  if (!column.editable) return false;
4242
- return column.editor === "switch" || column.type === "rating" || column.type === "checkbox" || column.type === "chip-list";
4473
+ return column.editor === "switch" || column.type === "rating" || column.type === "checkbox";
4474
+ }
4475
+ /** Feature B rework (#8) -- an editable `chip-list` column's own third category,
4476
+ * alongside `isAlwaysLiveEditable` (above) and the ordinary double-click GATED
4477
+ * shape: a single click (no truncation gate -- even a short, unclipped 2-tag cell
4478
+ * must open just as readily as a long one) opens `editors.tsx`'s `ChipListEditor`,
4479
+ * and it closes back to the plain `ChipListCell` display on blur/outside-click/
4480
+ * Escape (`DataTableBodyCell`'s own `overlayOpen`) -- never always-live
4481
+ * (crispy-seed#1094: that overflowed the cell and collided with sticky columns),
4482
+ * never simply GATED either (a `useDataTableEdit`-driven `isEditing` gate would
4483
+ * close itself after the first tag commits, see `ChipListEditor`'s own doc
4484
+ * comment).
4485
+ *
4486
+ * Unlike every OTHER open editor in this file, this one does NOT render inside
4487
+ * Feature C's `CellExpandOverlay` (an anchored box positioned relative to the
4488
+ * `<td>`, `position: absolute` against it) -- it renders through a REAL React
4489
+ * portal to `document.body` instead (`DataTableBodyCell`'s own `popoverStyle`).
4490
+ * Confirmed live, crispy-seed#1094's pinned-column follow-up: a `position: sticky`
4491
+ * `<td>` clips ANY descendant content that grows past its own box to the table's
4492
+ * scrollport, in EVERY browser tested, regardless of `overflow: visible` on every
4493
+ * element in between and regardless of how high its `z-index` is raised -- neither
4494
+ * of which is a stacking-context or overflow bug this file can fix from the
4495
+ * INSIDE. A portal sidesteps it entirely by not being a descendant of the sticky
4496
+ * `<td>` at all once mounted. The same sticky-clip almost certainly affects the
4497
+ * OTHER gated editors' `CellExpandOverlay` too when their own column is pinned,
4498
+ * but porting all of Feature C to this same portal model is its own change,
4499
+ * out of scope here -- this fix is scoped to the one editor crispy-seed#1094
4500
+ * actually reported against a pinned column. */ function isChipListEditable(column) {
4501
+ return !!column.editable && column.type === "chip-list";
4243
4502
  }
4244
4503
  function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyFirst, stickyLast }) {
4245
4504
  const effective = useEffectiveColumns(columns, {
@@ -4269,8 +4528,13 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4269
4528
  * `ColumnBodyCells` keys it by `column.key` the same way it always keyed the bare
4270
4529
  * `<DataTableCell>` before this split.
4271
4530
  */ function DataTableBodyCell({ column, row, classNames, locale, sticky }) {
4272
- const { startEdit, isEditing, isPeeking, openPeek, closePeek } = useDataTableContext();
4531
+ const { startEdit, cancelEdit, isEditing, isPending, commitEdit, isPeeking, openPeek, closePeek } = useDataTableContext();
4273
4532
  const cellRef = React.useRef(null);
4533
+ // Feature B rework (#8) -- the chip-list popover's own live position, in VIEWPORT
4534
+ // coordinates (`position: fixed`), since it portals to `document.body` rather than
4535
+ // anchoring inside the `<td>` (see `isChipListEditable`'s own doc comment for why).
4536
+ // `null` while closed -- nothing to portal yet.
4537
+ const [popoverStyle, setPopoverStyle] = React.useState(null);
4274
4538
  // Feature C (#12) -- whether THIS activation (click or double-click) should
4275
4539
  // render as the anchored overlay rather than plain inline. Frozen at the moment
4276
4540
  // of activation (re-measured every time, never left stale) rather than derived
@@ -4280,20 +4544,68 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4280
4544
  // rendering exactly as it always has, not suddenly gain overlay chrome.
4281
4545
  const [overlayOpen, setOverlayOpen] = React.useState(false);
4282
4546
  const alwaysLive = isAlwaysLiveEditable(column);
4283
- // A gated `editable` column (everything except the always-live widgets) opens on
4284
- // double-click -- Attio's own measured gesture (crisp's #411), matched here
4285
- // rather than a single click, which stays "select this cell" in crisp's own
4286
- // spreadsheet model and would otherwise be indistinguishable from this table's
4287
- // plain row click. Feature C ADDS a single click as a SECOND way in, but only
4288
- // when the cell's content is actually truncated (see `handleClick` below) --
4289
- // double-click keeps working unconditionally, same as before this feature.
4290
- const gated = column.editable && !alwaysLive;
4547
+ const chipListEditable = isChipListEditable(column);
4548
+ // A gated `editable` column (everything except the always-live widgets AND the
4549
+ // click-to-edit chip-list) opens on double-click -- Attio's own measured gesture
4550
+ // (crisp's #411), matched here rather than a single click, which stays "select
4551
+ // this cell" in crisp's own spreadsheet model and would otherwise be
4552
+ // indistinguishable from this table's plain row click. Feature C ADDS a single
4553
+ // click as a SECOND way in, but only when the cell's content is actually
4554
+ // truncated (see `handleClick` below) -- double-click keeps working
4555
+ // unconditionally, same as before this feature.
4556
+ const gated = column.editable && !alwaysLive && !chipListEditable;
4557
+ // This cell's chip-list popover is showing right now -- `overlayOpen` alone isn't
4558
+ // enough (it's shared with the read-only peek/gated-edit cases below), so this
4559
+ // ANDs in "is this actually a chip-list column" too. Gates the portal render
4560
+ // below (`popoverStyle` is only ever non-null while this is true).
4561
+ const chipListEditorOpen = chipListEditable && overlayOpen;
4562
+ // Feature B rework (#8) -- the chip-list popover's own position, read off the
4563
+ // `<td>`'s LIVE `getBoundingClientRect()` (viewport coordinates, matching
4564
+ // `position: fixed`) rather than anything Feature C's `pinOffset`/sticky math
4565
+ // already tracks -- those describe offsets WITHIN the scrolling table, not where
4566
+ // the cell actually sits in the viewport right now, which is what a portaled,
4567
+ // `position: fixed` box anchored below the cell needs.
4568
+ const measurePopoverPosition = React.useCallback(()=>{
4569
+ const cell = cellRef.current;
4570
+ if (!cell) return;
4571
+ const rect = cell.getBoundingClientRect();
4572
+ setPopoverStyle({
4573
+ position: "fixed",
4574
+ top: rect.bottom,
4575
+ insetInlineStart: rect.left,
4576
+ insetInlineEnd: "auto",
4577
+ minWidth: rect.width,
4578
+ // Both override the SAME `expandOverlay` class properties this box also
4579
+ // carries (`className={classNames.expandOverlay}`, below) -- that class's
4580
+ // `minHeight: calc(100% + 2px)` resolves against an ANCHORED box's own
4581
+ // containing block (the `<td>`, ~36px), which is meaningless (and would
4582
+ // resolve against the VIEWPORT instead, ~100% of the page's height) for a
4583
+ // portaled `position: fixed` box; `auto` lets the content size it instead.
4584
+ // `zIndex: 10` is Feature C's own value for an in-place overlay competing
4585
+ // only with OTHER table content -- a portal to `document.body` can land
4586
+ // beside arbitrary page chrome (this Storybook's own toolbar, a consuming
4587
+ // app's header), so it gets a value comfortably above typical page chrome.
4588
+ minHeight: "auto",
4589
+ zIndex: 1000
4590
+ });
4591
+ }, []);
4291
4592
  function handleDoubleClick(event) {
4292
4593
  if (!gated) return;
4293
4594
  setOverlayOpen(isCellTruncated(event.currentTarget));
4294
4595
  startEdit(row.id, column.key);
4295
4596
  }
4296
4597
  function handleClick(event) {
4598
+ if (chipListEditable) {
4599
+ // Attio's own chip-list gesture: click opens the editor regardless of
4600
+ // truncation -- a short, fully-visible 2-tag cell opens just as readily as
4601
+ // one already showing "+N". Must not also reach the row's own `onRowClick`,
4602
+ // same reasoning as the read-only peek/preview-button cases below.
4603
+ event.stopPropagation();
4604
+ measurePopoverPosition();
4605
+ setOverlayOpen(true);
4606
+ startEdit(row.id, column.key);
4607
+ return;
4608
+ }
4297
4609
  if (!isCellTruncated(event.currentTarget)) return;
4298
4610
  if (gated) {
4299
4611
  setOverlayOpen(true);
@@ -4301,7 +4613,7 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4301
4613
  } else if (alwaysLive) {
4302
4614
  // No separate open/edit state to start -- the always-live widget already
4303
4615
  // renders unconditionally; this only switches ITS OWN box to the anchored
4304
- // overlay so a long chip list (etc.) gets room to show every chip.
4616
+ // overlay so a long value (etc.) gets room to show in full.
4305
4617
  setOverlayOpen(true);
4306
4618
  } else {
4307
4619
  // Read-only truncated cell: this click's only job is opening the peek
@@ -4314,25 +4626,39 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4314
4626
  openPeek(row.id, column.key);
4315
4627
  }
4316
4628
  }
4629
+ // Feature B rework (#8) -- the portaled popover's own DOM node. Read by the
4630
+ // outside-click detector below (it lives outside `cellRef`'s subtree once
4631
+ // mounted, in `document.body` -- see `isChipListEditable`'s doc comment) and by
4632
+ // the reposition effect further down.
4633
+ const popoverRef = React.useRef(null);
4317
4634
  // Closes the overlay on Escape or an outside click/pointerdown -- a gated
4318
4635
  // editor's OWN Escape/blur handling already ends `isEditing` on its own (see
4319
4636
  // `TextEditor`/`SelectEditor`'s existing onCancel/onOpenChange wiring), which
4320
4637
  // alone is enough to hide a GATED cell's overlay (its render below is gated on
4321
- // `isEditing` too); this listener is what the PEEK and ALWAYS-LIVE cases need,
4322
- // since neither has any other mechanism that ever calls `closePeek`/resets
4323
- // `overlayOpen` on its own.
4638
+ // `isEditing` too); this listener is what the PEEK, ALWAYS-LIVE and chip-list-edit
4639
+ // cases need, since none of them has any other mechanism that ever calls
4640
+ // `closePeek`/`cancelEdit`/resets `overlayOpen` on its own. `cancelEdit()` is a
4641
+ // no-op for the peek/always-live cases (there is no `editingCell` to clear) and is
4642
+ // what actually ends the chip-list editor's own `startEdit` call above.
4324
4643
  React.useEffect(()=>{
4325
4644
  if (!overlayOpen) return;
4645
+ function isInside(target) {
4646
+ return (cellRef.current?.contains(target) ?? false) || (popoverRef.current?.contains(target) ?? false);
4647
+ }
4326
4648
  function handlePointerDown(event) {
4327
- if (cellRef.current && !cellRef.current.contains(event.target)) {
4649
+ if (!isInside(event.target)) {
4328
4650
  setOverlayOpen(false);
4651
+ setPopoverStyle(null);
4329
4652
  closePeek();
4653
+ cancelEdit();
4330
4654
  }
4331
4655
  }
4332
4656
  function handleKeyDown(event) {
4333
4657
  if (event.key !== "Escape") return;
4334
4658
  setOverlayOpen(false);
4659
+ setPopoverStyle(null);
4335
4660
  closePeek();
4661
+ cancelEdit();
4336
4662
  }
4337
4663
  document.addEventListener("pointerdown", handlePointerDown);
4338
4664
  document.addEventListener("keydown", handleKeyDown);
@@ -4342,27 +4668,70 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4342
4668
  };
4343
4669
  }, [
4344
4670
  overlayOpen,
4345
- closePeek
4671
+ closePeek,
4672
+ cancelEdit
4673
+ ]);
4674
+ // Feature B rework (#8) -- keeps the portaled popover glued to its cell while
4675
+ // open: any scroll on ANY ancestor (the table's own horizontal/vertical
4676
+ // `scrollArea`, or the page itself) or a viewport resize re-measures. `scroll`
4677
+ // doesn't bubble, so this listens in the CAPTURE phase on `window` -- the
4678
+ // standard way to observe a scroll on an arbitrary descendant scroll container
4679
+ // without knowing which element it is ahead of time.
4680
+ React.useEffect(()=>{
4681
+ if (!chipListEditorOpen) return;
4682
+ function reposition() {
4683
+ measurePopoverPosition();
4684
+ }
4685
+ window.addEventListener("scroll", reposition, true);
4686
+ window.addEventListener("resize", reposition);
4687
+ return ()=>{
4688
+ window.removeEventListener("scroll", reposition, true);
4689
+ window.removeEventListener("resize", reposition);
4690
+ };
4691
+ }, [
4692
+ chipListEditorOpen,
4693
+ measurePopoverPosition
4346
4694
  ]);
4347
4695
  const overlayActive = overlayOpen && (alwaysLive || isEditing(row.id, column.key) || isPeeking(row.id, column.key));
4348
- return /*#__PURE__*/ jsx(DataTableCell, {
4349
- ref: cellRef,
4350
- "data-numeric": dataAttr(column.type === "number" || column.type === "currency"),
4351
- "data-editable": dataAttr(!!column.editable),
4352
- "data-sticky": sticky["data-sticky"],
4353
- "data-sticky-edge": sticky["data-sticky-edge"],
4354
- "data-sticky-edge-end": sticky["data-sticky-edge-end"],
4355
- style: sticky.style,
4356
- onClick: handleClick,
4357
- onDoubleClick: handleDoubleClick,
4358
- children: /*#__PURE__*/ jsx(CellContent, {
4359
- column: column,
4360
- row: row,
4361
- rowId: row.id,
4362
- classNames: classNames,
4363
- locale: locale,
4364
- overlayActive: overlayActive
4365
- })
4696
+ const rawValue = getColumnValue(column, row);
4697
+ const pending = isPending(row.id, column.key);
4698
+ return /*#__PURE__*/ jsxs(Fragment, {
4699
+ children: [
4700
+ /*#__PURE__*/ jsx(DataTableCell, {
4701
+ ref: cellRef,
4702
+ "data-numeric": dataAttr(column.type === "number" || column.type === "currency"),
4703
+ "data-editable": dataAttr(!!column.editable),
4704
+ "data-sticky": sticky["data-sticky"],
4705
+ "data-sticky-edge": sticky["data-sticky-edge"],
4706
+ "data-sticky-edge-end": sticky["data-sticky-edge-end"],
4707
+ "data-overlay-open": dataAttr(overlayActive),
4708
+ style: sticky.style,
4709
+ onClick: handleClick,
4710
+ onDoubleClick: handleDoubleClick,
4711
+ children: /*#__PURE__*/ jsx(CellContent, {
4712
+ column: column,
4713
+ row: row,
4714
+ rowId: row.id,
4715
+ classNames: classNames,
4716
+ locale: locale,
4717
+ overlayActive: overlayActive
4718
+ })
4719
+ }),
4720
+ chipListEditorOpen && popoverStyle && /*#__PURE__*/ createPortal(// biome-ignore lint/a11y/noStaticElementInteractions: not a widget -- same shape as `CellExpandOverlay` (editors.tsx), which this box is a portaled variant of: it never responds to interaction on ITSELF, only stops propagation of clicks that already occurred on a real interactive descendant (the editor's input/remove buttons). No role fits it because it isn't a control.
4721
+ /*#__PURE__*/ jsx("div", {
4722
+ ref: popoverRef,
4723
+ className: classNames.expandOverlay,
4724
+ style: popoverStyle,
4725
+ onClick: (event)=>event.stopPropagation(),
4726
+ children: /*#__PURE__*/ jsx(ChipListEditor, {
4727
+ column: column,
4728
+ value: resolveEditValue(column, row, rawValue),
4729
+ pending: pending,
4730
+ onCommit: (value)=>commitEdit(row.id, column.key, value),
4731
+ classNames: classNames
4732
+ })
4733
+ }), document.body)
4734
+ ]
4366
4735
  });
4367
4736
  }
4368
4737
  /**
@@ -4402,12 +4771,18 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4402
4771
  onCommit: commit
4403
4772
  });
4404
4773
  } else if (column.editable && column.type === "chip-list") {
4405
- node = /*#__PURE__*/ jsx(ChipListEditor, {
4406
- column: column,
4407
- value: resolveEditValue(column, row, rawValue),
4408
- pending: pending,
4409
- onCommit: commit,
4410
- classNames: classNames
4774
+ // Feature B rework (#8) -- this column ALWAYS dispatches here to the plain
4775
+ // display (never to `DisplayCellContent`'s own read-only `chip-list` case,
4776
+ // never through the generic `isEditing` branch below): the click-to-edit
4777
+ // popover is a separate portal `DataTableBodyCell` renders alongside this
4778
+ // whole `<td>`, not a `node` this function returns -- see `isChipListEditable`'s
4779
+ // own doc comment (body-cells.tsx) for why it isn't anchored in place like
4780
+ // every other editor here.
4781
+ node = /*#__PURE__*/ jsx(ChipListCell, {
4782
+ value: rawValue,
4783
+ className: classNames.chipList,
4784
+ chipClassName: classNames.chip,
4785
+ overflowClassName: classNames.chipOverflow
4411
4786
  });
4412
4787
  } else if (column.editable && isEditing(rowId, column.key)) {
4413
4788
  if (column.editor === "select") {
@@ -4516,10 +4891,13 @@ function DisplayCellContent({ column, row, value, classNames, locale }) {
4516
4891
  className: classNames.chip
4517
4892
  });
4518
4893
  case "chip-list":
4894
+ // Non-editable chip-list only -- an editable one is fully handled by
4895
+ // `CellContent`'s own dedicated branch above and never reaches here.
4519
4896
  return /*#__PURE__*/ jsx(ChipListCell, {
4520
4897
  value: value,
4521
4898
  className: classNames.chipList,
4522
- chipClassName: classNames.chip
4899
+ chipClassName: classNames.chip,
4900
+ overflowClassName: classNames.chipOverflow
4523
4901
  });
4524
4902
  case "number":
4525
4903
  return /*#__PURE__*/ jsx(Fragment, {