@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.cjs CHANGED
@@ -16,6 +16,7 @@ var checkmark = require('@seed-design/css/recipes/checkmark');
16
16
  var reactCheckbox = require('@seed-design/react-checkbox');
17
17
  var reactScaleFeedback = require('@seed-design/react-scale-feedback');
18
18
  var checkboxGroup = require('@seed-design/css/recipes/checkbox-group');
19
+ var reactDom = require('react-dom');
19
20
  var reactImage = require('@seed-design/react-image');
20
21
  var avatar = require('@seed-design/css/recipes/avatar');
21
22
  require('@seed-design/css/recipes/avatar-stack');
@@ -2578,6 +2579,50 @@ const Badge = /*#__PURE__*/ React.forwardRef(({ className, children, ...props },
2578
2579
  });
2579
2580
  Badge.displayName = "Badge";
2580
2581
 
2582
+ // Feature B rework (#8, chip-list display) -- the pure "how many chips fit" decision,
2583
+ // isolated from the DOM measuring that drives it (`cells.tsx`'s `ChipListCell`) the same
2584
+ // way `truncate.ts`/`filtering.ts` already split a pure calculation out of the styled
2585
+ // layer: this file touches no DOM, so the actual fitting algorithm is unit-testable with
2586
+ // plain numbers instead of a real laid-out browser.
2587
+ //
2588
+ // Attio's own chip-list cell (e.g. Categories: "Internet", "Broadcasting", "+7") shows as
2589
+ // many WHOLE chips as fit left-to-right, then one "+N" badge for the rest -- never a
2590
+ // chip clipped mid-render, never a second wrapped line. `ChipListCell` measures every
2591
+ // chip's real rendered width (font/padding-dependent, so it can't be estimated from
2592
+ // character count) plus the container's current width, then asks this function how many
2593
+ // of those chips it can show before the running total (chips + gaps + the badge itself)
2594
+ // would exceed the container.
2595
+ /**
2596
+ * `overflowBadgeWidth` is the WORST-CASE badge width the caller measured (e.g. the
2597
+ * width of a badge showing every tag: `+<chipWidths.length>`) rather than the width of
2598
+ * the badge for whatever count this call actually returns -- the real badge (fewer
2599
+ * digits once some chips DO fit) is never wider than that, so reserving the worst case
2600
+ * up front avoids a second measuring pass and always yields a result that fits. It may
2601
+ * slightly under-fill the row by the odd chip in a boundary case (e.g. "+9" vs "+10" --
2602
+ * one fewer digit than reserved), which is the deliberate trade against re-measuring per
2603
+ * candidate count.
2604
+ */ function computeVisibleChipCount(chipWidths, containerWidth, gap, overflowBadgeWidth) {
2605
+ // An unmeasured/zero-width container (not yet laid out, e.g. `display: none` or a
2606
+ // pre-hydration frame) has no meaningful "fits" answer -- show everything rather than
2607
+ // collapsing every chip behind "+N" for a frame it'll never actually paint like that.
2608
+ if (containerWidth <= 0) return chipWidths.length;
2609
+ let used = 0;
2610
+ let count = 0;
2611
+ for(let index = 0; index < chipWidths.length; index++){
2612
+ const withGap = count > 0 ? gap : 0;
2613
+ const usedIfShown = used + withGap + chipWidths[index];
2614
+ const remainingAfterThis = chipWidths.length - (index + 1);
2615
+ // Every chip except the very last one needs room left over for the overflow badge
2616
+ // TOO (if it's going to exist at all) -- otherwise this loop would happily show
2617
+ // every chip except the last, then have nowhere left to put "+1".
2618
+ const badgeReserve = remainingAfterThis > 0 ? gap + overflowBadgeWidth : 0;
2619
+ if (usedIfShown + badgeReserve > containerWidth) break;
2620
+ used = usedIfShown;
2621
+ count++;
2622
+ }
2623
+ return count;
2624
+ }
2625
+
2581
2626
  // Attio cell-type renderers, built from seed's own components -- not exported from
2582
2627
  // the package's public surface (kept alongside DataTable.tsx as an implementation
2583
2628
  // detail, the way Select keeps `InternalIcon` under `components/private`). Each
@@ -2613,29 +2658,56 @@ function toneFor(value) {
2613
2658
  }
2614
2659
  return BADGE_TONES[hash % BADGE_TONES.length];
2615
2660
  }
2616
- // Feature A/B (#9/#8) -- Attio's own flat 12-swatch chip palette, ported from
2617
- // crisp's `tokens/palette.ts` (`tagSwatch1..12`, measured live off Attio) rather
2618
- // than seed's own 6-tone `Badge` (`BADGE_TONES` above): a swatch, unlike a Badge
2619
- // tone, has no ring/border and its ink is a FLAT black on every swatch (crisp's own
2620
- // `tagSwatchInk`, `#000000` -- every swatch background is already a pale tint, so
2621
- // one dark foreground reads on all 12), not a per-tone contrast color. No seed token
2622
- // lands on any of these 12 hex values, so -- same footnote as the qvism recipe's own
2623
- // `ATTIO_BRAND_BLUE`/`RATING_FILLED` literals -- they stay literal here too.
2624
2661
  const CHIP_SWATCHES = [
2625
- "#fff3cc",
2626
- "#f4fbcb",
2627
- "#fdf7c4",
2628
- "#feecf3",
2629
- "#e0fced",
2630
- "#ddf7ff",
2631
- "#f6f7f7",
2632
- "#ffebeb",
2633
- "#feeee1",
2634
- "#e5eeff",
2635
- "#f5f0ff",
2636
- "#fdedff"
2662
+ {
2663
+ background: "#dbeafe",
2664
+ color: "#1e40af"
2665
+ },
2666
+ {
2667
+ background: "#dcfce7",
2668
+ color: "#166534"
2669
+ },
2670
+ {
2671
+ background: "#ede9fe",
2672
+ color: "#5b21b6"
2673
+ },
2674
+ {
2675
+ background: "#fef3c7",
2676
+ color: "#92400e"
2677
+ },
2678
+ {
2679
+ background: "#fee2e2",
2680
+ color: "#991b1b"
2681
+ },
2682
+ {
2683
+ background: "#ccfbf1",
2684
+ color: "#115e59"
2685
+ },
2686
+ {
2687
+ background: "#fce7f3",
2688
+ color: "#9d174d"
2689
+ },
2690
+ {
2691
+ background: "#e0e7ff",
2692
+ color: "#3730a3"
2693
+ },
2694
+ {
2695
+ background: "#cffafe",
2696
+ color: "#155e75"
2697
+ },
2698
+ {
2699
+ background: "#ecfccb",
2700
+ color: "#3f6212"
2701
+ },
2702
+ {
2703
+ background: "#ffe4e6",
2704
+ color: "#9f1239"
2705
+ },
2706
+ {
2707
+ background: "#e2e8f0",
2708
+ color: "#334155"
2709
+ }
2637
2710
  ];
2638
- const CHIP_SWATCH_INK = "#000000";
2639
2711
  /** Same stable multiply-and-add hash `toneFor` above already uses over seed's 6
2640
2712
  * Badge tones, just over this file's own 12-swatch palette instead -- same value,
2641
2713
  * same swatch, every render (crisp's own `toneFor`, `datatable.tsx`). */ function swatchIndexFor(value) {
@@ -2646,7 +2718,13 @@ const CHIP_SWATCH_INK = "#000000";
2646
2718
  return hash % CHIP_SWATCHES.length;
2647
2719
  }
2648
2720
  function swatchColorFor(value) {
2649
- return CHIP_SWATCHES[swatchIndexFor(value)];
2721
+ return CHIP_SWATCHES[swatchIndexFor(value)].background;
2722
+ }
2723
+ /** The per-hue dark text paired with `swatchColorFor`'s background, at the SAME
2724
+ * index (`swatchIndexFor`) -- replaces the old single flat `CHIP_SWATCH_INK`
2725
+ * (`#000000`) now that each background is saturated enough that one foreground no
2726
+ * longer reads well on all 12. */ function swatchInkFor(value) {
2727
+ return CHIP_SWATCHES[swatchIndexFor(value)].color;
2650
2728
  }
2651
2729
  function initialsFor(name) {
2652
2730
  const parts = name.trim().split(/\s+/);
@@ -2763,7 +2841,7 @@ function StatusCell({ value }) {
2763
2841
  });
2764
2842
  }
2765
2843
  /** One swatch chip -- `style` carries the per-value background/ink (computed by
2766
- * `swatchColorFor`/`CHIP_SWATCH_INK` above) since neither is a recipe token, the
2844
+ * `swatchColorFor`/`swatchInkFor` above) since neither is a recipe token, the
2767
2845
  * same "literal color as a computed prop" call `RatingCell`'s star fill already
2768
2846
  * makes. Shared by `ChipCell` (Feature A, single value) and `ChipListCell`/
2769
2847
  * editors.tsx's `ChipListEditor` (Feature B, multi value) so all three chip
@@ -2772,7 +2850,7 @@ function StatusCell({ value }) {
2772
2850
  className: className,
2773
2851
  style: {
2774
2852
  backgroundColor: swatchColorFor(value),
2775
- color: CHIP_SWATCH_INK
2853
+ color: swatchInkFor(value)
2776
2854
  },
2777
2855
  children: [
2778
2856
  value,
@@ -2792,19 +2870,125 @@ function StatusCell({ value }) {
2792
2870
  className: className
2793
2871
  });
2794
2872
  }
2795
- /** `type: "chip-list"`, READ-ONLY (Feature B, #8) -- multiple swatch chips, no add/
2796
- * remove affordances. The editable case is editors.tsx's `ChipListEditor`, an
2797
- * always-live widget (interacting with it IS the commit, the same `switch`/
2798
- * `rating`/`checkbox` shape `body-cells.tsx`'s `isAlwaysLiveEditable` already
2799
- * covers) -- this component only ever renders the REST display. */ function ChipListCell({ value, className, chipClassName }) {
2873
+ // Feature B rework (#8) -- the chip-list DISPLAY row's own gap, mirrored from the
2874
+ // qvism recipe's `chipList` slot (`packages/qvism-preset/src/recipes/data-table.ts`'s
2875
+ // `CHIP_GAP`, measured live off Attio at 3px between chips). Kept as a literal here
2876
+ // too, same footnote as `CHIP_SWATCHES` above: this file and the recipe are two
2877
+ // independent sources for numbers that have to agree, not one importing the other
2878
+ // (react/AGENTS.md -- style stays in the recipe; this is the one JS-side computation,
2879
+ // `computeVisibleChipCount`, that needs the SAME number to size against).
2880
+ const CHIP_LIST_GAP_PX = 3;
2881
+ /** `type: "chip-list"` display -- every editable/non-editable chip-list cell renders
2882
+ * through this component for its REST state (editable: when its click-to-edit
2883
+ * popover, editors.tsx's `ChipListEditor`, isn't open; read-only: always). Attio's
2884
+ * own model (its Categories cell: "Internet", "Broadcasting", "+7") -- show as many
2885
+ * WHOLE chips as fit the cell's current width, then one "+N" badge for the rest,
2886
+ * never a chip clipped mid-render, never a second wrapped line, never overflowing
2887
+ * into a neighboring (possibly sticky) column. `body-cells.tsx`'s `isAlwaysLiveEditable`
2888
+ * no longer lists `chip-list` -- the always-live inline editor this replaced is what
2889
+ * produced exactly that overflow (crispy-seed's #1094).
2890
+ *
2891
+ * The fitting decision itself (`computeVisibleChipCount`, `chip-overflow.ts`) is pure
2892
+ * and unit-tested on its own; this component's only job is feeding it real measured
2893
+ * widths. Two widths need measuring that can't be estimated from character count
2894
+ * (font/padding-dependent): every chip's own rendered width, and the "+N" badge's
2895
+ * worst-case width (reserved for the FULL tag count, so the real badge -- fewer
2896
+ * digits once some chips fit -- never overflows what was reserved for it; see
2897
+ * `computeVisibleChipCount`'s own doc comment). Both are measured off a permanently
2898
+ * mounted, invisible copy (`visibility: hidden`, absolutely positioned so it never
2899
+ * affects layout -- `position: relative` on the `chipList` slot's own recipe rule
2900
+ * makes THIS element, not the `<td>` two levels up, its containing block, and the
2901
+ * `chipList` slot's own `overflow: hidden` clips it there too) -- kept mounted at ALL
2902
+ * times, not just while a `ResizeObserver` fires, because widening the column back
2903
+ * out needs to know the width of chips currently hidden behind "+N" to decide
2904
+ * whether they now fit again.
2905
+ */ function ChipListCell({ value, className, chipClassName, overflowClassName }) {
2800
2906
  const tags = Array.isArray(value) ? value.filter((tag)=>!!tag) : [];
2907
+ const containerRef = React__namespace.useRef(null);
2908
+ const measureRef = React__namespace.useRef(null);
2909
+ const overflowMeasureRef = React__namespace.useRef(null);
2910
+ const [visibleCount, setVisibleCount] = React__namespace.useState(tags.length);
2911
+ // Every tag joined into one string -- a stable primitive dependency for the effect
2912
+ // below, so it only re-measures when the actual tag SET changes, not on every
2913
+ // unrelated re-render of the row (a new `tags` array identity from a fresh
2914
+ // `getColumnValue` call on every render would otherwise re-run this every time).
2915
+ const tagsKey = tags.join("\u0000");
2916
+ // 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.
2917
+ React__namespace.useLayoutEffect(()=>{
2918
+ const container = containerRef.current;
2919
+ const measure = measureRef.current;
2920
+ // No real layout to measure against (SSR, or a test environment with no layout
2921
+ // engine, e.g. this package's own `bun test` -- happy-dom lays out nothing, every
2922
+ // rect comes back zero) -- show every chip rather than a permanently-wrong "+N"
2923
+ // for a width this environment can never actually report.
2924
+ if (!container || !measure || typeof ResizeObserver === "undefined") {
2925
+ setVisibleCount(tags.length);
2926
+ return;
2927
+ }
2928
+ // Rebound to non-null consts (rather than a `!` assertion inside `recompute`
2929
+ // below) -- `container`/`measure` are already narrowed non-null here, and these
2930
+ // two bindings carry that narrowing into the closure without re-deriving it.
2931
+ const measuredContainer = container;
2932
+ const measuredList = measure;
2933
+ function recompute() {
2934
+ const chipWidths = Array.from(measuredList.children, (el)=>el.getBoundingClientRect().width);
2935
+ const overflowWidth = overflowMeasureRef.current?.getBoundingClientRect().width ?? 0;
2936
+ setVisibleCount(computeVisibleChipCount(chipWidths, measuredContainer.clientWidth, CHIP_LIST_GAP_PX, overflowWidth));
2937
+ }
2938
+ recompute();
2939
+ const observer = new ResizeObserver(recompute);
2940
+ observer.observe(container);
2941
+ return ()=>observer.disconnect();
2942
+ }, [
2943
+ tagsKey
2944
+ ]);
2801
2945
  if (!tags.length) return null;
2802
- return /*#__PURE__*/ jsxRuntime.jsx("span", {
2946
+ const clampedVisible = Math.min(visibleCount, tags.length);
2947
+ const overflowCount = tags.length - clampedVisible;
2948
+ return /*#__PURE__*/ jsxRuntime.jsxs("span", {
2803
2949
  className: className,
2804
- children: tags.map((tag)=>/*#__PURE__*/ jsxRuntime.jsx(SwatchChip, {
2805
- value: tag,
2806
- className: chipClassName
2807
- }, tag))
2950
+ ref: containerRef,
2951
+ children: [
2952
+ tags.slice(0, clampedVisible).map((tag)=>/*#__PURE__*/ jsxRuntime.jsx(SwatchChip, {
2953
+ value: tag,
2954
+ className: chipClassName
2955
+ }, tag)),
2956
+ overflowCount > 0 && /*#__PURE__*/ jsxRuntime.jsxs("span", {
2957
+ className: overflowClassName,
2958
+ children: [
2959
+ "+",
2960
+ overflowCount
2961
+ ]
2962
+ }),
2963
+ /*#__PURE__*/ jsxRuntime.jsx("span", {
2964
+ ref: measureRef,
2965
+ "aria-hidden": "true",
2966
+ className: className,
2967
+ style: {
2968
+ position: "absolute",
2969
+ visibility: "hidden",
2970
+ pointerEvents: "none"
2971
+ },
2972
+ children: tags.map((tag)=>/*#__PURE__*/ jsxRuntime.jsx(SwatchChip, {
2973
+ value: tag,
2974
+ className: chipClassName
2975
+ }, tag))
2976
+ }),
2977
+ /*#__PURE__*/ jsxRuntime.jsxs("span", {
2978
+ ref: overflowMeasureRef,
2979
+ "aria-hidden": "true",
2980
+ className: overflowClassName,
2981
+ style: {
2982
+ position: "absolute",
2983
+ visibility: "hidden",
2984
+ pointerEvents: "none"
2985
+ },
2986
+ children: [
2987
+ "+",
2988
+ tags.length
2989
+ ]
2990
+ })
2991
+ ]
2808
2992
  });
2809
2993
  }
2810
2994
  /**
@@ -2970,8 +3154,28 @@ function RatingCell({ value, maxRating = 5 }) {
2970
3154
  let cursor = selectable ? 40 : 0;
2971
3155
  let pinnedCount = 0;
2972
3156
  const entries = ordered.map((key)=>byKey.get(key)).filter((column)=>Boolean(column)).map((column)=>{
2973
- const width = getColumnWidth(column.key) ?? column.width;
3157
+ const rawWidth = getColumnWidth(column.key) ?? column.width;
2974
3158
  const pinned = pinnedSet.has(column.key);
3159
+ // Fix (0.10.4, screenshot-confirmed pinned-column overlay): a pinned column
3160
+ // with no explicit/resized width used to keep `width` as `undefined` here,
3161
+ // which `DataTableColGroup` (DataTable.tsx) renders as an AUTO `<col>` --
3162
+ // under `table-layout: fixed`, the browser then sizes it from whatever
3163
+ // remaining space is left (anywhere from its full share down to 0, e.g. once
3164
+ // the OTHER explicit-width columns already exceed the container). Meanwhile
3165
+ // `cursor` below used `DEFAULT_PINNED_COLUMN_WIDTH` (180) as its OWN guess for
3166
+ // that same column's width, to compute every LATER pinned column's
3167
+ // `insetInlineStart`. Those two numbers -- the browser's real rendered width
3168
+ // and this file's 180px guess -- had no reason to agree, and whenever they
3169
+ // didn't, every pinned/unpinned column after the mismatched one landed at the
3170
+ // wrong screen position: the next pinned column's sticky box (positioned at
3171
+ // the WRONG, JS-assumed offset) then visually overlapped whatever unpinned
3172
+ // content actually continues from the browser's REAL (different) offset.
3173
+ // Forcing the SAME fallback into the `<col>` itself (not just the cursor
3174
+ // math) is what makes the two agree by construction -- a pinned column
3175
+ // without its own width now always renders at exactly
3176
+ // `DEFAULT_PINNED_COLUMN_WIDTH`, matching what every other pinned column's
3177
+ // offset already assumed it would be.
3178
+ const width = pinned && rawWidth === undefined ? DEFAULT_PINNED_COLUMN_WIDTH : rawWidth;
2975
3179
  const pinOffset = pinned ? cursor : 0;
2976
3180
  if (pinned) {
2977
3181
  cursor += width ?? DEFAULT_PINNED_COLUMN_WIDTH;
@@ -3376,7 +3580,7 @@ const TextFieldTextarea = /*#__PURE__*/ React.forwardRef(({ className, autoresiz
3376
3580
  });
3377
3581
  TextFieldTextarea.displayName = "TextFieldTextarea";
3378
3582
 
3379
- __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))}");
3583
+ __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))}");
3380
3584
 
3381
3585
  const createClassName = (className, variants, compoundVariants = [])=>{
3382
3586
  const variantKeys = Object.keys(variants);
@@ -3479,6 +3683,14 @@ const dataTableSlotNames = [
3479
3683
  "chipList",
3480
3684
  "seed-data-table__chipList"
3481
3685
  ],
3686
+ [
3687
+ "chipOverflow",
3688
+ "seed-data-table__chipOverflow"
3689
+ ],
3690
+ [
3691
+ "chipListEditor",
3692
+ "seed-data-table__chipListEditor"
3693
+ ],
3482
3694
  [
3483
3695
  "chipRemove",
3484
3696
  "seed-data-table__chipRemove"
@@ -4082,20 +4294,31 @@ function RatingEditor({ column, value, pending, onCommit }) {
4082
4294
  });
4083
4295
  }
4084
4296
  ////////////////////////////////////////////////////////////////////////////////////
4085
- // -- editable: "chip-list" (Feature B, #8) -- always-live, the same shape
4086
- // `SwitchEditor`/`CheckboxEditor`/`RatingEditor` above already establish
4087
- // (interacting with the widget IS the commit, no double-click gate, no separate
4088
- // rest display -- `body-cells.tsx`'s `isAlwaysLiveEditable` covers it alongside
4089
- // them). A gated open/close state (like `SelectEditor`'s) would fight adding
4090
- // MULTIPLE tags in one sitting: `useDataTableEdit`'s `commitEdit` closes editing
4091
- // the instant a SYNC commit resolves, so a gated chip editor would close itself
4092
- // after the very first tag. Always-live sidesteps that entirely -- there is no
4093
- // `editingCell` to close, the widget is simply always the cell's own content.
4094
- // Every add/remove commits the FULL next array through the same one-string
4297
+ // -- editable: "chip-list" (Feature B rework, #8) -- click-to-edit, Attio's own model
4298
+ // rather than the always-live widget this replaced (crispy-seed#1094: every row
4299
+ // rendering a live add-tag editor at once overflowed the cell and collided with
4300
+ // sticky columns). `body-cells.tsx`'s `DataTableBodyCell` opens this INSIDE the
4301
+ // same anchored `CellExpandOverlay` Feature C already built for a gated editor's
4302
+ // expanded state, on a plain single click (no truncation gate, unlike every other
4303
+ // read-only cell's peek) -- clicking a short 2-tag cell must open the editor just
4304
+ // as readily as a 10-tag one. It stays open across MULTIPLE add/remove commits
4305
+ // (closes only on blur/outside-click/Escape, `DataTableBodyCell`'s own effect) by
4306
+ // NOT going through `useDataTableEdit`'s `isEditing` gate the way `SelectEditor`/
4307
+ // `TextEditor` do -- `commitEdit` closes `editingCell` the instant a SYNC commit
4308
+ // resolves (its own doc comment), which would have closed a gated chip editor
4309
+ // after the very first tag. `DataTableBodyCell` instead tracks THIS editor's own
4310
+ // open/closed state in its local `overlayOpen`, independent of `editingCell`.
4311
+ // Every add/remove still commits the FULL next array through the same one-string
4095
4312
  // contract every other editor already uses (`useDataTableEdit`'s own doc
4096
- // comment): `JSON.stringify(nextTags)`, which the consumer's `onEdit` parses
4097
- // back -- crisp/GRC's contract stays "one string in, no typed union", not a
4098
- // second commit shape invented for just this column.
4313
+ // comment): `JSON.stringify(nextTags)`, which the consumer's `onEdit` parses back
4314
+ // -- crisp/GRC's contract stays "one string in, no typed union", not a second
4315
+ // commit shape invented for just this column.
4316
+ //
4317
+ // Renders through `classNames.chipListEditor`, NOT the read-only `chipList` slot
4318
+ // (cells.tsx's `ChipListCell`) -- the display row clips to one line with a "+N"
4319
+ // badge; this editor instead WRAPS onto as many lines as it needs inside the
4320
+ // overlay's own grow-in-height box, since every tag (plus the add-input) has to
4321
+ // stay visible and editable at once.
4099
4322
  function ChipListEditor({ column, value, pending, onCommit, classNames }) {
4100
4323
  const tags = Array.isArray(value) ? value.filter((tag)=>!!tag) : [];
4101
4324
  const [draft, setDraft] = React__namespace.useState("");
@@ -4116,7 +4339,7 @@ function ChipListEditor({ column, value, pending, onCommit, classNames }) {
4116
4339
  }
4117
4340
  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.
4118
4341
  /*#__PURE__*/ jsxRuntime.jsxs("span", {
4119
- className: classNames.chipList,
4342
+ className: classNames.chipListEditor,
4120
4343
  onClick: (event)=>event.stopPropagation(),
4121
4344
  children: [
4122
4345
  tags.map((tag)=>/*#__PURE__*/ jsxRuntime.jsx(SwatchChip, {
@@ -4134,6 +4357,8 @@ function ChipListEditor({ column, value, pending, onCommit, classNames }) {
4134
4357
  })
4135
4358
  }, tag)),
4136
4359
  /*#__PURE__*/ jsxRuntime.jsx("input", {
4360
+ // 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.
4361
+ autoFocus: true,
4137
4362
  className: classNames.chipInput,
4138
4363
  value: draft,
4139
4364
  disabled: pending,
@@ -4141,14 +4366,19 @@ function ChipListEditor({ column, value, pending, onCommit, classNames }) {
4141
4366
  "aria-label": `Add tag to ${column.key}`,
4142
4367
  onChange: (event)=>setDraft(event.target.value),
4143
4368
  onKeyDown: (event)=>{
4369
+ // Escape is the ONE key this input must NOT stop propagation for: there is
4370
+ // no in-progress draft state worth clearing locally (every add/remove
4371
+ // already commits immediately), so it needs to bubble all the way to
4372
+ // `document` -- `DataTableBodyCell`'s own Escape listener is what actually
4373
+ // closes this popover. Every other key stops here, same as before, so it
4374
+ // never reaches the row's own click/keyboard handling.
4375
+ if (event.key === "Escape") return;
4144
4376
  event.stopPropagation();
4145
4377
  if (event.key === "Enter") {
4146
4378
  event.preventDefault();
4147
4379
  addTag();
4148
4380
  } else if (event.key === "Backspace" && draft === "" && tags.length > 0) {
4149
4381
  removeTag(tags[tags.length - 1]);
4150
- } else if (event.key === "Escape") {
4151
- setDraft("");
4152
4382
  }
4153
4383
  }
4154
4384
  }),
@@ -4253,12 +4483,41 @@ function isCellTruncated(cell) {
4253
4483
  /** `editable` + one of these shapes needs no double-click gate and no separate rest
4254
4484
  * display -- interacting with the widget itself IS the commit gesture (crisp's own
4255
4485
  * #453/#760/#761: `switch`/`rating`/`checkbox` never had a meaningfully different
4256
- * "rest" state to show). `chip-list` (Feature B, #8) joins them for a different
4257
- * reason -- see `ChipListEditor`'s own doc comment (editors.tsx): a GATED chip
4258
- * editor would close itself the instant the first tag committed. Every other
4259
- * `editable` column opens on double-click. */ function isAlwaysLiveEditable(column) {
4486
+ * "rest" state to show). `chip-list` used to join them here (Feature B, #8) but no
4487
+ * longer does -- see `isChipListEditable` below, its own separate "click opens a
4488
+ * popover, closed the rest of the time" category, and `ChipListEditor`'s own doc
4489
+ * comment (editors.tsx) for why it isn't just the ordinary GATED (double-click)
4490
+ * shape either. Every other `editable` column opens on double-click. */ function isAlwaysLiveEditable(column) {
4260
4491
  if (!column.editable) return false;
4261
- return column.editor === "switch" || column.type === "rating" || column.type === "checkbox" || column.type === "chip-list";
4492
+ return column.editor === "switch" || column.type === "rating" || column.type === "checkbox";
4493
+ }
4494
+ /** Feature B rework (#8) -- an editable `chip-list` column's own third category,
4495
+ * alongside `isAlwaysLiveEditable` (above) and the ordinary double-click GATED
4496
+ * shape: a single click (no truncation gate -- even a short, unclipped 2-tag cell
4497
+ * must open just as readily as a long one) opens `editors.tsx`'s `ChipListEditor`,
4498
+ * and it closes back to the plain `ChipListCell` display on blur/outside-click/
4499
+ * Escape (`DataTableBodyCell`'s own `overlayOpen`) -- never always-live
4500
+ * (crispy-seed#1094: that overflowed the cell and collided with sticky columns),
4501
+ * never simply GATED either (a `useDataTableEdit`-driven `isEditing` gate would
4502
+ * close itself after the first tag commits, see `ChipListEditor`'s own doc
4503
+ * comment).
4504
+ *
4505
+ * Unlike every OTHER open editor in this file, this one does NOT render inside
4506
+ * Feature C's `CellExpandOverlay` (an anchored box positioned relative to the
4507
+ * `<td>`, `position: absolute` against it) -- it renders through a REAL React
4508
+ * portal to `document.body` instead (`DataTableBodyCell`'s own `popoverStyle`).
4509
+ * Confirmed live, crispy-seed#1094's pinned-column follow-up: a `position: sticky`
4510
+ * `<td>` clips ANY descendant content that grows past its own box to the table's
4511
+ * scrollport, in EVERY browser tested, regardless of `overflow: visible` on every
4512
+ * element in between and regardless of how high its `z-index` is raised -- neither
4513
+ * of which is a stacking-context or overflow bug this file can fix from the
4514
+ * INSIDE. A portal sidesteps it entirely by not being a descendant of the sticky
4515
+ * `<td>` at all once mounted. The same sticky-clip almost certainly affects the
4516
+ * OTHER gated editors' `CellExpandOverlay` too when their own column is pinned,
4517
+ * but porting all of Feature C to this same portal model is its own change,
4518
+ * out of scope here -- this fix is scoped to the one editor crispy-seed#1094
4519
+ * actually reported against a pinned column. */ function isChipListEditable(column) {
4520
+ return !!column.editable && column.type === "chip-list";
4262
4521
  }
4263
4522
  function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyFirst, stickyLast }) {
4264
4523
  const effective = useEffectiveColumns(columns, {
@@ -4288,8 +4547,13 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4288
4547
  * `ColumnBodyCells` keys it by `column.key` the same way it always keyed the bare
4289
4548
  * `<DataTableCell>` before this split.
4290
4549
  */ function DataTableBodyCell({ column, row, classNames, locale, sticky }) {
4291
- const { startEdit, isEditing, isPeeking, openPeek, closePeek } = useDataTableContext();
4550
+ const { startEdit, cancelEdit, isEditing, isPending, commitEdit, isPeeking, openPeek, closePeek } = useDataTableContext();
4292
4551
  const cellRef = React__namespace.useRef(null);
4552
+ // Feature B rework (#8) -- the chip-list popover's own live position, in VIEWPORT
4553
+ // coordinates (`position: fixed`), since it portals to `document.body` rather than
4554
+ // anchoring inside the `<td>` (see `isChipListEditable`'s own doc comment for why).
4555
+ // `null` while closed -- nothing to portal yet.
4556
+ const [popoverStyle, setPopoverStyle] = React__namespace.useState(null);
4293
4557
  // Feature C (#12) -- whether THIS activation (click or double-click) should
4294
4558
  // render as the anchored overlay rather than plain inline. Frozen at the moment
4295
4559
  // of activation (re-measured every time, never left stale) rather than derived
@@ -4299,20 +4563,68 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4299
4563
  // rendering exactly as it always has, not suddenly gain overlay chrome.
4300
4564
  const [overlayOpen, setOverlayOpen] = React__namespace.useState(false);
4301
4565
  const alwaysLive = isAlwaysLiveEditable(column);
4302
- // A gated `editable` column (everything except the always-live widgets) opens on
4303
- // double-click -- Attio's own measured gesture (crisp's #411), matched here
4304
- // rather than a single click, which stays "select this cell" in crisp's own
4305
- // spreadsheet model and would otherwise be indistinguishable from this table's
4306
- // plain row click. Feature C ADDS a single click as a SECOND way in, but only
4307
- // when the cell's content is actually truncated (see `handleClick` below) --
4308
- // double-click keeps working unconditionally, same as before this feature.
4309
- const gated = column.editable && !alwaysLive;
4566
+ const chipListEditable = isChipListEditable(column);
4567
+ // A gated `editable` column (everything except the always-live widgets AND the
4568
+ // click-to-edit chip-list) opens on double-click -- Attio's own measured gesture
4569
+ // (crisp's #411), matched here rather than a single click, which stays "select
4570
+ // this cell" in crisp's own spreadsheet model and would otherwise be
4571
+ // indistinguishable from this table's plain row click. Feature C ADDS a single
4572
+ // click as a SECOND way in, but only when the cell's content is actually
4573
+ // truncated (see `handleClick` below) -- double-click keeps working
4574
+ // unconditionally, same as before this feature.
4575
+ const gated = column.editable && !alwaysLive && !chipListEditable;
4576
+ // This cell's chip-list popover is showing right now -- `overlayOpen` alone isn't
4577
+ // enough (it's shared with the read-only peek/gated-edit cases below), so this
4578
+ // ANDs in "is this actually a chip-list column" too. Gates the portal render
4579
+ // below (`popoverStyle` is only ever non-null while this is true).
4580
+ const chipListEditorOpen = chipListEditable && overlayOpen;
4581
+ // Feature B rework (#8) -- the chip-list popover's own position, read off the
4582
+ // `<td>`'s LIVE `getBoundingClientRect()` (viewport coordinates, matching
4583
+ // `position: fixed`) rather than anything Feature C's `pinOffset`/sticky math
4584
+ // already tracks -- those describe offsets WITHIN the scrolling table, not where
4585
+ // the cell actually sits in the viewport right now, which is what a portaled,
4586
+ // `position: fixed` box anchored below the cell needs.
4587
+ const measurePopoverPosition = React__namespace.useCallback(()=>{
4588
+ const cell = cellRef.current;
4589
+ if (!cell) return;
4590
+ const rect = cell.getBoundingClientRect();
4591
+ setPopoverStyle({
4592
+ position: "fixed",
4593
+ top: rect.bottom,
4594
+ insetInlineStart: rect.left,
4595
+ insetInlineEnd: "auto",
4596
+ minWidth: rect.width,
4597
+ // Both override the SAME `expandOverlay` class properties this box also
4598
+ // carries (`className={classNames.expandOverlay}`, below) -- that class's
4599
+ // `minHeight: calc(100% + 2px)` resolves against an ANCHORED box's own
4600
+ // containing block (the `<td>`, ~36px), which is meaningless (and would
4601
+ // resolve against the VIEWPORT instead, ~100% of the page's height) for a
4602
+ // portaled `position: fixed` box; `auto` lets the content size it instead.
4603
+ // `zIndex: 10` is Feature C's own value for an in-place overlay competing
4604
+ // only with OTHER table content -- a portal to `document.body` can land
4605
+ // beside arbitrary page chrome (this Storybook's own toolbar, a consuming
4606
+ // app's header), so it gets a value comfortably above typical page chrome.
4607
+ minHeight: "auto",
4608
+ zIndex: 1000
4609
+ });
4610
+ }, []);
4310
4611
  function handleDoubleClick(event) {
4311
4612
  if (!gated) return;
4312
4613
  setOverlayOpen(isCellTruncated(event.currentTarget));
4313
4614
  startEdit(row.id, column.key);
4314
4615
  }
4315
4616
  function handleClick(event) {
4617
+ if (chipListEditable) {
4618
+ // Attio's own chip-list gesture: click opens the editor regardless of
4619
+ // truncation -- a short, fully-visible 2-tag cell opens just as readily as
4620
+ // one already showing "+N". Must not also reach the row's own `onRowClick`,
4621
+ // same reasoning as the read-only peek/preview-button cases below.
4622
+ event.stopPropagation();
4623
+ measurePopoverPosition();
4624
+ setOverlayOpen(true);
4625
+ startEdit(row.id, column.key);
4626
+ return;
4627
+ }
4316
4628
  if (!isCellTruncated(event.currentTarget)) return;
4317
4629
  if (gated) {
4318
4630
  setOverlayOpen(true);
@@ -4320,7 +4632,7 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4320
4632
  } else if (alwaysLive) {
4321
4633
  // No separate open/edit state to start -- the always-live widget already
4322
4634
  // renders unconditionally; this only switches ITS OWN box to the anchored
4323
- // overlay so a long chip list (etc.) gets room to show every chip.
4635
+ // overlay so a long value (etc.) gets room to show in full.
4324
4636
  setOverlayOpen(true);
4325
4637
  } else {
4326
4638
  // Read-only truncated cell: this click's only job is opening the peek
@@ -4333,25 +4645,39 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4333
4645
  openPeek(row.id, column.key);
4334
4646
  }
4335
4647
  }
4648
+ // Feature B rework (#8) -- the portaled popover's own DOM node. Read by the
4649
+ // outside-click detector below (it lives outside `cellRef`'s subtree once
4650
+ // mounted, in `document.body` -- see `isChipListEditable`'s doc comment) and by
4651
+ // the reposition effect further down.
4652
+ const popoverRef = React__namespace.useRef(null);
4336
4653
  // Closes the overlay on Escape or an outside click/pointerdown -- a gated
4337
4654
  // editor's OWN Escape/blur handling already ends `isEditing` on its own (see
4338
4655
  // `TextEditor`/`SelectEditor`'s existing onCancel/onOpenChange wiring), which
4339
4656
  // alone is enough to hide a GATED cell's overlay (its render below is gated on
4340
- // `isEditing` too); this listener is what the PEEK and ALWAYS-LIVE cases need,
4341
- // since neither has any other mechanism that ever calls `closePeek`/resets
4342
- // `overlayOpen` on its own.
4657
+ // `isEditing` too); this listener is what the PEEK, ALWAYS-LIVE and chip-list-edit
4658
+ // cases need, since none of them has any other mechanism that ever calls
4659
+ // `closePeek`/`cancelEdit`/resets `overlayOpen` on its own. `cancelEdit()` is a
4660
+ // no-op for the peek/always-live cases (there is no `editingCell` to clear) and is
4661
+ // what actually ends the chip-list editor's own `startEdit` call above.
4343
4662
  React__namespace.useEffect(()=>{
4344
4663
  if (!overlayOpen) return;
4664
+ function isInside(target) {
4665
+ return (cellRef.current?.contains(target) ?? false) || (popoverRef.current?.contains(target) ?? false);
4666
+ }
4345
4667
  function handlePointerDown(event) {
4346
- if (cellRef.current && !cellRef.current.contains(event.target)) {
4668
+ if (!isInside(event.target)) {
4347
4669
  setOverlayOpen(false);
4670
+ setPopoverStyle(null);
4348
4671
  closePeek();
4672
+ cancelEdit();
4349
4673
  }
4350
4674
  }
4351
4675
  function handleKeyDown(event) {
4352
4676
  if (event.key !== "Escape") return;
4353
4677
  setOverlayOpen(false);
4678
+ setPopoverStyle(null);
4354
4679
  closePeek();
4680
+ cancelEdit();
4355
4681
  }
4356
4682
  document.addEventListener("pointerdown", handlePointerDown);
4357
4683
  document.addEventListener("keydown", handleKeyDown);
@@ -4361,27 +4687,70 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4361
4687
  };
4362
4688
  }, [
4363
4689
  overlayOpen,
4364
- closePeek
4690
+ closePeek,
4691
+ cancelEdit
4692
+ ]);
4693
+ // Feature B rework (#8) -- keeps the portaled popover glued to its cell while
4694
+ // open: any scroll on ANY ancestor (the table's own horizontal/vertical
4695
+ // `scrollArea`, or the page itself) or a viewport resize re-measures. `scroll`
4696
+ // doesn't bubble, so this listens in the CAPTURE phase on `window` -- the
4697
+ // standard way to observe a scroll on an arbitrary descendant scroll container
4698
+ // without knowing which element it is ahead of time.
4699
+ React__namespace.useEffect(()=>{
4700
+ if (!chipListEditorOpen) return;
4701
+ function reposition() {
4702
+ measurePopoverPosition();
4703
+ }
4704
+ window.addEventListener("scroll", reposition, true);
4705
+ window.addEventListener("resize", reposition);
4706
+ return ()=>{
4707
+ window.removeEventListener("scroll", reposition, true);
4708
+ window.removeEventListener("resize", reposition);
4709
+ };
4710
+ }, [
4711
+ chipListEditorOpen,
4712
+ measurePopoverPosition
4365
4713
  ]);
4366
4714
  const overlayActive = overlayOpen && (alwaysLive || isEditing(row.id, column.key) || isPeeking(row.id, column.key));
4367
- return /*#__PURE__*/ jsxRuntime.jsx(DataTableCell, {
4368
- ref: cellRef,
4369
- "data-numeric": domUtils.dataAttr(column.type === "number" || column.type === "currency"),
4370
- "data-editable": domUtils.dataAttr(!!column.editable),
4371
- "data-sticky": sticky["data-sticky"],
4372
- "data-sticky-edge": sticky["data-sticky-edge"],
4373
- "data-sticky-edge-end": sticky["data-sticky-edge-end"],
4374
- style: sticky.style,
4375
- onClick: handleClick,
4376
- onDoubleClick: handleDoubleClick,
4377
- children: /*#__PURE__*/ jsxRuntime.jsx(CellContent, {
4378
- column: column,
4379
- row: row,
4380
- rowId: row.id,
4381
- classNames: classNames,
4382
- locale: locale,
4383
- overlayActive: overlayActive
4384
- })
4715
+ const rawValue = getColumnValue(column, row);
4716
+ const pending = isPending(row.id, column.key);
4717
+ return /*#__PURE__*/ jsxRuntime.jsxs(jsxRuntime.Fragment, {
4718
+ children: [
4719
+ /*#__PURE__*/ jsxRuntime.jsx(DataTableCell, {
4720
+ ref: cellRef,
4721
+ "data-numeric": domUtils.dataAttr(column.type === "number" || column.type === "currency"),
4722
+ "data-editable": domUtils.dataAttr(!!column.editable),
4723
+ "data-sticky": sticky["data-sticky"],
4724
+ "data-sticky-edge": sticky["data-sticky-edge"],
4725
+ "data-sticky-edge-end": sticky["data-sticky-edge-end"],
4726
+ "data-overlay-open": domUtils.dataAttr(overlayActive),
4727
+ style: sticky.style,
4728
+ onClick: handleClick,
4729
+ onDoubleClick: handleDoubleClick,
4730
+ children: /*#__PURE__*/ jsxRuntime.jsx(CellContent, {
4731
+ column: column,
4732
+ row: row,
4733
+ rowId: row.id,
4734
+ classNames: classNames,
4735
+ locale: locale,
4736
+ overlayActive: overlayActive
4737
+ })
4738
+ }),
4739
+ chipListEditorOpen && popoverStyle && /*#__PURE__*/ reactDom.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.
4740
+ /*#__PURE__*/ jsxRuntime.jsx("div", {
4741
+ ref: popoverRef,
4742
+ className: classNames.expandOverlay,
4743
+ style: popoverStyle,
4744
+ onClick: (event)=>event.stopPropagation(),
4745
+ children: /*#__PURE__*/ jsxRuntime.jsx(ChipListEditor, {
4746
+ column: column,
4747
+ value: resolveEditValue(column, row, rawValue),
4748
+ pending: pending,
4749
+ onCommit: (value)=>commitEdit(row.id, column.key, value),
4750
+ classNames: classNames
4751
+ })
4752
+ }), document.body)
4753
+ ]
4385
4754
  });
4386
4755
  }
4387
4756
  /**
@@ -4421,12 +4790,18 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4421
4790
  onCommit: commit
4422
4791
  });
4423
4792
  } else if (column.editable && column.type === "chip-list") {
4424
- node = /*#__PURE__*/ jsxRuntime.jsx(ChipListEditor, {
4425
- column: column,
4426
- value: resolveEditValue(column, row, rawValue),
4427
- pending: pending,
4428
- onCommit: commit,
4429
- classNames: classNames
4793
+ // Feature B rework (#8) -- this column ALWAYS dispatches here to the plain
4794
+ // display (never to `DisplayCellContent`'s own read-only `chip-list` case,
4795
+ // never through the generic `isEditing` branch below): the click-to-edit
4796
+ // popover is a separate portal `DataTableBodyCell` renders alongside this
4797
+ // whole `<td>`, not a `node` this function returns -- see `isChipListEditable`'s
4798
+ // own doc comment (body-cells.tsx) for why it isn't anchored in place like
4799
+ // every other editor here.
4800
+ node = /*#__PURE__*/ jsxRuntime.jsx(ChipListCell, {
4801
+ value: rawValue,
4802
+ className: classNames.chipList,
4803
+ chipClassName: classNames.chip,
4804
+ overflowClassName: classNames.chipOverflow
4430
4805
  });
4431
4806
  } else if (column.editable && isEditing(rowId, column.key)) {
4432
4807
  if (column.editor === "select") {
@@ -4535,10 +4910,13 @@ function DisplayCellContent({ column, row, value, classNames, locale }) {
4535
4910
  className: classNames.chip
4536
4911
  });
4537
4912
  case "chip-list":
4913
+ // Non-editable chip-list only -- an editable one is fully handled by
4914
+ // `CellContent`'s own dedicated branch above and never reaches here.
4538
4915
  return /*#__PURE__*/ jsxRuntime.jsx(ChipListCell, {
4539
4916
  value: value,
4540
4917
  className: classNames.chipList,
4541
- chipClassName: classNames.chip
4918
+ chipClassName: classNames.chip,
4919
+ overflowClassName: classNames.chipOverflow
4542
4920
  });
4543
4921
  case "number":
4544
4922
  return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {