@crisp-ui-kit/crisp 0.7.4 → 0.7.6

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.
@@ -107,16 +107,30 @@ function formatCell(type, v, symbol) {
107
107
  return asText(v);
108
108
  }
109
109
  /** Built-in typed-cell renderer (used when a column has no custom `render`). */
110
- function renderTyped(col, value, avatarTone) {
110
+ // `value` is always the RAW cell value and `display` the caller's `format`
111
+ // output, if any. They are separate arguments on purpose: the entity and tags
112
+ // branches inspect the value's SHAPE (object / array), so handing them a
113
+ // formatted string used to make them fall through to their empty state and
114
+ // silently drop the cell's data. Each branch decides for itself where a
115
+ // display string belongs -- as an entity's name, a status' label -- while the
116
+ // raw value keeps driving structure and tone.
117
+ function renderTyped(col, value, avatarTone, display) {
111
118
  const empty = (0, jsx_runtime_1.jsx)("span", { className: "datatable-muted", children: "\u2014" });
112
119
  switch (col.type) {
113
120
  case "entity": {
114
- const e = (typeof value === "object" && value ? value : { name: asText(value) });
121
+ const raw = (typeof value === "object" && value ? value : { name: asText(value) });
122
+ // a formatted string relabels the entity; its avatar and its hashed tone
123
+ // still come from the real value
124
+ const e = display ? { ...raw, name: display } : raw;
115
125
  if (!e.name)
116
126
  return empty;
117
- return ((0, jsx_runtime_1.jsxs)("span", { className: "datatable-entity", children: [(0, jsx_runtime_1.jsx)("span", { className: "datatable-avatar", "data-tone": avatarTone === "hashed" ? toneFor(e.name) : undefined, children: e.avatar ? ((0, jsx_runtime_1.jsx)("img", { src: e.avatar, alt: "" })) : (initials(e.name).slice(0, 2)) }), (0, jsx_runtime_1.jsx)("span", { className: "datatable-entity-name", children: e.name })] }));
127
+ return ((0, jsx_runtime_1.jsxs)("span", { className: "datatable-entity", children: [(0, jsx_runtime_1.jsx)("span", { className: "datatable-avatar", "data-tone": avatarTone === "hashed" ? toneFor(raw.name) : undefined, children: e.avatar ? ((0, jsx_runtime_1.jsx)("img", { src: e.avatar, alt: "" })) : (initials(e.name).slice(0, 2)) }), (0, jsx_runtime_1.jsx)("span", { className: "datatable-entity-name", children: e.name })] }));
118
128
  }
119
129
  case "tags": {
130
+ // asking for a format on a tag column means "render it your way" -- show
131
+ // that string rather than trying to split it back into chips
132
+ if (display)
133
+ return display;
120
134
  const tags = Array.isArray(value) ? value : [];
121
135
  if (!tags.length)
122
136
  return empty;
@@ -126,10 +140,12 @@ function renderTyped(col, value, avatarTone) {
126
140
  const s = asText(value);
127
141
  if (!s)
128
142
  return empty;
129
- return ((0, jsx_runtime_1.jsx)("span", { className: "datatable-tag", "data-tone": col.tones?.[s] ?? toneFor(s), children: s }));
143
+ // tone is looked up by the RAW status, so formatting the label (casing,
144
+ // translation) can never silently repaint the chip
145
+ return ((0, jsx_runtime_1.jsx)("span", { className: "datatable-tag", "data-tone": col.tones?.[s] ?? toneFor(s), children: display || s }));
130
146
  }
131
147
  default: {
132
- const text = formatCell(col.type, value, col.symbol);
148
+ const text = display || formatCell(col.type, value, col.symbol);
133
149
  const isNum = col.type === "number" || col.type === "currency";
134
150
  if (!text)
135
151
  return empty;
@@ -324,7 +340,8 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
324
340
  }
325
341
  /**
326
342
  * Spreadsheet keyboard model, ported from RecordsTable: arrows move the
327
- * selected cell, Enter opens its editor, Escape clears, and Cmd/Ctrl+C
343
+ * selected cell, Enter opens its editor (or, on a non-editable cell,
344
+ * activates the row via `onRowClick`), Escape clears, and Cmd/Ctrl+C
328
345
  * copies the cell's RENDERED text (what the user sees, not the raw value).
329
346
  */
330
347
  function onGridKeyDown(e) {
@@ -361,6 +378,10 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
361
378
  e.preventDefault();
362
379
  setEditing({ id: selCell.id, key: selCell.key });
363
380
  }
381
+ else if (onRowClick) {
382
+ e.preventDefault();
383
+ onRowClick(rows[r]);
384
+ }
364
385
  return;
365
386
  }
366
387
  else
@@ -502,7 +523,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
502
523
  }
503
524
  }, children: col.render
504
525
  ? col.render(value, row)
505
- : renderTyped(col, col.format ? col.format(value, row) : value, avatarTone) }, col.key));
526
+ : renderTyped(col, value, avatarTone, col.format?.(value, row)) }, col.key));
506
527
  })] }, row.id)))] }, group ?? "__ungrouped")))) }), (hasSummary || onAddRow) && ((0, jsx_runtime_1.jsxs)(table_1.TableFooter, { children: [hasSummary && ((0, jsx_runtime_1.jsxs)(table_1.TableRow, { children: [showGrip && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-2) }), selectable && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-1) }), columns.map((col, i) => ((0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), ...stickyCol(i), children: col.summary ? col.summary(rows) : null }, col.key)))] })), onAddRow && ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { className: "datatable-addcell", colSpan: totalCols, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "datatable-addrow", onClick: onAddRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), addRowLabel] }) }) }))] }))] })] }));
507
528
  }
508
529
  DataTable.displayName = "DataTable";
@@ -394,6 +394,15 @@ function CreateRecordDialog({ open, onOpenChange, columns, entityLabel, onCreate
394
394
  function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, rows: rowsProp = exports.defaultRows, selectedIds, onSelectionChange, showNew = true, allowAddColumn = true, onOpenRecord, onOpenRecordPage, avatarTone = "brand", className, }) {
395
395
  const [cols, setCols] = React.useState(columnsProp);
396
396
  const [data, setData] = React.useState(rowsProp);
397
+ // The table owns `cols`/`data` so it can rename a column, edit a cell, or
398
+ // append a record without a round trip. That state is seeded from the props,
399
+ // so without these it would be a mount-time SNAPSHOT: an app that renders
400
+ // `rows={[]}` and re-renders with fetched rows would show "No records yet"
401
+ // for ever. Re-seed whenever the caller hands us a new array -- caller wins,
402
+ // exactly as DataTable does for its columns. Local edits survive until then,
403
+ // because a caller passing a stable array never triggers this.
404
+ React.useEffect(() => setCols(columnsProp), [columnsProp]);
405
+ React.useEffect(() => setData(rowsProp), [rowsProp]);
397
406
  // Attio's multi-criteria sort: an ordered list of {field, direction} rows.
398
407
  const [sorts, setSorts] = React.useState([]);
399
408
  const [dragSort, setDragSort] = React.useState(null);
@@ -607,7 +616,20 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
607
616
  };
608
617
  const allSelected = visibleRows.length > 0 && visibleRows.every((r) => selected.has(r.id));
609
618
  const someSelected = selected.size > 0 && !allSelected;
610
- const toggleAll = () => setSelected(allSelected ? new Set() : new Set(visibleRows.map((r) => r.id)));
619
+ // The header checkbox speaks for the rows you can SEE. Rebuilding the whole
620
+ // set from `visibleRows` would silently drop anything an active filter is
621
+ // hiding -- on a surface with a bulk Delete, that loses a selection the user
622
+ // made. Add/remove the visible rows and leave the rest of the set alone.
623
+ const toggleAll = () => {
624
+ const next = new Set(selected);
625
+ for (const r of visibleRows) {
626
+ if (allSelected)
627
+ next.delete(r.id);
628
+ else
629
+ next.add(r.id);
630
+ }
631
+ setSelected(next);
632
+ };
611
633
  const toggleRow = (id) => {
612
634
  const next = new Set(selected);
613
635
  // shift-click → select the contiguous range from the anchor to this row
@@ -804,7 +826,7 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
804
826
  URL.revokeObjectURL(url);
805
827
  setOpenMenu(null);
806
828
  };
807
- const entityKey = cols.find((c) => c.type === "entity")?.key;
829
+ const entityCol = cols.find((c) => c.type === "entity");
808
830
  // "Add to list" → pin the selected rows (shows a ★ on the entity)
809
831
  const addToList = () => {
810
832
  setPinned((prev) => new Set([...prev, ...selected]));
@@ -814,7 +836,10 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
814
836
  const sendEmail = () => {
815
837
  const names = data
816
838
  .filter((r) => selected.has(r.id))
817
- .map((r) => entityKey ? cellText({ key: entityKey, label: "" }, r) : r.id);
839
+ // Pass the real column: `cellText` -> `rawText` reads `col.type` to pull
840
+ // an entity's `.name`, so a `{key, label}` stand-in with no `type` falls
841
+ // through to String(value) and yields "[object Object]".
842
+ .map((r) => (entityCol ? cellText(entityCol, r) : r.id));
818
843
  window.open(`mailto:?subject=${encodeURIComponent(names.join(", "))}`);
819
844
  };
820
845
  // "Run workflow" → a demo automation: duplicate the selected records
@@ -71,16 +71,30 @@ function formatCell(type, v, symbol) {
71
71
  return asText(v);
72
72
  }
73
73
  /** Built-in typed-cell renderer (used when a column has no custom `render`). */
74
- function renderTyped(col, value, avatarTone) {
74
+ // `value` is always the RAW cell value and `display` the caller's `format`
75
+ // output, if any. They are separate arguments on purpose: the entity and tags
76
+ // branches inspect the value's SHAPE (object / array), so handing them a
77
+ // formatted string used to make them fall through to their empty state and
78
+ // silently drop the cell's data. Each branch decides for itself where a
79
+ // display string belongs -- as an entity's name, a status' label -- while the
80
+ // raw value keeps driving structure and tone.
81
+ function renderTyped(col, value, avatarTone, display) {
75
82
  const empty = _jsx("span", { className: "datatable-muted", children: "\u2014" });
76
83
  switch (col.type) {
77
84
  case "entity": {
78
- const e = (typeof value === "object" && value ? value : { name: asText(value) });
85
+ const raw = (typeof value === "object" && value ? value : { name: asText(value) });
86
+ // a formatted string relabels the entity; its avatar and its hashed tone
87
+ // still come from the real value
88
+ const e = display ? { ...raw, name: display } : raw;
79
89
  if (!e.name)
80
90
  return empty;
81
- return (_jsxs("span", { className: "datatable-entity", children: [_jsx("span", { className: "datatable-avatar", "data-tone": avatarTone === "hashed" ? toneFor(e.name) : undefined, children: e.avatar ? (_jsx("img", { src: e.avatar, alt: "" })) : (initials(e.name).slice(0, 2)) }), _jsx("span", { className: "datatable-entity-name", children: e.name })] }));
91
+ return (_jsxs("span", { className: "datatable-entity", children: [_jsx("span", { className: "datatable-avatar", "data-tone": avatarTone === "hashed" ? toneFor(raw.name) : undefined, children: e.avatar ? (_jsx("img", { src: e.avatar, alt: "" })) : (initials(e.name).slice(0, 2)) }), _jsx("span", { className: "datatable-entity-name", children: e.name })] }));
82
92
  }
83
93
  case "tags": {
94
+ // asking for a format on a tag column means "render it your way" -- show
95
+ // that string rather than trying to split it back into chips
96
+ if (display)
97
+ return display;
84
98
  const tags = Array.isArray(value) ? value : [];
85
99
  if (!tags.length)
86
100
  return empty;
@@ -90,10 +104,12 @@ function renderTyped(col, value, avatarTone) {
90
104
  const s = asText(value);
91
105
  if (!s)
92
106
  return empty;
93
- return (_jsx("span", { className: "datatable-tag", "data-tone": col.tones?.[s] ?? toneFor(s), children: s }));
107
+ // tone is looked up by the RAW status, so formatting the label (casing,
108
+ // translation) can never silently repaint the chip
109
+ return (_jsx("span", { className: "datatable-tag", "data-tone": col.tones?.[s] ?? toneFor(s), children: display || s }));
94
110
  }
95
111
  default: {
96
- const text = formatCell(col.type, value, col.symbol);
112
+ const text = display || formatCell(col.type, value, col.symbol);
97
113
  const isNum = col.type === "number" || col.type === "currency";
98
114
  if (!text)
99
115
  return empty;
@@ -288,7 +304,8 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
288
304
  }
289
305
  /**
290
306
  * Spreadsheet keyboard model, ported from RecordsTable: arrows move the
291
- * selected cell, Enter opens its editor, Escape clears, and Cmd/Ctrl+C
307
+ * selected cell, Enter opens its editor (or, on a non-editable cell,
308
+ * activates the row via `onRowClick`), Escape clears, and Cmd/Ctrl+C
292
309
  * copies the cell's RENDERED text (what the user sees, not the raw value).
293
310
  */
294
311
  function onGridKeyDown(e) {
@@ -325,6 +342,10 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
325
342
  e.preventDefault();
326
343
  setEditing({ id: selCell.id, key: selCell.key });
327
344
  }
345
+ else if (onRowClick) {
346
+ e.preventDefault();
347
+ onRowClick(rows[r]);
348
+ }
328
349
  return;
329
350
  }
330
351
  else
@@ -466,7 +487,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
466
487
  }
467
488
  }, children: col.render
468
489
  ? col.render(value, row)
469
- : renderTyped(col, col.format ? col.format(value, row) : value, avatarTone) }, col.key));
490
+ : renderTyped(col, value, avatarTone, col.format?.(value, row)) }, col.key));
470
491
  })] }, row.id)))] }, group ?? "__ungrouped")))) }), (hasSummary || onAddRow) && (_jsxs(TableFooter, { children: [hasSummary && (_jsxs(TableRow, { children: [showGrip && _jsx(TableCell, { ...stickyCol(-2) }), selectable && _jsx(TableCell, { ...stickyCol(-1) }), columns.map((col, i) => (_jsx(TableCell, { align: alignFor(col), ...stickyCol(i), children: col.summary ? col.summary(rows) : null }, col.key)))] })), onAddRow && (_jsx(TableRow, { children: _jsx(TableCell, { className: "datatable-addcell", colSpan: totalCols, children: _jsxs("button", { type: "button", className: "datatable-addrow", onClick: onAddRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), addRowLabel] }) }) }))] }))] })] }));
471
492
  }
472
493
  DataTable.displayName = "DataTable";
@@ -356,6 +356,15 @@ function CreateRecordDialog({ open, onOpenChange, columns, entityLabel, onCreate
356
356
  export function RecordsTable({ labels, columns: columnsProp = defaultColumns, rows: rowsProp = defaultRows, selectedIds, onSelectionChange, showNew = true, allowAddColumn = true, onOpenRecord, onOpenRecordPage, avatarTone = "brand", className, }) {
357
357
  const [cols, setCols] = React.useState(columnsProp);
358
358
  const [data, setData] = React.useState(rowsProp);
359
+ // The table owns `cols`/`data` so it can rename a column, edit a cell, or
360
+ // append a record without a round trip. That state is seeded from the props,
361
+ // so without these it would be a mount-time SNAPSHOT: an app that renders
362
+ // `rows={[]}` and re-renders with fetched rows would show "No records yet"
363
+ // for ever. Re-seed whenever the caller hands us a new array -- caller wins,
364
+ // exactly as DataTable does for its columns. Local edits survive until then,
365
+ // because a caller passing a stable array never triggers this.
366
+ React.useEffect(() => setCols(columnsProp), [columnsProp]);
367
+ React.useEffect(() => setData(rowsProp), [rowsProp]);
359
368
  // Attio's multi-criteria sort: an ordered list of {field, direction} rows.
360
369
  const [sorts, setSorts] = React.useState([]);
361
370
  const [dragSort, setDragSort] = React.useState(null);
@@ -569,7 +578,20 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
569
578
  };
570
579
  const allSelected = visibleRows.length > 0 && visibleRows.every((r) => selected.has(r.id));
571
580
  const someSelected = selected.size > 0 && !allSelected;
572
- const toggleAll = () => setSelected(allSelected ? new Set() : new Set(visibleRows.map((r) => r.id)));
581
+ // The header checkbox speaks for the rows you can SEE. Rebuilding the whole
582
+ // set from `visibleRows` would silently drop anything an active filter is
583
+ // hiding -- on a surface with a bulk Delete, that loses a selection the user
584
+ // made. Add/remove the visible rows and leave the rest of the set alone.
585
+ const toggleAll = () => {
586
+ const next = new Set(selected);
587
+ for (const r of visibleRows) {
588
+ if (allSelected)
589
+ next.delete(r.id);
590
+ else
591
+ next.add(r.id);
592
+ }
593
+ setSelected(next);
594
+ };
573
595
  const toggleRow = (id) => {
574
596
  const next = new Set(selected);
575
597
  // shift-click → select the contiguous range from the anchor to this row
@@ -766,7 +788,7 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
766
788
  URL.revokeObjectURL(url);
767
789
  setOpenMenu(null);
768
790
  };
769
- const entityKey = cols.find((c) => c.type === "entity")?.key;
791
+ const entityCol = cols.find((c) => c.type === "entity");
770
792
  // "Add to list" → pin the selected rows (shows a ★ on the entity)
771
793
  const addToList = () => {
772
794
  setPinned((prev) => new Set([...prev, ...selected]));
@@ -776,7 +798,10 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
776
798
  const sendEmail = () => {
777
799
  const names = data
778
800
  .filter((r) => selected.has(r.id))
779
- .map((r) => entityKey ? cellText({ key: entityKey, label: "" }, r) : r.id);
801
+ // Pass the real column: `cellText` -> `rawText` reads `col.type` to pull
802
+ // an entity's `.name`, so a `{key, label}` stand-in with no `type` falls
803
+ // through to String(value) and yields "[object Object]".
804
+ .map((r) => (entityCol ? cellText(entityCol, r) : r.id));
780
805
  window.open(`mailto:?subject=${encodeURIComponent(names.join(", "))}`);
781
806
  };
782
807
  // "Run workflow" → a demo automation: duplicate the selected records
@@ -39,7 +39,9 @@ export type DataTableColumn<T> = {
39
39
  accessor?: (row: T) => string | number;
40
40
  /** Transform the value into a display string (locale formatting, masking, a
41
41
  * unit suffix, …); the result still flows through the built-in type
42
- * renderer, so a `number`/`currency` column keeps its alignment + chrome.
42
+ * renderer, so a `number`/`currency` column keeps its alignment + chrome, an
43
+ * `entity` keeps its avatar, and a `status` keeps the tone its raw value maps
44
+ * to. A `tags` column renders the string as-is instead of its chips.
43
45
  * Precedence: `render` (full custom node) beats `format` (value string)
44
46
  * beats the built-in type renderer. Display-only — sorting and filtering
45
47
  * use the raw/`accessor` value, never the formatted string. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -87,6 +87,11 @@
87
87
  "import": "./dist/esm/components/checkbox/index.js",
88
88
  "require": "./dist/cjs/components/checkbox/index.js"
89
89
  },
90
+ "./columncalc": {
91
+ "types": "./dist/types/components/columncalc/index.d.ts",
92
+ "import": "./dist/esm/components/columncalc/index.js",
93
+ "require": "./dist/cjs/components/columncalc/index.js"
94
+ },
90
95
  "./contextmenu": {
91
96
  "types": "./dist/types/components/contextmenu/index.d.ts",
92
97
  "import": "./dist/esm/components/contextmenu/index.js",
@@ -257,6 +262,11 @@
257
262
  "import": "./dist/esm/product/appshell/index.js",
258
263
  "require": "./dist/cjs/product/appshell/index.js"
259
264
  },
265
+ "./authshell": {
266
+ "types": "./dist/types/product/authshell/index.d.ts",
267
+ "import": "./dist/esm/product/authshell/index.js",
268
+ "require": "./dist/cjs/product/authshell/index.js"
269
+ },
260
270
  "./board": {
261
271
  "types": "./dist/types/product/board/index.d.ts",
262
272
  "import": "./dist/esm/product/board/index.js",