@crispy-seed/data-table 0.9.0 → 0.10.0

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
@@ -901,6 +901,40 @@ function useDataTableColumnCalc(props) {
901
901
  };
902
902
  }
903
903
 
904
+ function sameCell$1(a, id, key) {
905
+ return a !== null && a.id === id && a.key === key;
906
+ }
907
+ /**
908
+ * Tracks which single cell (if any) is showing its "show full content" overlay
909
+ * (DATATABLE-ATTIO-SPEC.md's cell-expand feature, #12) for the READ-ONLY case -- a
910
+ * non-`editable` column with truncated content, which has nothing to commit and so
911
+ * no reason to go through `useDataTableEdit`'s `editingCell`. An `editable` column's
912
+ * own truncated-cell overlay instead reuses `useDataTableEdit`'s `startEdit`/
913
+ * `editingCell` directly (the styled layer's `body-cells.tsx` picks which of the two
914
+ * a given activation opens) -- "for editable cells it's the editor" per the spec,
915
+ * not a second parallel state machine duplicating what editing already tracks.
916
+ *
917
+ * Only one cell can be peeked at a time, the same single-cell shape
918
+ * `useDataTableEdit`'s `editingCell` already uses -- this table has no multi-cell
919
+ * expand any more than it has multi-cell edit.
920
+ */ function useDataTableCellPeek() {
921
+ const [peekedCell, setPeekedCell] = React.useState(null);
922
+ const isPeeking = React.useCallback((id, key)=>sameCell$1(peekedCell, id, key), [
923
+ peekedCell
924
+ ]);
925
+ const openPeek = React.useCallback((id, key)=>setPeekedCell({
926
+ id,
927
+ key
928
+ }), []);
929
+ const closePeek = React.useCallback(()=>setPeekedCell(null), []);
930
+ return {
931
+ peekedCell,
932
+ isPeeking,
933
+ openPeek,
934
+ closePeek
935
+ };
936
+ }
937
+
904
938
  // Select's `dom.ts` reads state off REAL rendered elements (aria-disabled, data-value)
905
939
  // because its options register asynchronously through a floating-ui list and the DOM
906
940
  // is the one thing always in sync with `elementsRef`. DataTable's selection has no
@@ -1916,6 +1950,11 @@ function useDataTable(props) {
1916
1950
  const columnCalc = useDataTableColumnCalc(props);
1917
1951
  const scroll = useDataTableScroll();
1918
1952
  const edit = useDataTableEdit(props);
1953
+ // Feature C (#12) -- which single cell (if any) is showing its read-only "show
1954
+ // full content" overlay. Independent of `edit` above -- see
1955
+ // `useDataTableCellPeek.ts`'s own doc comment for why an `editable` column's
1956
+ // truncated-cell overlay reuses `edit` instead of this.
1957
+ const peek = useDataTableCellPeek();
1919
1958
  // Wired to `scroll`'s own scroll-container ref (not a fresh one of its own) so a
1920
1959
  // BOUNDED table's auto-load triggers off its own internal scroll, not the
1921
1960
  // browsing viewport -- see useDataTableInfiniteScroll.ts's own doc comment.
@@ -1933,6 +1972,7 @@ function useDataTable(props) {
1933
1972
  ...columnCalc,
1934
1973
  ...scroll,
1935
1974
  ...edit,
1975
+ ...peek,
1936
1976
  ...infiniteScroll,
1937
1977
  rootProps: domUtils.elementProps({
1938
1978
  role: "table"
@@ -2573,6 +2613,41 @@ function toneFor(value) {
2573
2613
  }
2574
2614
  return BADGE_TONES[hash % BADGE_TONES.length];
2575
2615
  }
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
+ 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"
2637
+ ];
2638
+ const CHIP_SWATCH_INK = "#000000";
2639
+ /** Same stable multiply-and-add hash `toneFor` above already uses over seed's 6
2640
+ * Badge tones, just over this file's own 12-swatch palette instead -- same value,
2641
+ * same swatch, every render (crisp's own `toneFor`, `datatable.tsx`). */ function swatchIndexFor(value) {
2642
+ let hash = 0;
2643
+ for(let index = 0; index < value.length; index++){
2644
+ hash = hash * 31 + value.charCodeAt(index) >>> 0;
2645
+ }
2646
+ return hash % CHIP_SWATCHES.length;
2647
+ }
2648
+ function swatchColorFor(value) {
2649
+ return CHIP_SWATCHES[swatchIndexFor(value)];
2650
+ }
2576
2651
  function initialsFor(name) {
2577
2652
  const parts = name.trim().split(/\s+/);
2578
2653
  return ((parts[0]?.[0] ?? "") + (parts[1]?.[0] ?? "")).toUpperCase() || "?";
@@ -2687,11 +2762,89 @@ function StatusCell({ value }) {
2687
2762
  children: status
2688
2763
  });
2689
2764
  }
2690
- function LinkCell({ column, value, className }) {
2765
+ /** 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
2767
+ * same "literal color as a computed prop" call `RatingCell`'s star fill already
2768
+ * makes. Shared by `ChipCell` (Feature A, single value) and `ChipListCell`/
2769
+ * editors.tsx's `ChipListEditor` (Feature B, multi value) so all three chip
2770
+ * renderings stay byte-identical. */ function SwatchChip({ value, className, children }) {
2771
+ return /*#__PURE__*/ jsxRuntime.jsxs("span", {
2772
+ className: className,
2773
+ style: {
2774
+ backgroundColor: swatchColorFor(value),
2775
+ color: CHIP_SWATCH_INK
2776
+ },
2777
+ children: [
2778
+ value,
2779
+ children
2780
+ ]
2781
+ });
2782
+ }
2783
+ /** `type: "chip"` (Feature A, #9) -- a single value rendered as one colored chip,
2784
+ * the color derived deterministically from the value string (`swatchColorFor`):
2785
+ * same value, same color, every render, every column. For a column like provider/
2786
+ * tenant/vendor/status/select where a plain `Badge` (seed's 6-tone `status`/`tags`
2787
+ * types above) doesn't need to be color-STABLE per value -- this one does. */ function ChipCell({ value, className }) {
2788
+ const text = value == null ? "" : String(value);
2789
+ if (!text) return null;
2790
+ return /*#__PURE__*/ jsxRuntime.jsx(SwatchChip, {
2791
+ value: text,
2792
+ className: className
2793
+ });
2794
+ }
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 }) {
2800
+ const tags = Array.isArray(value) ? value.filter((tag)=>!!tag) : [];
2801
+ if (!tags.length) return null;
2802
+ return /*#__PURE__*/ jsxRuntime.jsx("span", {
2803
+ className: className,
2804
+ children: tags.map((tag)=>/*#__PURE__*/ jsxRuntime.jsx(SwatchChip, {
2805
+ value: tag,
2806
+ className: chipClassName
2807
+ }, tag))
2808
+ });
2809
+ }
2810
+ /**
2811
+ * Feature D (#13) -- generic text+affix composition: `suffix` (if given) renders as
2812
+ * a `flex-shrink: 0` icon beside the text, and the TEXT itself (not the outer `<td>`)
2813
+ * gets its own `flex: 1 1 0%; min-width: 0` + ellipsis, so a long value truncates
2814
+ * while the affix stays visible instead of being squeezed out of the cell entirely
2815
+ * (the bug: composing text+icon inside a plain truncating `<td>` lets the icon
2816
+ * scroll/clip away with the text, since a flex child never inherits its PARENT's
2817
+ * own `text-overflow`). `data-seed-truncate` on the text span is what `truncate.ts`'s
2818
+ * `isCellTruncated` (Feature C's click-to-expand gate) measures instead of the
2819
+ * `<td>` itself for exactly the same reason -- see that file's own comment.
2820
+ * Any FUTURE cell composing a fixed leading/trailing glyph beside truncating text
2821
+ * should render through this component rather than re-deriving the same three-slot
2822
+ * flex shape.
2823
+ */ function CellWithSuffix({ text, suffix, rowClassName, textClassName, suffixClassName }) {
2824
+ if (!suffix) return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
2825
+ children: text
2826
+ });
2827
+ return /*#__PURE__*/ jsxRuntime.jsxs("span", {
2828
+ className: rowClassName,
2829
+ children: [
2830
+ /*#__PURE__*/ jsxRuntime.jsx("span", {
2831
+ className: textClassName,
2832
+ "data-seed-truncate": "",
2833
+ children: text
2834
+ }),
2835
+ /*#__PURE__*/ jsxRuntime.jsx("span", {
2836
+ className: suffixClassName,
2837
+ children: suffix
2838
+ })
2839
+ ]
2840
+ });
2841
+ }
2842
+ function LinkCell({ column, value, row, className, rowClassName, textClassName, suffixClassName }) {
2691
2843
  const text = value == null ? "" : String(value);
2692
2844
  if (!text) return null;
2693
2845
  const isUrl = column.type === "url";
2694
2846
  const href = isUrl ? /^https?:\/\//.test(text) ? text : `https://${text}` : `mailto:${text}`;
2847
+ const suffix = column.suffixIcon?.(row);
2695
2848
  return /*#__PURE__*/ jsxRuntime.jsx("a", {
2696
2849
  className: className,
2697
2850
  href: href,
@@ -2700,7 +2853,13 @@ function LinkCell({ column, value, className }) {
2700
2853
  // A link cell points somewhere other than "select this row" -- its own
2701
2854
  // navigation must not also fire a row click handler.
2702
2855
  onClick: (event)=>event.stopPropagation(),
2703
- children: text
2856
+ children: /*#__PURE__*/ jsxRuntime.jsx(CellWithSuffix, {
2857
+ text: text,
2858
+ suffix: suffix,
2859
+ rowClassName: rowClassName,
2860
+ textClassName: textClassName,
2861
+ suffixClassName: suffixClassName
2862
+ })
2704
2863
  });
2705
2864
  }
2706
2865
  // Attio's own measured 5-point star path (crisp's `RATING_STAR_D`, traced from the
@@ -2726,8 +2885,7 @@ function RatingCell({ value, maxRating = 5 }) {
2726
2885
  },
2727
2886
  children: Array.from({
2728
2887
  length: maxRating
2729
- }, (_, index)=>// biome-ignore lint/suspicious/noArrayIndexKey: a fixed-length star row has no other identity
2730
- /*#__PURE__*/ jsxRuntime.jsx("svg", {
2888
+ }, (_, index)=>/*#__PURE__*/ jsxRuntime.jsx("svg", {
2731
2889
  width: 16,
2732
2890
  height: 16,
2733
2891
  viewBox: "0 0 16 16",
@@ -3218,7 +3376,7 @@ const TextFieldTextarea = /*#__PURE__*/ React.forwardRef(({ className, autoresiz
3218
3376
  });
3219
3377
  TextFieldTextarea.displayName = "TextFieldTextarea";
3220
3378
 
3221
- __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__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))}");
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))}");
3222
3380
 
3223
3381
  const createClassName = (className, variants, compoundVariants = [])=>{
3224
3382
  const variantKeys = Object.keys(variants);
@@ -3313,6 +3471,42 @@ const dataTableSlotNames = [
3313
3471
  "link",
3314
3472
  "seed-data-table__link"
3315
3473
  ],
3474
+ [
3475
+ "chip",
3476
+ "seed-data-table__chip"
3477
+ ],
3478
+ [
3479
+ "chipList",
3480
+ "seed-data-table__chipList"
3481
+ ],
3482
+ [
3483
+ "chipRemove",
3484
+ "seed-data-table__chipRemove"
3485
+ ],
3486
+ [
3487
+ "chipInput",
3488
+ "seed-data-table__chipInput"
3489
+ ],
3490
+ [
3491
+ "expandOverlay",
3492
+ "seed-data-table__expandOverlay"
3493
+ ],
3494
+ [
3495
+ "expandOverlayText",
3496
+ "seed-data-table__expandOverlayText"
3497
+ ],
3498
+ [
3499
+ "cellSuffixRow",
3500
+ "seed-data-table__cellSuffixRow"
3501
+ ],
3502
+ [
3503
+ "cellSuffixText",
3504
+ "seed-data-table__cellSuffixText"
3505
+ ],
3506
+ [
3507
+ "cellSuffixIcon",
3508
+ "seed-data-table__cellSuffixIcon"
3509
+ ],
3316
3510
  [
3317
3511
  "entityActions",
3318
3512
  "seed-data-table__entityActions"
@@ -3887,6 +4081,103 @@ function RatingEditor({ column, value, pending, onCommit }) {
3887
4081
  ]
3888
4082
  });
3889
4083
  }
4084
+ ////////////////////////////////////////////////////////////////////////////////////
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
4095
+ // 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.
4099
+ function ChipListEditor({ column, value, pending, onCommit, classNames }) {
4100
+ const tags = Array.isArray(value) ? value.filter((tag)=>!!tag) : [];
4101
+ const [draft, setDraft] = React__namespace.useState("");
4102
+ function commitTags(next) {
4103
+ onCommit(JSON.stringify(next));
4104
+ }
4105
+ function addTag() {
4106
+ const tag = draft.trim();
4107
+ setDraft("");
4108
+ if (!tag || tags.includes(tag)) return;
4109
+ commitTags([
4110
+ ...tags,
4111
+ tag
4112
+ ]);
4113
+ }
4114
+ function removeTag(tag) {
4115
+ commitTags(tags.filter((existing)=>existing !== tag));
4116
+ }
4117
+ 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
+ /*#__PURE__*/ jsxRuntime.jsxs("span", {
4119
+ className: classNames.chipList,
4120
+ onClick: (event)=>event.stopPropagation(),
4121
+ children: [
4122
+ tags.map((tag)=>/*#__PURE__*/ jsxRuntime.jsx(SwatchChip, {
4123
+ value: tag,
4124
+ className: classNames.chip,
4125
+ children: /*#__PURE__*/ jsxRuntime.jsx("button", {
4126
+ type: "button",
4127
+ disabled: pending,
4128
+ "aria-label": `Remove ${tag}`,
4129
+ className: classNames.chipRemove,
4130
+ onClick: ()=>removeTag(tag),
4131
+ children: /*#__PURE__*/ jsxRuntime.jsx(IconXmarkLine_default, {
4132
+ size: 10
4133
+ })
4134
+ })
4135
+ }, tag)),
4136
+ /*#__PURE__*/ jsxRuntime.jsx("input", {
4137
+ className: classNames.chipInput,
4138
+ value: draft,
4139
+ disabled: pending,
4140
+ placeholder: tags.length ? "" : "Add…",
4141
+ "aria-label": `Add tag to ${column.key}`,
4142
+ onChange: (event)=>setDraft(event.target.value),
4143
+ onKeyDown: (event)=>{
4144
+ event.stopPropagation();
4145
+ if (event.key === "Enter") {
4146
+ event.preventDefault();
4147
+ addTag();
4148
+ } else if (event.key === "Backspace" && draft === "" && tags.length > 0) {
4149
+ removeTag(tags[tags.length - 1]);
4150
+ } else if (event.key === "Escape") {
4151
+ setDraft("");
4152
+ }
4153
+ }
4154
+ }),
4155
+ pending && /*#__PURE__*/ jsxRuntime.jsx(EditPendingIndicator, {})
4156
+ ]
4157
+ }));
4158
+ }
4159
+ ////////////////////////////////////////////////////////////////////////////////////
4160
+ // -- Feature C (#12): cell-expand "show full content" overlay -- an anchored box
4161
+ // (position:absolute over the cell it belongs to, grows in HEIGHT, elevated)
4162
+ // that wraps whatever a truncated cell would otherwise show inline: an editable
4163
+ // column's OWN editor node (unchanged, just relocated into this box instead of
4164
+ // rendered flush in the 36px row) for the editable case, or the plain full value
4165
+ // for a read-only one. `body-cells.tsx` decides WHEN to render this (a click on a
4166
+ // cell whose content is actually truncated, `truncate.ts`'s `isCellTruncated`);
4167
+ // this component only supplies the box.
4168
+ function CellExpandOverlay({ className, children }) {
4169
+ return(// `stopPropagation` -- an overlay sits on top of (and past) its own cell/row;
4170
+ // a click anywhere inside it must never ALSO fire the row's own `onRowClick`,
4171
+ // the same guard every other interactive cell affordance in this directory
4172
+ // already applies (`LinkCell`, `EntityCell`'s preview button, editors.tsx's
4173
+ // `TextEditor` root).
4174
+ // biome-ignore lint/a11y/noStaticElementInteractions: not a widget -- this div never responds to interaction on ITSELF, only stops propagation of events that already occurred on a real interactive descendant (the editor/text it wraps). No role fits it because it isn't a control.
4175
+ /*#__PURE__*/ jsxRuntime.jsx("div", {
4176
+ className: className,
4177
+ onClick: (event)=>event.stopPropagation(),
4178
+ children: children
4179
+ }));
4180
+ }
3890
4181
 
3891
4182
  // Row-narrowing for the toolbar's search box -- split out of DataTable.tsx the way
3892
4183
  // cells.tsx splits out cell-type rendering: a self-contained, independently testable
@@ -3926,49 +4217,167 @@ function RatingEditor({ column, value, pending, onCommit }) {
3926
4217
  return data.filter((row)=>searchableColumns.some((column)=>matchesSearch(getColumnValue(column, row), query)));
3927
4218
  }
3928
4219
 
4220
+ // Feature C (#12) -- the click-to-expand gate: measures whether a cell's rendered
4221
+ // content is ACTUALLY truncated right now, so a single click only opens the
4222
+ // anchored overlay (editors.tsx's `CellExpandOverlay`) when there's real hidden
4223
+ // content to show, never on a short value that already fits. Isolated out of
4224
+ // body-cells.tsx the same way select-search.tsx/filtering.ts already split their own
4225
+ // concerns out of the styled layer, so this one pure DOM read is unit-testable on
4226
+ // its own (a plain `<td>`'s own truncate CSS vs. a suffix-icon cell's nested flex
4227
+ // child need two different elements measured -- see below).
4228
+ //
4229
+ // This one genuinely reads a real, laid-out DOM element (`scrollWidth`/
4230
+ // `clientWidth`), unlike react-headless/data-table/src/dom.ts's own helpers (plain
4231
+ // data in, plain data out) -- it lives in the REACT (styled) package rather than
4232
+ // react-headless for that reason: measuring actual layout is a rendering concern,
4233
+ // not table state.
4234
+ function isCellTruncated(cell) {
4235
+ // The common case: a plain cell's own `white-space: nowrap; overflow: clip` (the
4236
+ // `cell` recipe slot) makes the `<td>` ITSELF overflow once its text is longer
4237
+ // than the column is wide.
4238
+ if (cell.scrollWidth > cell.clientWidth) return true;
4239
+ // A cell composing a suffix icon beside its text (cells.tsx's `CellWithSuffix`,
4240
+ // Feature D) wraps the text in its OWN `flex: 1 1 0%; min-width: 0` child instead
4241
+ // -- that flex child never lets the `<td>` itself overflow (it's constrained to
4242
+ // the remaining row width by construction), so the truncating element to measure
4243
+ // is the flex child, marked `data-seed-truncate` by whichever cell renderer
4244
+ // applies its own ellipsis (`CellWithSuffix`'s text span today; any future
4245
+ // truncating inner element should carry the same marker).
4246
+ const targets = cell.querySelectorAll("[data-seed-truncate]");
4247
+ for (const target of targets){
4248
+ if (target.scrollWidth > target.clientWidth) return true;
4249
+ }
4250
+ return false;
4251
+ }
4252
+
3929
4253
  /** `editable` + one of these shapes needs no double-click gate and no separate rest
3930
4254
  * display -- interacting with the widget itself IS the commit gesture (crisp's own
3931
4255
  * #453/#760/#761: `switch`/`rating`/`checkbox` never had a meaningfully different
3932
- * "rest" state to show). Every other `editable` column opens on double-click. */ function isAlwaysLiveEditable(column) {
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) {
3933
4260
  if (!column.editable) return false;
3934
- return column.editor === "switch" || column.type === "rating" || column.type === "checkbox";
4261
+ return column.editor === "switch" || column.type === "rating" || column.type === "checkbox" || column.type === "chip-list";
3935
4262
  }
3936
4263
  function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyFirst, stickyLast }) {
3937
4264
  const effective = useEffectiveColumns(columns, {
3938
4265
  selectable,
3939
4266
  stickyFirst
3940
4267
  });
3941
- const { startEdit } = useDataTableContext();
3942
4268
  return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
3943
4269
  children: effective.map((entry, index)=>{
3944
4270
  const { column } = entry;
3945
4271
  const isLast = index === effective.length - 1;
3946
4272
  const sticky = pinnedCellProps(entry, stickyLast, isLast);
3947
- // A gated `editable` column (everything except the always-live widgets)
3948
- // opens on double-click -- Attio's own measured gesture (crisp's #411),
3949
- // matched here rather than a single click, which stays "select this cell"
3950
- // in crisp's own spreadsheet model and would otherwise be indistinguishable
3951
- // from this table's plain row click.
3952
- const gated = column.editable && !isAlwaysLiveEditable(column);
3953
- return /*#__PURE__*/ jsxRuntime.jsx(DataTableCell, {
3954
- "data-numeric": domUtils.dataAttr(column.type === "number" || column.type === "currency"),
3955
- "data-editable": domUtils.dataAttr(!!column.editable),
3956
- "data-sticky": sticky["data-sticky"],
3957
- "data-sticky-edge": sticky["data-sticky-edge"],
3958
- "data-sticky-edge-end": sticky["data-sticky-edge-end"],
3959
- style: sticky.style,
3960
- onDoubleClick: gated ? ()=>startEdit(row.id, column.key) : undefined,
3961
- children: /*#__PURE__*/ jsxRuntime.jsx(CellContent, {
3962
- column: column,
3963
- row: row,
3964
- rowId: row.id,
3965
- classNames: classNames,
3966
- locale: locale
3967
- })
4273
+ return /*#__PURE__*/ jsxRuntime.jsx(DataTableBodyCell, {
4274
+ column: column,
4275
+ row: row,
4276
+ classNames: classNames,
4277
+ locale: locale,
4278
+ sticky: sticky
3968
4279
  }, column.key);
3969
4280
  })
3970
4281
  });
3971
4282
  }
4283
+ /**
4284
+ * One `<td>` -- split out of `ColumnBodyCells`'s own map (which now only computes
4285
+ * per-column LAYOUT, `pinnedCellProps`) so this component can own its own per-cell
4286
+ * "is the Feature C overlay open" local state (`overlayOpen`) without re-deriving it
4287
+ * on every row's render. React keeps this instance stable across re-renders because
4288
+ * `ColumnBodyCells` keys it by `column.key` the same way it always keyed the bare
4289
+ * `<DataTableCell>` before this split.
4290
+ */ function DataTableBodyCell({ column, row, classNames, locale, sticky }) {
4291
+ const { startEdit, isEditing, isPeeking, openPeek, closePeek } = useDataTableContext();
4292
+ const cellRef = React__namespace.useRef(null);
4293
+ // Feature C (#12) -- whether THIS activation (click or double-click) should
4294
+ // render as the anchored overlay rather than plain inline. Frozen at the moment
4295
+ // of activation (re-measured every time, never left stale) rather than derived
4296
+ // from `isEditing`/`isPeeking` alone, because those two states don't carry "was
4297
+ // the content actually truncated when this opened" on their own -- a SHORT
4298
+ // editable cell's existing double-click-to-edit (tranche 5, unchanged) must keep
4299
+ // rendering exactly as it always has, not suddenly gain overlay chrome.
4300
+ const [overlayOpen, setOverlayOpen] = React__namespace.useState(false);
4301
+ 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;
4310
+ function handleDoubleClick(event) {
4311
+ if (!gated) return;
4312
+ setOverlayOpen(isCellTruncated(event.currentTarget));
4313
+ startEdit(row.id, column.key);
4314
+ }
4315
+ function handleClick(event) {
4316
+ if (!isCellTruncated(event.currentTarget)) return;
4317
+ if (gated) {
4318
+ setOverlayOpen(true);
4319
+ startEdit(row.id, column.key);
4320
+ } else if (alwaysLive) {
4321
+ // No separate open/edit state to start -- the always-live widget already
4322
+ // 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.
4324
+ setOverlayOpen(true);
4325
+ } else {
4326
+ setOverlayOpen(true);
4327
+ openPeek(row.id, column.key);
4328
+ }
4329
+ }
4330
+ // Closes the overlay on Escape or an outside click/pointerdown -- a gated
4331
+ // editor's OWN Escape/blur handling already ends `isEditing` on its own (see
4332
+ // `TextEditor`/`SelectEditor`'s existing onCancel/onOpenChange wiring), which
4333
+ // alone is enough to hide a GATED cell's overlay (its render below is gated on
4334
+ // `isEditing` too); this listener is what the PEEK and ALWAYS-LIVE cases need,
4335
+ // since neither has any other mechanism that ever calls `closePeek`/resets
4336
+ // `overlayOpen` on its own.
4337
+ React__namespace.useEffect(()=>{
4338
+ if (!overlayOpen) return;
4339
+ function handlePointerDown(event) {
4340
+ if (cellRef.current && !cellRef.current.contains(event.target)) {
4341
+ setOverlayOpen(false);
4342
+ closePeek();
4343
+ }
4344
+ }
4345
+ function handleKeyDown(event) {
4346
+ if (event.key !== "Escape") return;
4347
+ setOverlayOpen(false);
4348
+ closePeek();
4349
+ }
4350
+ document.addEventListener("pointerdown", handlePointerDown);
4351
+ document.addEventListener("keydown", handleKeyDown);
4352
+ return ()=>{
4353
+ document.removeEventListener("pointerdown", handlePointerDown);
4354
+ document.removeEventListener("keydown", handleKeyDown);
4355
+ };
4356
+ }, [
4357
+ overlayOpen,
4358
+ closePeek
4359
+ ]);
4360
+ const overlayActive = overlayOpen && (alwaysLive || isEditing(row.id, column.key) || isPeeking(row.id, column.key));
4361
+ return /*#__PURE__*/ jsxRuntime.jsx(DataTableCell, {
4362
+ ref: cellRef,
4363
+ "data-numeric": domUtils.dataAttr(column.type === "number" || column.type === "currency"),
4364
+ "data-editable": domUtils.dataAttr(!!column.editable),
4365
+ "data-sticky": sticky["data-sticky"],
4366
+ "data-sticky-edge": sticky["data-sticky-edge"],
4367
+ "data-sticky-edge-end": sticky["data-sticky-edge-end"],
4368
+ style: sticky.style,
4369
+ onClick: handleClick,
4370
+ onDoubleClick: handleDoubleClick,
4371
+ children: /*#__PURE__*/ jsxRuntime.jsx(CellContent, {
4372
+ column: column,
4373
+ row: row,
4374
+ rowId: row.id,
4375
+ classNames: classNames,
4376
+ locale: locale,
4377
+ overlayActive: overlayActive
4378
+ })
4379
+ });
4380
+ }
3972
4381
  /**
3973
4382
  * Dispatches to whichever of cells.tsx's static renderers or editors.tsx's live
3974
4383
  * editors applies to THIS column/row right now -- always-live first (no rest state to
@@ -3976,7 +4385,7 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
3976
4385
  * display (a custom `render` wins there, same as before `editable` existed; it never
3977
4386
  * overrides an actual editing/always-live state, matching crisp's own split between
3978
4387
  * `renderTyped`'s rest-state branch and the editing branches above it).
3979
- */ function CellContent({ column, row, rowId, classNames, locale }) {
4388
+ */ function CellContent({ column, row, rowId, classNames, locale, overlayActive }) {
3980
4389
  const { isEditing, isPending, getEditError, commitEdit, cancelEdit } = useDataTableContext();
3981
4390
  const rawValue = getColumnValue(column, row);
3982
4391
  const pending = isPending(rowId, column.key);
@@ -4005,6 +4414,14 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4005
4414
  pending: pending,
4006
4415
  onCommit: commit
4007
4416
  });
4417
+ } else if (column.editable && column.type === "chip-list") {
4418
+ node = /*#__PURE__*/ jsxRuntime.jsx(ChipListEditor, {
4419
+ column: column,
4420
+ value: resolveEditValue(column, row, rawValue),
4421
+ pending: pending,
4422
+ onCommit: commit,
4423
+ classNames: classNames
4424
+ });
4008
4425
  } else if (column.editable && isEditing(rowId, column.key)) {
4009
4426
  if (column.editor === "select") {
4010
4427
  node = /*#__PURE__*/ jsxRuntime.jsx(SelectEditor, {
@@ -4040,6 +4457,10 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4040
4457
  });
4041
4458
  }
4042
4459
  } else {
4460
+ // Feature C (#12), read-only case: `DataTableBodyCell`'s own `isPeeking` already
4461
+ // decided whether the overlay should be open (folded into `overlayActive`
4462
+ // below) -- this branch always renders the SAME `DisplayCellContent` either
4463
+ // way, just possibly relocated into the overlay box.
4043
4464
  node = /*#__PURE__*/ jsxRuntime.jsx(DisplayCellContent, {
4044
4465
  column: column,
4045
4466
  row: row,
@@ -4050,7 +4471,13 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4050
4471
  }
4051
4472
  return /*#__PURE__*/ jsxRuntime.jsxs(jsxRuntime.Fragment, {
4052
4473
  children: [
4053
- node,
4474
+ overlayActive ? /*#__PURE__*/ jsxRuntime.jsx(CellExpandOverlay, {
4475
+ className: classNames.expandOverlay,
4476
+ children: column.editable ? node : /*#__PURE__*/ jsxRuntime.jsx("span", {
4477
+ className: classNames.expandOverlayText,
4478
+ children: node
4479
+ })
4480
+ }) : node,
4054
4481
  error && /*#__PURE__*/ jsxRuntime.jsx(EditError, {
4055
4482
  message: error
4056
4483
  })
@@ -4081,7 +4508,11 @@ function DisplayCellContent({ column, row, value, classNames, locale }) {
4081
4508
  return /*#__PURE__*/ jsxRuntime.jsx(LinkCell, {
4082
4509
  column: column,
4083
4510
  value: value,
4084
- className: classNames.link
4511
+ row: row,
4512
+ className: classNames.link,
4513
+ rowClassName: classNames.cellSuffixRow,
4514
+ textClassName: classNames.cellSuffixText,
4515
+ suffixClassName: classNames.cellSuffixIcon
4085
4516
  });
4086
4517
  case "tags":
4087
4518
  return /*#__PURE__*/ jsxRuntime.jsx(TagsCell, {
@@ -4092,6 +4523,17 @@ function DisplayCellContent({ column, row, value, classNames, locale }) {
4092
4523
  return /*#__PURE__*/ jsxRuntime.jsx(StatusCell, {
4093
4524
  value: value
4094
4525
  });
4526
+ case "chip":
4527
+ return /*#__PURE__*/ jsxRuntime.jsx(ChipCell, {
4528
+ value: value,
4529
+ className: classNames.chip
4530
+ });
4531
+ case "chip-list":
4532
+ return /*#__PURE__*/ jsxRuntime.jsx(ChipListCell, {
4533
+ value: value,
4534
+ className: classNames.chipList,
4535
+ chipClassName: classNames.chip
4536
+ });
4095
4537
  case "number":
4096
4538
  return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
4097
4539
  children: formatNumberCell(value, locale)
@@ -4118,9 +4560,23 @@ function DisplayCellContent({ column, row, value, classNames, locale }) {
4118
4560
  value: value
4119
4561
  });
4120
4562
  default:
4121
- return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
4122
- children: value == null ? null : String(value)
4123
- });
4563
+ {
4564
+ const text = value == null ? null : String(value);
4565
+ const suffix = column.suffixIcon?.(row);
4566
+ // Feature D (#13) -- a plain text column carrying a `suffixIcon` (e.g. a
4567
+ // location column's pin glyph) composes through the same `CellWithSuffix`
4568
+ // LinkCell already uses, so the icon never gets squeezed out by a long value.
4569
+ if (!suffix || text == null) return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
4570
+ children: text
4571
+ });
4572
+ return /*#__PURE__*/ jsxRuntime.jsx(CellWithSuffix, {
4573
+ text: text,
4574
+ suffix: suffix,
4575
+ rowClassName: classNames.cellSuffixRow,
4576
+ textClassName: classNames.cellSuffixText,
4577
+ suffixClassName: classNames.cellSuffixIcon
4578
+ });
4579
+ }
4124
4580
  }
4125
4581
  }
4126
4582