@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.js CHANGED
@@ -10,7 +10,7 @@ function __insertCSS(code) {
10
10
 
11
11
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
12
12
  import * as React from 'react';
13
- import React__default, { forwardRef, useCallback, useMemo, useState, useRef, useEffect, createContext, useContext, useId } from 'react';
13
+ import React__default, { forwardRef, useCallback, useState, useMemo, useRef, useEffect, createContext, useContext, useId } from 'react';
14
14
  import { dataAttr, elementProps, ariaAttr, mergeProps as mergeProps$1, buttonProps } from '@seed-design/dom-utils';
15
15
  import { checkbox } from '@seed-design/css/recipes/checkbox';
16
16
  import { checkmark } from '@seed-design/css/recipes/checkmark';
@@ -882,6 +882,40 @@ function useDataTableColumnCalc(props) {
882
882
  };
883
883
  }
884
884
 
885
+ function sameCell$1(a, id, key) {
886
+ return a !== null && a.id === id && a.key === key;
887
+ }
888
+ /**
889
+ * Tracks which single cell (if any) is showing its "show full content" overlay
890
+ * (DATATABLE-ATTIO-SPEC.md's cell-expand feature, #12) for the READ-ONLY case -- a
891
+ * non-`editable` column with truncated content, which has nothing to commit and so
892
+ * no reason to go through `useDataTableEdit`'s `editingCell`. An `editable` column's
893
+ * own truncated-cell overlay instead reuses `useDataTableEdit`'s `startEdit`/
894
+ * `editingCell` directly (the styled layer's `body-cells.tsx` picks which of the two
895
+ * a given activation opens) -- "for editable cells it's the editor" per the spec,
896
+ * not a second parallel state machine duplicating what editing already tracks.
897
+ *
898
+ * Only one cell can be peeked at a time, the same single-cell shape
899
+ * `useDataTableEdit`'s `editingCell` already uses -- this table has no multi-cell
900
+ * expand any more than it has multi-cell edit.
901
+ */ function useDataTableCellPeek() {
902
+ const [peekedCell, setPeekedCell] = useState(null);
903
+ const isPeeking = useCallback((id, key)=>sameCell$1(peekedCell, id, key), [
904
+ peekedCell
905
+ ]);
906
+ const openPeek = useCallback((id, key)=>setPeekedCell({
907
+ id,
908
+ key
909
+ }), []);
910
+ const closePeek = useCallback(()=>setPeekedCell(null), []);
911
+ return {
912
+ peekedCell,
913
+ isPeeking,
914
+ openPeek,
915
+ closePeek
916
+ };
917
+ }
918
+
885
919
  // Select's `dom.ts` reads state off REAL rendered elements (aria-disabled, data-value)
886
920
  // because its options register asynchronously through a floating-ui list and the DOM
887
921
  // is the one thing always in sync with `elementsRef`. DataTable's selection has no
@@ -1897,6 +1931,11 @@ function useDataTable(props) {
1897
1931
  const columnCalc = useDataTableColumnCalc(props);
1898
1932
  const scroll = useDataTableScroll();
1899
1933
  const edit = useDataTableEdit(props);
1934
+ // Feature C (#12) -- which single cell (if any) is showing its read-only "show
1935
+ // full content" overlay. Independent of `edit` above -- see
1936
+ // `useDataTableCellPeek.ts`'s own doc comment for why an `editable` column's
1937
+ // truncated-cell overlay reuses `edit` instead of this.
1938
+ const peek = useDataTableCellPeek();
1900
1939
  // Wired to `scroll`'s own scroll-container ref (not a fresh one of its own) so a
1901
1940
  // BOUNDED table's auto-load triggers off its own internal scroll, not the
1902
1941
  // browsing viewport -- see useDataTableInfiniteScroll.ts's own doc comment.
@@ -1914,6 +1953,7 @@ function useDataTable(props) {
1914
1953
  ...columnCalc,
1915
1954
  ...scroll,
1916
1955
  ...edit,
1956
+ ...peek,
1917
1957
  ...infiniteScroll,
1918
1958
  rootProps: elementProps({
1919
1959
  role: "table"
@@ -2554,6 +2594,41 @@ function toneFor(value) {
2554
2594
  }
2555
2595
  return BADGE_TONES[hash % BADGE_TONES.length];
2556
2596
  }
2597
+ // Feature A/B (#9/#8) -- Attio's own flat 12-swatch chip palette, ported from
2598
+ // crisp's `tokens/palette.ts` (`tagSwatch1..12`, measured live off Attio) rather
2599
+ // than seed's own 6-tone `Badge` (`BADGE_TONES` above): a swatch, unlike a Badge
2600
+ // tone, has no ring/border and its ink is a FLAT black on every swatch (crisp's own
2601
+ // `tagSwatchInk`, `#000000` -- every swatch background is already a pale tint, so
2602
+ // one dark foreground reads on all 12), not a per-tone contrast color. No seed token
2603
+ // lands on any of these 12 hex values, so -- same footnote as the qvism recipe's own
2604
+ // `ATTIO_BRAND_BLUE`/`RATING_FILLED` literals -- they stay literal here too.
2605
+ const CHIP_SWATCHES = [
2606
+ "#fff3cc",
2607
+ "#f4fbcb",
2608
+ "#fdf7c4",
2609
+ "#feecf3",
2610
+ "#e0fced",
2611
+ "#ddf7ff",
2612
+ "#f6f7f7",
2613
+ "#ffebeb",
2614
+ "#feeee1",
2615
+ "#e5eeff",
2616
+ "#f5f0ff",
2617
+ "#fdedff"
2618
+ ];
2619
+ const CHIP_SWATCH_INK = "#000000";
2620
+ /** Same stable multiply-and-add hash `toneFor` above already uses over seed's 6
2621
+ * Badge tones, just over this file's own 12-swatch palette instead -- same value,
2622
+ * same swatch, every render (crisp's own `toneFor`, `datatable.tsx`). */ function swatchIndexFor(value) {
2623
+ let hash = 0;
2624
+ for(let index = 0; index < value.length; index++){
2625
+ hash = hash * 31 + value.charCodeAt(index) >>> 0;
2626
+ }
2627
+ return hash % CHIP_SWATCHES.length;
2628
+ }
2629
+ function swatchColorFor(value) {
2630
+ return CHIP_SWATCHES[swatchIndexFor(value)];
2631
+ }
2557
2632
  function initialsFor(name) {
2558
2633
  const parts = name.trim().split(/\s+/);
2559
2634
  return ((parts[0]?.[0] ?? "") + (parts[1]?.[0] ?? "")).toUpperCase() || "?";
@@ -2668,11 +2743,89 @@ function StatusCell({ value }) {
2668
2743
  children: status
2669
2744
  });
2670
2745
  }
2671
- function LinkCell({ column, value, className }) {
2746
+ /** One swatch chip -- `style` carries the per-value background/ink (computed by
2747
+ * `swatchColorFor`/`CHIP_SWATCH_INK` above) since neither is a recipe token, the
2748
+ * same "literal color as a computed prop" call `RatingCell`'s star fill already
2749
+ * makes. Shared by `ChipCell` (Feature A, single value) and `ChipListCell`/
2750
+ * editors.tsx's `ChipListEditor` (Feature B, multi value) so all three chip
2751
+ * renderings stay byte-identical. */ function SwatchChip({ value, className, children }) {
2752
+ return /*#__PURE__*/ jsxs("span", {
2753
+ className: className,
2754
+ style: {
2755
+ backgroundColor: swatchColorFor(value),
2756
+ color: CHIP_SWATCH_INK
2757
+ },
2758
+ children: [
2759
+ value,
2760
+ children
2761
+ ]
2762
+ });
2763
+ }
2764
+ /** `type: "chip"` (Feature A, #9) -- a single value rendered as one colored chip,
2765
+ * the color derived deterministically from the value string (`swatchColorFor`):
2766
+ * same value, same color, every render, every column. For a column like provider/
2767
+ * tenant/vendor/status/select where a plain `Badge` (seed's 6-tone `status`/`tags`
2768
+ * types above) doesn't need to be color-STABLE per value -- this one does. */ function ChipCell({ value, className }) {
2769
+ const text = value == null ? "" : String(value);
2770
+ if (!text) return null;
2771
+ return /*#__PURE__*/ jsx(SwatchChip, {
2772
+ value: text,
2773
+ className: className
2774
+ });
2775
+ }
2776
+ /** `type: "chip-list"`, READ-ONLY (Feature B, #8) -- multiple swatch chips, no add/
2777
+ * remove affordances. The editable case is editors.tsx's `ChipListEditor`, an
2778
+ * always-live widget (interacting with it IS the commit, the same `switch`/
2779
+ * `rating`/`checkbox` shape `body-cells.tsx`'s `isAlwaysLiveEditable` already
2780
+ * covers) -- this component only ever renders the REST display. */ function ChipListCell({ value, className, chipClassName }) {
2781
+ const tags = Array.isArray(value) ? value.filter((tag)=>!!tag) : [];
2782
+ if (!tags.length) return null;
2783
+ return /*#__PURE__*/ jsx("span", {
2784
+ className: className,
2785
+ children: tags.map((tag)=>/*#__PURE__*/ jsx(SwatchChip, {
2786
+ value: tag,
2787
+ className: chipClassName
2788
+ }, tag))
2789
+ });
2790
+ }
2791
+ /**
2792
+ * Feature D (#13) -- generic text+affix composition: `suffix` (if given) renders as
2793
+ * a `flex-shrink: 0` icon beside the text, and the TEXT itself (not the outer `<td>`)
2794
+ * gets its own `flex: 1 1 0%; min-width: 0` + ellipsis, so a long value truncates
2795
+ * while the affix stays visible instead of being squeezed out of the cell entirely
2796
+ * (the bug: composing text+icon inside a plain truncating `<td>` lets the icon
2797
+ * scroll/clip away with the text, since a flex child never inherits its PARENT's
2798
+ * own `text-overflow`). `data-seed-truncate` on the text span is what `truncate.ts`'s
2799
+ * `isCellTruncated` (Feature C's click-to-expand gate) measures instead of the
2800
+ * `<td>` itself for exactly the same reason -- see that file's own comment.
2801
+ * Any FUTURE cell composing a fixed leading/trailing glyph beside truncating text
2802
+ * should render through this component rather than re-deriving the same three-slot
2803
+ * flex shape.
2804
+ */ function CellWithSuffix({ text, suffix, rowClassName, textClassName, suffixClassName }) {
2805
+ if (!suffix) return /*#__PURE__*/ jsx(Fragment, {
2806
+ children: text
2807
+ });
2808
+ return /*#__PURE__*/ jsxs("span", {
2809
+ className: rowClassName,
2810
+ children: [
2811
+ /*#__PURE__*/ jsx("span", {
2812
+ className: textClassName,
2813
+ "data-seed-truncate": "",
2814
+ children: text
2815
+ }),
2816
+ /*#__PURE__*/ jsx("span", {
2817
+ className: suffixClassName,
2818
+ children: suffix
2819
+ })
2820
+ ]
2821
+ });
2822
+ }
2823
+ function LinkCell({ column, value, row, className, rowClassName, textClassName, suffixClassName }) {
2672
2824
  const text = value == null ? "" : String(value);
2673
2825
  if (!text) return null;
2674
2826
  const isUrl = column.type === "url";
2675
2827
  const href = isUrl ? /^https?:\/\//.test(text) ? text : `https://${text}` : `mailto:${text}`;
2828
+ const suffix = column.suffixIcon?.(row);
2676
2829
  return /*#__PURE__*/ jsx("a", {
2677
2830
  className: className,
2678
2831
  href: href,
@@ -2681,7 +2834,13 @@ function LinkCell({ column, value, className }) {
2681
2834
  // A link cell points somewhere other than "select this row" -- its own
2682
2835
  // navigation must not also fire a row click handler.
2683
2836
  onClick: (event)=>event.stopPropagation(),
2684
- children: text
2837
+ children: /*#__PURE__*/ jsx(CellWithSuffix, {
2838
+ text: text,
2839
+ suffix: suffix,
2840
+ rowClassName: rowClassName,
2841
+ textClassName: textClassName,
2842
+ suffixClassName: suffixClassName
2843
+ })
2685
2844
  });
2686
2845
  }
2687
2846
  // Attio's own measured 5-point star path (crisp's `RATING_STAR_D`, traced from the
@@ -2707,8 +2866,7 @@ function RatingCell({ value, maxRating = 5 }) {
2707
2866
  },
2708
2867
  children: Array.from({
2709
2868
  length: maxRating
2710
- }, (_, index)=>// biome-ignore lint/suspicious/noArrayIndexKey: a fixed-length star row has no other identity
2711
- /*#__PURE__*/ jsx("svg", {
2869
+ }, (_, index)=>/*#__PURE__*/ jsx("svg", {
2712
2870
  width: 16,
2713
2871
  height: 16,
2714
2872
  viewBox: "0 0 16 16",
@@ -3199,7 +3357,7 @@ const TextFieldTextarea = /*#__PURE__*/ forwardRef(({ className, autoresize = tr
3199
3357
  });
3200
3358
  TextFieldTextarea.displayName = "TextFieldTextarea";
3201
3359
 
3202
- __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))}");
3360
+ __insertCSS(".seed-data-table__root{box-sizing:border-box;display:flex;flex-direction:column;width:100%;border-width:1px;border-style:solid;font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}.seed-data-table__table{width:100%;border-collapse:separate;border-spacing:0;table-layout:fixed}.seed-data-table__headerCell{box-sizing:border-box;position:sticky;top:0;z-index:2;vertical-align:middle;text-align:start;padding:0;height:var(--seed-dimension-x10);background-color:var(--seed-color-bg-layer-default);border-top:1px solid var(--seed-color-stroke-neutral-muted);border-bottom:1px solid var(--seed-color-stroke-neutral-muted);border-inline-end:1px solid var(--seed-color-stroke-neutral-muted);border-inline-start:none;color:var(--seed-color-fg-neutral);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);font-weight:var(--seed-font-weight-medium);letter-spacing:-.14px}.seed-data-table__headerCell:last-child{border-inline-end:none}.seed-data-table__headerCell[data-sortable]{cursor:pointer;user-select:none}.seed-data-table__headerCell [data-chevron]{opacity:0;transition:opacity .12s}.seed-data-table__headerCell[data-sortable]:hover [data-chevron]{opacity:.6}.seed-data-table__headerCell[data-sorted] [data-chevron]{opacity:1;color:var(--seed-color-fg-neutral)}.seed-data-table__headerCell:hover [data-column-menu]{opacity:1}.seed-data-table__headerCell [data-column-menu]:focus-visible,.seed-data-table__headerCell [data-column-menu][data-open]{opacity:1}.seed-data-table__headerCell[data-dragging]{opacity:.5}.seed-data-table__headerCell[data-sticky]{position:sticky;z-index:3;background-color:var(--seed-color-bg-layer-default)}.seed-data-table__headerCell{outline:var(--seed-dimension-x0_5) solid transparent;outline-offset:calc(var(--seed-dimension-x0_5) * -1)}.seed-data-table__headerCell:is(:focus-visible,[data-focus-visible]){outline:var(--seed-dimension-x0_5) solid var(--seed-color-stroke-focus-ring);outline-offset:calc(var(--seed-dimension-x0_5) * -1)}.seed-data-table__headerCell .seed-data-table-resize-handle{position:absolute;top:0;inset-inline-end:calc(7px / -2);width:7px;height:100%;z-index:4;display:flex;align-items:center;justify-content:center;cursor:col-resize;touch-action:none}.seed-data-table__headerCell .seed-data-table-resize-handle-inner{width:3px;height:100%;border-radius:3px;background-color:#0298d9;opacity:0;transition:opacity .1s}.seed-data-table__headerCell .seed-data-table-resize-handle:hover .seed-data-table-resize-handle-inner,.seed-data-table__headerCell .seed-data-table-resize-handle[data-resizing] .seed-data-table-resize-handle-inner{opacity:1}.seed-data-table__headerCellInner{box-sizing:border-box;display:flex;align-items:center;width:100%;height:100%;min-width:0;gap:var(--seed-dimension-x1_5);padding-inline:var(--seed-dimension-x3)}.seed-data-table__headerCellLabel{min-width:0;flex:1 1 0%;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__headerCellIcon{flex-shrink:0;margin-inline-start:auto;display:inline-flex;align-items:center;justify-content:center;width:var(--seed-dimension-x4);height:var(--seed-dimension-x4);color:var(--seed-color-fg-neutral-subtle);border:none;background:0 0;padding:0;font:inherit;cursor:pointer}.seed-data-table__headerCellIcon[data-active]{color:var(--seed-color-fg-neutral)}.seed-data-table__headerCellIcon[data-column-menu]{opacity:0;transition:opacity .12s}.seed-data-table__row{position:relative;background-color:var(--seed-color-bg-layer-default);--seed-data-table-row-bg:var(--seed-color-bg-layer-default)}.seed-data-table__row[data-clickable]{cursor:pointer}.seed-data-table__row:is([aria-selected=true],[data-selected]){background-color:color-mix(in srgb,#0298d9 10%,var(--seed-color-bg-layer-default));--seed-data-table-row-bg:color-mix(in srgb, #0298d9 10%, var(--seed-color-bg-layer-default))}.seed-data-table__row:is([aria-selected=true],[data-selected]):is(:hover,[data-hover]){background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 3%,color-mix(in srgb,#0298d9 10%,var(--seed-color-bg-layer-default)));--seed-data-table-row-bg:color-mix(in srgb, var(--seed-color-fg-neutral) 3%, color-mix(in srgb, #0298d9 10%, var(--seed-color-bg-layer-default)))}.seed-data-table__cell{box-sizing:border-box;position:relative;vertical-align:middle;text-align:start;height:var(--seed-dimension-x9);border-bottom:1px solid var(--seed-color-stroke-neutral-muted);border-inline-end:1px solid var(--seed-color-stroke-neutral-muted);border-top:none;border-inline-start:none;padding-block:0;padding-inline:var(--seed-dimension-x3);color:var(--seed-color-fg-neutral);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);font-weight:var(--seed-font-weight-regular);letter-spacing:-.14px;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__cell:last-child{border-inline-end:none}tbody tr:last-child .seed-data-table__cell{border-bottom:none}.seed-data-table__cell[data-numeric]{text-align:end;font-variant-numeric:tabular-nums}.seed-data-table__cell[data-sticky]{position:sticky;z-index:1;background-color:var(--seed-data-table-row-bg)}.seed-data-table__cell[data-editable]{cursor:default}.seed-data-table__cell .seed-data-table-edit-picker{display:flex;align-items:center;gap:var(--seed-dimension-x1_5);min-width:0}.seed-data-table__cell .seed-data-table-edit-pending{flex-shrink:0;color:var(--seed-color-fg-neutral-subtle);animation:rotate .6s linear infinite}.seed-data-table__cell .seed-data-table-edit-error{position:absolute;top:100%;inset-inline-start:var(--seed-dimension-x3);inset-inline-end:var(--seed-dimension-x3);z-index:2;padding-top:2px;font-size:12px;line-height:1.3;color:var(--seed-color-fg-critical);overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__entity{display:flex;align-items:center;min-width:0;gap:var(--seed-dimension-x1_5)}.seed-data-table__entityName{min-width:0;font-weight:var(--seed-font-weight-medium);overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__tagList{display:flex;align-items:center;gap:var(--seed-dimension-x1_5);overflow:hidden;min-width:0}.seed-data-table__entityActions{display:flex;align-items:center;gap:6px;margin-inline-start:auto;flex-shrink:0;opacity:0;transition:opacity .15s}tr:hover .seed-data-table__entityActions{opacity:1}.seed-data-table__entityActions:focus-within{opacity:1}.seed-data-table__entityAction{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin:0;padding:2px;border:none;border-radius:6px;background:0 0;font-family:inherit;color:color-mix(in srgb,var(--seed-color-fg-neutral) 55%,transparent);cursor:pointer;flex-shrink:0;transition:background-color .2s,color .2s,box-shadow .2s}.seed-data-table__entityAction:hover{background:color-mix(in srgb,var(--seed-color-fg-neutral) 4%,transparent);color:var(--seed-color-fg-neutral)}.seed-data-table__link{color:var(--seed-color-fg-neutral);text-decoration:none;min-width:0;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__link:is(:hover,[data-hover]){text-decoration:underline}.seed-data-table__chip{box-sizing:border-box;display:inline-flex;align-items:center;flex-shrink:0;max-width:100%;height:22px;padding:0 4px;border-radius:7px;font-size:10px;font-weight:var(--seed-font-weight-regular);line-height:22px;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__chipList{display:flex;align-items:center;flex-wrap:nowrap;gap:var(--seed-dimension-x1_5);overflow:hidden;min-width:0;width:100%}.seed-data-table__chipRemove{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:12px;height:12px;margin:0;margin-inline-start:2px;padding:0;border:none;border-radius:999px;background:0 0;color:inherit;opacity:.6;cursor:pointer;transition:opacity .12s,background-color .12s}.seed-data-table__chipRemove:hover{opacity:1;background-color:rgba(0,0,0,.08)}.seed-data-table__chipInput{flex:1 1 auto;min-width:40px;height:22px;margin:0;padding:0;border:none;outline:0;background:0 0;font-family:inherit;font-size:var(--seed-font-size-t4);color:var(--seed-color-fg-neutral);letter-spacing:-.14px}.seed-data-table__expandOverlay{box-sizing:border-box;position:absolute;top:-1px;inset-inline-start:-1px;inset-inline-end:-1px;z-index:10;min-height:calc(100% + 2px);display:flex;align-items:flex-start;padding-inline:var(--seed-dimension-x3);padding-block:var(--seed-dimension-x3);background-color:var(--seed-color-bg-layer-floating);border-radius:var(--seed-radius-r5);box-shadow:inset 0 0 0 1px var(--seed-color-stroke-neutral-weak),var(--seed-shadow-s3);color:var(--seed-color-fg-neutral);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);letter-spacing:-.14px;cursor:default}.seed-data-table__expandOverlayText{width:100%;min-width:0;white-space:normal;word-break:break-word}.seed-data-table__cellSuffixRow{display:flex;align-items:center;gap:4px;min-width:0;width:100%}.seed-data-table__cellSuffixText{flex:1 1 0%;min-width:0;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__cellSuffixIcon{flex-shrink:0;display:inline-flex;align-items:center;color:color-mix(in srgb,var(--seed-color-fg-neutral) 55%,transparent)}.seed-data-table__toolbar{box-sizing:border-box;display:flex;align-items:center;width:100%;flex-shrink:0;min-height:49px;gap:var(--seed-dimension-x2);padding-inline:var(--seed-dimension-x3);border-top:1px solid var(--seed-color-stroke-neutral-muted)}.seed-data-table__search{position:relative;display:inline-flex;align-items:center;flex:1 1 0%;max-width:280px}.seed-data-table__search svg{position:absolute;inset-inline-start:var(--seed-dimension-x2);width:var(--seed-dimension-x4);height:var(--seed-dimension-x4);color:var(--seed-color-fg-neutral-muted);pointer-events:none}.seed-data-table__search input{width:100%;height:var(--seed-dimension-x8);padding-inline-start:var(--seed-dimension-x7);padding-inline-end:var(--seed-dimension-x2);border:none;border-radius:var(--seed-radius-r2);background-color:var(--seed-color-bg-layer-default);box-shadow:inset 0 0 0 1px var(--seed-color-stroke-neutral-weak);font-family:inherit;font-size:var(--seed-font-size-t4);color:var(--seed-color-fg-neutral);outline:0;transition:box-shadow .1s var(--seed-timing-function-easing)}.seed-data-table__search input::placeholder{color:var(--seed-color-fg-neutral-subtle)}.seed-data-table__search input:focus{box-shadow:inset 0 0 0 2px var(--seed-color-stroke-neutral-contrast)}.seed-data-table__count{flex-shrink:0;color:var(--seed-color-fg-neutral-subtle);font-size:var(--seed-font-size-t3);font-variant-numeric:tabular-nums}.seed-data-table__toolbarEnd{flex-shrink:0;margin-inline-start:auto;display:inline-flex;align-items:center;gap:var(--seed-dimension-x2)}.seed-data-table__scrollArea{position:relative;display:flex;flex-direction:column;width:100%;min-width:0;flex:1 1 auto;min-height:0;overflow-x:auto;overflow-y:auto}.seed-data-table__scrollArea[data-scrolled-start] [data-sticky-edge]{clip-path:inset(0 -38px 0 0);box-shadow:6px 0 16px 4px rgba(0,0,0,.12)}.seed-data-table__scrollArea[data-scrolled-end] [data-sticky-edge-end]{clip-path:inset(0 0 0 -38px);box-shadow:-6px 0 16px 4px rgba(0,0,0,.12)}.seed-data-table__footerCell{box-sizing:border-box;position:sticky;bottom:0;z-index:2;vertical-align:middle;padding:0;height:var(--seed-dimension-x9);background-color:var(--seed-color-bg-layer-default);border-top:1px solid var(--seed-color-stroke-neutral-muted);border-inline-end:1px solid var(--seed-color-stroke-neutral-muted);border-inline-start:none}.seed-data-table__footerCell:last-child{border-inline-end:none}.seed-data-table__footerCell .seed-data-table-column-calc-trigger{display:flex;align-items:center;gap:var(--seed-dimension-x1_5);width:100%;height:100%;min-width:0;margin:0;padding:0 var(--seed-dimension-x3);border:none;background:0 0;color:var(--seed-color-fg-neutral-subtle);font-family:inherit;font-size:var(--seed-font-size-t4);font-weight:var(--seed-font-weight-medium);white-space:nowrap;overflow:clip;cursor:pointer;transition:background-color .12s,color .12s}.seed-data-table__footerCell .seed-data-table-column-calc-trigger:hover{background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 4%,transparent);color:var(--seed-color-fg-neutral)}.seed-data-table__footerCell .seed-data-table-column-calc-trigger[data-align=right]{justify-content:flex-end}.seed-data-table__footerCell .seed-data-table-column-calc-trigger svg{flex-shrink:0;opacity:.7}.seed-data-table__footerCell .seed-data-table-column-calc-placeholder{overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__footerCell .seed-data-table-column-calc-value{min-width:0;flex-shrink:1;color:var(--seed-color-fg-neutral);font-variant-numeric:tabular-nums;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__footerCell .seed-data-table-column-calc-op{min-width:0;flex-shrink:1;color:color-mix(in srgb,var(--seed-color-fg-neutral) 63%,transparent);overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__emptyState{box-sizing:border-box;text-align:center;padding-block:var(--seed-dimension-x8);padding-inline:var(--seed-dimension-x3);color:var(--seed-color-fg-neutral-subtle);font-size:var(--seed-font-size-t4)}.seed-data-table__loadingState{box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:10px;padding-block:var(--seed-dimension-x8);padding-inline:var(--seed-dimension-x3);color:color-mix(in srgb,var(--seed-color-fg-neutral) 63%,transparent);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);font-weight:var(--seed-font-weight-medium)}.seed-data-table__loadingState[data-inline]{padding-block:0;padding-inline:0;height:var(--seed-dimension-x9)}.seed-data-table__loadingState .seed-data-table-loading-spinner{flex-shrink:0;color:color-mix(in srgb,var(--seed-color-fg-neutral) 63%,transparent);animation:rotate .6s linear infinite}.seed-data-table__addRowCell{box-sizing:border-box;padding:0;border-top:none}.seed-data-table__addRow{box-sizing:border-box;display:flex;align-items:center;width:100%;margin:0;border:none;border-top:1px solid var(--seed-color-stroke-neutral-muted);background-color:var(--seed-color-bg-layer-default);height:var(--seed-dimension-x9);gap:var(--seed-dimension-x2);padding-inline:var(--seed-dimension-x2);color:var(--seed-color-fg-neutral-subtle);font-family:inherit;font-size:var(--seed-font-size-t4);cursor:pointer;transition:background-color .12s,color .12s}.seed-data-table__addRow:hover{background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 4%,var(--seed-color-bg-layer-default));color:var(--seed-color-fg-neutral)}.seed-data-table__addRow svg{flex-shrink:0}.seed-data-table__selectionCell{text-align:center;padding:0}.seed-data-table__root--surface_card{border-color:var(--seed-color-stroke-neutral-muted);background-color:var(--seed-color-bg-layer-default);border-radius:var(--seed-radius-r3)}.seed-data-table__root--surface_bleed{border-color:var(--seed-color-bg-transparent);background-color:var(--seed-color-bg-transparent)}.seed-data-table__row--interactive_true{transition:background-color .12s}.seed-data-table__row--interactive_true:is(:hover,[data-hover]){background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 3%,transparent);--seed-data-table-row-bg:color-mix(in srgb, var(--seed-color-fg-neutral) 3%, var(--seed-color-bg-layer-default))}");
3203
3361
 
3204
3362
  const createClassName = (className, variants, compoundVariants = [])=>{
3205
3363
  const variantKeys = Object.keys(variants);
@@ -3294,6 +3452,42 @@ const dataTableSlotNames = [
3294
3452
  "link",
3295
3453
  "seed-data-table__link"
3296
3454
  ],
3455
+ [
3456
+ "chip",
3457
+ "seed-data-table__chip"
3458
+ ],
3459
+ [
3460
+ "chipList",
3461
+ "seed-data-table__chipList"
3462
+ ],
3463
+ [
3464
+ "chipRemove",
3465
+ "seed-data-table__chipRemove"
3466
+ ],
3467
+ [
3468
+ "chipInput",
3469
+ "seed-data-table__chipInput"
3470
+ ],
3471
+ [
3472
+ "expandOverlay",
3473
+ "seed-data-table__expandOverlay"
3474
+ ],
3475
+ [
3476
+ "expandOverlayText",
3477
+ "seed-data-table__expandOverlayText"
3478
+ ],
3479
+ [
3480
+ "cellSuffixRow",
3481
+ "seed-data-table__cellSuffixRow"
3482
+ ],
3483
+ [
3484
+ "cellSuffixText",
3485
+ "seed-data-table__cellSuffixText"
3486
+ ],
3487
+ [
3488
+ "cellSuffixIcon",
3489
+ "seed-data-table__cellSuffixIcon"
3490
+ ],
3297
3491
  [
3298
3492
  "entityActions",
3299
3493
  "seed-data-table__entityActions"
@@ -3868,6 +4062,103 @@ function RatingEditor({ column, value, pending, onCommit }) {
3868
4062
  ]
3869
4063
  });
3870
4064
  }
4065
+ ////////////////////////////////////////////////////////////////////////////////////
4066
+ // -- editable: "chip-list" (Feature B, #8) -- always-live, the same shape
4067
+ // `SwitchEditor`/`CheckboxEditor`/`RatingEditor` above already establish
4068
+ // (interacting with the widget IS the commit, no double-click gate, no separate
4069
+ // rest display -- `body-cells.tsx`'s `isAlwaysLiveEditable` covers it alongside
4070
+ // them). A gated open/close state (like `SelectEditor`'s) would fight adding
4071
+ // MULTIPLE tags in one sitting: `useDataTableEdit`'s `commitEdit` closes editing
4072
+ // the instant a SYNC commit resolves, so a gated chip editor would close itself
4073
+ // after the very first tag. Always-live sidesteps that entirely -- there is no
4074
+ // `editingCell` to close, the widget is simply always the cell's own content.
4075
+ // Every add/remove commits the FULL next array through the same one-string
4076
+ // contract every other editor already uses (`useDataTableEdit`'s own doc
4077
+ // comment): `JSON.stringify(nextTags)`, which the consumer's `onEdit` parses
4078
+ // back -- crisp/GRC's contract stays "one string in, no typed union", not a
4079
+ // second commit shape invented for just this column.
4080
+ function ChipListEditor({ column, value, pending, onCommit, classNames }) {
4081
+ const tags = Array.isArray(value) ? value.filter((tag)=>!!tag) : [];
4082
+ const [draft, setDraft] = React.useState("");
4083
+ function commitTags(next) {
4084
+ onCommit(JSON.stringify(next));
4085
+ }
4086
+ function addTag() {
4087
+ const tag = draft.trim();
4088
+ setDraft("");
4089
+ if (!tag || tags.includes(tag)) return;
4090
+ commitTags([
4091
+ ...tags,
4092
+ tag
4093
+ ]);
4094
+ }
4095
+ function removeTag(tag) {
4096
+ commitTags(tags.filter((existing)=>existing !== tag));
4097
+ }
4098
+ return(// biome-ignore lint/a11y/noStaticElementInteractions: not a widget -- this span never responds to interaction on ITSELF, only stops propagation of events that already occurred on a real interactive descendant (the remove buttons, the add-tag input). No role fits it because it isn't a control.
4099
+ /*#__PURE__*/ jsxs("span", {
4100
+ className: classNames.chipList,
4101
+ onClick: (event)=>event.stopPropagation(),
4102
+ children: [
4103
+ tags.map((tag)=>/*#__PURE__*/ jsx(SwatchChip, {
4104
+ value: tag,
4105
+ className: classNames.chip,
4106
+ children: /*#__PURE__*/ jsx("button", {
4107
+ type: "button",
4108
+ disabled: pending,
4109
+ "aria-label": `Remove ${tag}`,
4110
+ className: classNames.chipRemove,
4111
+ onClick: ()=>removeTag(tag),
4112
+ children: /*#__PURE__*/ jsx(IconXmarkLine_default, {
4113
+ size: 10
4114
+ })
4115
+ })
4116
+ }, tag)),
4117
+ /*#__PURE__*/ jsx("input", {
4118
+ className: classNames.chipInput,
4119
+ value: draft,
4120
+ disabled: pending,
4121
+ placeholder: tags.length ? "" : "Add…",
4122
+ "aria-label": `Add tag to ${column.key}`,
4123
+ onChange: (event)=>setDraft(event.target.value),
4124
+ onKeyDown: (event)=>{
4125
+ event.stopPropagation();
4126
+ if (event.key === "Enter") {
4127
+ event.preventDefault();
4128
+ addTag();
4129
+ } else if (event.key === "Backspace" && draft === "" && tags.length > 0) {
4130
+ removeTag(tags[tags.length - 1]);
4131
+ } else if (event.key === "Escape") {
4132
+ setDraft("");
4133
+ }
4134
+ }
4135
+ }),
4136
+ pending && /*#__PURE__*/ jsx(EditPendingIndicator, {})
4137
+ ]
4138
+ }));
4139
+ }
4140
+ ////////////////////////////////////////////////////////////////////////////////////
4141
+ // -- Feature C (#12): cell-expand "show full content" overlay -- an anchored box
4142
+ // (position:absolute over the cell it belongs to, grows in HEIGHT, elevated)
4143
+ // that wraps whatever a truncated cell would otherwise show inline: an editable
4144
+ // column's OWN editor node (unchanged, just relocated into this box instead of
4145
+ // rendered flush in the 36px row) for the editable case, or the plain full value
4146
+ // for a read-only one. `body-cells.tsx` decides WHEN to render this (a click on a
4147
+ // cell whose content is actually truncated, `truncate.ts`'s `isCellTruncated`);
4148
+ // this component only supplies the box.
4149
+ function CellExpandOverlay({ className, children }) {
4150
+ return(// `stopPropagation` -- an overlay sits on top of (and past) its own cell/row;
4151
+ // a click anywhere inside it must never ALSO fire the row's own `onRowClick`,
4152
+ // the same guard every other interactive cell affordance in this directory
4153
+ // already applies (`LinkCell`, `EntityCell`'s preview button, editors.tsx's
4154
+ // `TextEditor` root).
4155
+ // 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.
4156
+ /*#__PURE__*/ jsx("div", {
4157
+ className: className,
4158
+ onClick: (event)=>event.stopPropagation(),
4159
+ children: children
4160
+ }));
4161
+ }
3871
4162
 
3872
4163
  // Row-narrowing for the toolbar's search box -- split out of DataTable.tsx the way
3873
4164
  // cells.tsx splits out cell-type rendering: a self-contained, independently testable
@@ -3907,49 +4198,167 @@ function RatingEditor({ column, value, pending, onCommit }) {
3907
4198
  return data.filter((row)=>searchableColumns.some((column)=>matchesSearch(getColumnValue(column, row), query)));
3908
4199
  }
3909
4200
 
4201
+ // Feature C (#12) -- the click-to-expand gate: measures whether a cell's rendered
4202
+ // content is ACTUALLY truncated right now, so a single click only opens the
4203
+ // anchored overlay (editors.tsx's `CellExpandOverlay`) when there's real hidden
4204
+ // content to show, never on a short value that already fits. Isolated out of
4205
+ // body-cells.tsx the same way select-search.tsx/filtering.ts already split their own
4206
+ // concerns out of the styled layer, so this one pure DOM read is unit-testable on
4207
+ // its own (a plain `<td>`'s own truncate CSS vs. a suffix-icon cell's nested flex
4208
+ // child need two different elements measured -- see below).
4209
+ //
4210
+ // This one genuinely reads a real, laid-out DOM element (`scrollWidth`/
4211
+ // `clientWidth`), unlike react-headless/data-table/src/dom.ts's own helpers (plain
4212
+ // data in, plain data out) -- it lives in the REACT (styled) package rather than
4213
+ // react-headless for that reason: measuring actual layout is a rendering concern,
4214
+ // not table state.
4215
+ function isCellTruncated(cell) {
4216
+ // The common case: a plain cell's own `white-space: nowrap; overflow: clip` (the
4217
+ // `cell` recipe slot) makes the `<td>` ITSELF overflow once its text is longer
4218
+ // than the column is wide.
4219
+ if (cell.scrollWidth > cell.clientWidth) return true;
4220
+ // A cell composing a suffix icon beside its text (cells.tsx's `CellWithSuffix`,
4221
+ // Feature D) wraps the text in its OWN `flex: 1 1 0%; min-width: 0` child instead
4222
+ // -- that flex child never lets the `<td>` itself overflow (it's constrained to
4223
+ // the remaining row width by construction), so the truncating element to measure
4224
+ // is the flex child, marked `data-seed-truncate` by whichever cell renderer
4225
+ // applies its own ellipsis (`CellWithSuffix`'s text span today; any future
4226
+ // truncating inner element should carry the same marker).
4227
+ const targets = cell.querySelectorAll("[data-seed-truncate]");
4228
+ for (const target of targets){
4229
+ if (target.scrollWidth > target.clientWidth) return true;
4230
+ }
4231
+ return false;
4232
+ }
4233
+
3910
4234
  /** `editable` + one of these shapes needs no double-click gate and no separate rest
3911
4235
  * display -- interacting with the widget itself IS the commit gesture (crisp's own
3912
4236
  * #453/#760/#761: `switch`/`rating`/`checkbox` never had a meaningfully different
3913
- * "rest" state to show). Every other `editable` column opens on double-click. */ function isAlwaysLiveEditable(column) {
4237
+ * "rest" state to show). `chip-list` (Feature B, #8) joins them for a different
4238
+ * reason -- see `ChipListEditor`'s own doc comment (editors.tsx): a GATED chip
4239
+ * editor would close itself the instant the first tag committed. Every other
4240
+ * `editable` column opens on double-click. */ function isAlwaysLiveEditable(column) {
3914
4241
  if (!column.editable) return false;
3915
- return column.editor === "switch" || column.type === "rating" || column.type === "checkbox";
4242
+ return column.editor === "switch" || column.type === "rating" || column.type === "checkbox" || column.type === "chip-list";
3916
4243
  }
3917
4244
  function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyFirst, stickyLast }) {
3918
4245
  const effective = useEffectiveColumns(columns, {
3919
4246
  selectable,
3920
4247
  stickyFirst
3921
4248
  });
3922
- const { startEdit } = useDataTableContext();
3923
4249
  return /*#__PURE__*/ jsx(Fragment, {
3924
4250
  children: effective.map((entry, index)=>{
3925
4251
  const { column } = entry;
3926
4252
  const isLast = index === effective.length - 1;
3927
4253
  const sticky = pinnedCellProps(entry, stickyLast, isLast);
3928
- // A gated `editable` column (everything except the always-live widgets)
3929
- // opens on double-click -- Attio's own measured gesture (crisp's #411),
3930
- // matched here rather than a single click, which stays "select this cell"
3931
- // in crisp's own spreadsheet model and would otherwise be indistinguishable
3932
- // from this table's plain row click.
3933
- const gated = column.editable && !isAlwaysLiveEditable(column);
3934
- return /*#__PURE__*/ jsx(DataTableCell, {
3935
- "data-numeric": dataAttr(column.type === "number" || column.type === "currency"),
3936
- "data-editable": dataAttr(!!column.editable),
3937
- "data-sticky": sticky["data-sticky"],
3938
- "data-sticky-edge": sticky["data-sticky-edge"],
3939
- "data-sticky-edge-end": sticky["data-sticky-edge-end"],
3940
- style: sticky.style,
3941
- onDoubleClick: gated ? ()=>startEdit(row.id, column.key) : undefined,
3942
- children: /*#__PURE__*/ jsx(CellContent, {
3943
- column: column,
3944
- row: row,
3945
- rowId: row.id,
3946
- classNames: classNames,
3947
- locale: locale
3948
- })
4254
+ return /*#__PURE__*/ jsx(DataTableBodyCell, {
4255
+ column: column,
4256
+ row: row,
4257
+ classNames: classNames,
4258
+ locale: locale,
4259
+ sticky: sticky
3949
4260
  }, column.key);
3950
4261
  })
3951
4262
  });
3952
4263
  }
4264
+ /**
4265
+ * One `<td>` -- split out of `ColumnBodyCells`'s own map (which now only computes
4266
+ * per-column LAYOUT, `pinnedCellProps`) so this component can own its own per-cell
4267
+ * "is the Feature C overlay open" local state (`overlayOpen`) without re-deriving it
4268
+ * on every row's render. React keeps this instance stable across re-renders because
4269
+ * `ColumnBodyCells` keys it by `column.key` the same way it always keyed the bare
4270
+ * `<DataTableCell>` before this split.
4271
+ */ function DataTableBodyCell({ column, row, classNames, locale, sticky }) {
4272
+ const { startEdit, isEditing, isPeeking, openPeek, closePeek } = useDataTableContext();
4273
+ const cellRef = React.useRef(null);
4274
+ // Feature C (#12) -- whether THIS activation (click or double-click) should
4275
+ // render as the anchored overlay rather than plain inline. Frozen at the moment
4276
+ // of activation (re-measured every time, never left stale) rather than derived
4277
+ // from `isEditing`/`isPeeking` alone, because those two states don't carry "was
4278
+ // the content actually truncated when this opened" on their own -- a SHORT
4279
+ // editable cell's existing double-click-to-edit (tranche 5, unchanged) must keep
4280
+ // rendering exactly as it always has, not suddenly gain overlay chrome.
4281
+ const [overlayOpen, setOverlayOpen] = React.useState(false);
4282
+ const alwaysLive = isAlwaysLiveEditable(column);
4283
+ // A gated `editable` column (everything except the always-live widgets) opens on
4284
+ // double-click -- Attio's own measured gesture (crisp's #411), matched here
4285
+ // rather than a single click, which stays "select this cell" in crisp's own
4286
+ // spreadsheet model and would otherwise be indistinguishable from this table's
4287
+ // plain row click. Feature C ADDS a single click as a SECOND way in, but only
4288
+ // when the cell's content is actually truncated (see `handleClick` below) --
4289
+ // double-click keeps working unconditionally, same as before this feature.
4290
+ const gated = column.editable && !alwaysLive;
4291
+ function handleDoubleClick(event) {
4292
+ if (!gated) return;
4293
+ setOverlayOpen(isCellTruncated(event.currentTarget));
4294
+ startEdit(row.id, column.key);
4295
+ }
4296
+ function handleClick(event) {
4297
+ if (!isCellTruncated(event.currentTarget)) return;
4298
+ if (gated) {
4299
+ setOverlayOpen(true);
4300
+ startEdit(row.id, column.key);
4301
+ } else if (alwaysLive) {
4302
+ // No separate open/edit state to start -- the always-live widget already
4303
+ // renders unconditionally; this only switches ITS OWN box to the anchored
4304
+ // overlay so a long chip list (etc.) gets room to show every chip.
4305
+ setOverlayOpen(true);
4306
+ } else {
4307
+ setOverlayOpen(true);
4308
+ openPeek(row.id, column.key);
4309
+ }
4310
+ }
4311
+ // Closes the overlay on Escape or an outside click/pointerdown -- a gated
4312
+ // editor's OWN Escape/blur handling already ends `isEditing` on its own (see
4313
+ // `TextEditor`/`SelectEditor`'s existing onCancel/onOpenChange wiring), which
4314
+ // alone is enough to hide a GATED cell's overlay (its render below is gated on
4315
+ // `isEditing` too); this listener is what the PEEK and ALWAYS-LIVE cases need,
4316
+ // since neither has any other mechanism that ever calls `closePeek`/resets
4317
+ // `overlayOpen` on its own.
4318
+ React.useEffect(()=>{
4319
+ if (!overlayOpen) return;
4320
+ function handlePointerDown(event) {
4321
+ if (cellRef.current && !cellRef.current.contains(event.target)) {
4322
+ setOverlayOpen(false);
4323
+ closePeek();
4324
+ }
4325
+ }
4326
+ function handleKeyDown(event) {
4327
+ if (event.key !== "Escape") return;
4328
+ setOverlayOpen(false);
4329
+ closePeek();
4330
+ }
4331
+ document.addEventListener("pointerdown", handlePointerDown);
4332
+ document.addEventListener("keydown", handleKeyDown);
4333
+ return ()=>{
4334
+ document.removeEventListener("pointerdown", handlePointerDown);
4335
+ document.removeEventListener("keydown", handleKeyDown);
4336
+ };
4337
+ }, [
4338
+ overlayOpen,
4339
+ closePeek
4340
+ ]);
4341
+ const overlayActive = overlayOpen && (alwaysLive || isEditing(row.id, column.key) || isPeeking(row.id, column.key));
4342
+ return /*#__PURE__*/ jsx(DataTableCell, {
4343
+ ref: cellRef,
4344
+ "data-numeric": dataAttr(column.type === "number" || column.type === "currency"),
4345
+ "data-editable": dataAttr(!!column.editable),
4346
+ "data-sticky": sticky["data-sticky"],
4347
+ "data-sticky-edge": sticky["data-sticky-edge"],
4348
+ "data-sticky-edge-end": sticky["data-sticky-edge-end"],
4349
+ style: sticky.style,
4350
+ onClick: handleClick,
4351
+ onDoubleClick: handleDoubleClick,
4352
+ children: /*#__PURE__*/ jsx(CellContent, {
4353
+ column: column,
4354
+ row: row,
4355
+ rowId: row.id,
4356
+ classNames: classNames,
4357
+ locale: locale,
4358
+ overlayActive: overlayActive
4359
+ })
4360
+ });
4361
+ }
3953
4362
  /**
3954
4363
  * Dispatches to whichever of cells.tsx's static renderers or editors.tsx's live
3955
4364
  * editors applies to THIS column/row right now -- always-live first (no rest state to
@@ -3957,7 +4366,7 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
3957
4366
  * display (a custom `render` wins there, same as before `editable` existed; it never
3958
4367
  * overrides an actual editing/always-live state, matching crisp's own split between
3959
4368
  * `renderTyped`'s rest-state branch and the editing branches above it).
3960
- */ function CellContent({ column, row, rowId, classNames, locale }) {
4369
+ */ function CellContent({ column, row, rowId, classNames, locale, overlayActive }) {
3961
4370
  const { isEditing, isPending, getEditError, commitEdit, cancelEdit } = useDataTableContext();
3962
4371
  const rawValue = getColumnValue(column, row);
3963
4372
  const pending = isPending(rowId, column.key);
@@ -3986,6 +4395,14 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
3986
4395
  pending: pending,
3987
4396
  onCommit: commit
3988
4397
  });
4398
+ } else if (column.editable && column.type === "chip-list") {
4399
+ node = /*#__PURE__*/ jsx(ChipListEditor, {
4400
+ column: column,
4401
+ value: resolveEditValue(column, row, rawValue),
4402
+ pending: pending,
4403
+ onCommit: commit,
4404
+ classNames: classNames
4405
+ });
3989
4406
  } else if (column.editable && isEditing(rowId, column.key)) {
3990
4407
  if (column.editor === "select") {
3991
4408
  node = /*#__PURE__*/ jsx(SelectEditor, {
@@ -4021,6 +4438,10 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4021
4438
  });
4022
4439
  }
4023
4440
  } else {
4441
+ // Feature C (#12), read-only case: `DataTableBodyCell`'s own `isPeeking` already
4442
+ // decided whether the overlay should be open (folded into `overlayActive`
4443
+ // below) -- this branch always renders the SAME `DisplayCellContent` either
4444
+ // way, just possibly relocated into the overlay box.
4024
4445
  node = /*#__PURE__*/ jsx(DisplayCellContent, {
4025
4446
  column: column,
4026
4447
  row: row,
@@ -4031,7 +4452,13 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4031
4452
  }
4032
4453
  return /*#__PURE__*/ jsxs(Fragment, {
4033
4454
  children: [
4034
- node,
4455
+ overlayActive ? /*#__PURE__*/ jsx(CellExpandOverlay, {
4456
+ className: classNames.expandOverlay,
4457
+ children: column.editable ? node : /*#__PURE__*/ jsx("span", {
4458
+ className: classNames.expandOverlayText,
4459
+ children: node
4460
+ })
4461
+ }) : node,
4035
4462
  error && /*#__PURE__*/ jsx(EditError, {
4036
4463
  message: error
4037
4464
  })
@@ -4062,7 +4489,11 @@ function DisplayCellContent({ column, row, value, classNames, locale }) {
4062
4489
  return /*#__PURE__*/ jsx(LinkCell, {
4063
4490
  column: column,
4064
4491
  value: value,
4065
- className: classNames.link
4492
+ row: row,
4493
+ className: classNames.link,
4494
+ rowClassName: classNames.cellSuffixRow,
4495
+ textClassName: classNames.cellSuffixText,
4496
+ suffixClassName: classNames.cellSuffixIcon
4066
4497
  });
4067
4498
  case "tags":
4068
4499
  return /*#__PURE__*/ jsx(TagsCell, {
@@ -4073,6 +4504,17 @@ function DisplayCellContent({ column, row, value, classNames, locale }) {
4073
4504
  return /*#__PURE__*/ jsx(StatusCell, {
4074
4505
  value: value
4075
4506
  });
4507
+ case "chip":
4508
+ return /*#__PURE__*/ jsx(ChipCell, {
4509
+ value: value,
4510
+ className: classNames.chip
4511
+ });
4512
+ case "chip-list":
4513
+ return /*#__PURE__*/ jsx(ChipListCell, {
4514
+ value: value,
4515
+ className: classNames.chipList,
4516
+ chipClassName: classNames.chip
4517
+ });
4076
4518
  case "number":
4077
4519
  return /*#__PURE__*/ jsx(Fragment, {
4078
4520
  children: formatNumberCell(value, locale)
@@ -4099,9 +4541,23 @@ function DisplayCellContent({ column, row, value, classNames, locale }) {
4099
4541
  value: value
4100
4542
  });
4101
4543
  default:
4102
- return /*#__PURE__*/ jsx(Fragment, {
4103
- children: value == null ? null : String(value)
4104
- });
4544
+ {
4545
+ const text = value == null ? null : String(value);
4546
+ const suffix = column.suffixIcon?.(row);
4547
+ // Feature D (#13) -- a plain text column carrying a `suffixIcon` (e.g. a
4548
+ // location column's pin glyph) composes through the same `CellWithSuffix`
4549
+ // LinkCell already uses, so the icon never gets squeezed out by a long value.
4550
+ if (!suffix || text == null) return /*#__PURE__*/ jsx(Fragment, {
4551
+ children: text
4552
+ });
4553
+ return /*#__PURE__*/ jsx(CellWithSuffix, {
4554
+ text: text,
4555
+ suffix: suffix,
4556
+ rowClassName: classNames.cellSuffixRow,
4557
+ textClassName: classNames.cellSuffixText,
4558
+ suffixClassName: classNames.cellSuffixIcon
4559
+ });
4560
+ }
4105
4561
  }
4106
4562
  }
4107
4563