@crisp-ui-kit/crisp 0.54.2 → 0.54.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/board.css +13 -1
  2. package/dist/board.layered.css +13 -1
  3. package/dist/chip.css +7 -1
  4. package/dist/chip.d.ts +2 -1
  5. package/dist/chip.layered.css +7 -1
  6. package/dist/cjs/components/chip/chip.js +3 -2
  7. package/dist/cjs/components/chip/chip.recipe.js +37 -1
  8. package/dist/cjs/components/datatable/datatable.js +489 -111
  9. package/dist/cjs/components/emptystate/emptystate.js +3 -2
  10. package/dist/cjs/components/emptystate/emptystate.recipe.js +26 -1
  11. package/dist/cjs/components/personchip/index.js +17 -0
  12. package/dist/cjs/components/personchip/personchip.js +50 -0
  13. package/dist/cjs/components/table/table.js +2 -2
  14. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +41 -4
  15. package/dist/cjs/components/tabs/tabs.js +2 -2
  16. package/dist/cjs/components/tabs/tabs.recipe.js +30 -3
  17. package/dist/cjs/components/text/index.js +7 -0
  18. package/dist/cjs/components/text/text.js +55 -0
  19. package/dist/cjs/components/text/text.recipe.js +34 -0
  20. package/dist/cjs/index.js +4 -0
  21. package/dist/cjs/product/board/board.js +60 -2
  22. package/dist/cjs/product/board/board.recipe.js +10 -0
  23. package/dist/cjs/product/comments/comments.js +426 -0
  24. package/dist/cjs/product/comments/comments.recipe.js +126 -0
  25. package/dist/cjs/product/comments/index.js +8 -0
  26. package/dist/cjs/product/headerbar/headerbar.recipe.js +9 -1
  27. package/dist/cjs/product/listpage/listpage.recipe.js +10 -1
  28. package/dist/cjs/product/objectsettings/index.js +16 -0
  29. package/dist/cjs/product/objectsettings/objectsettings.js +365 -0
  30. package/dist/cjs/product/objectsettings/objectsettings.recipe.js +65 -0
  31. package/dist/cjs/product/recordpage/recordpage.recipe.js +10 -1
  32. package/dist/cjs/product/recordstable/recordstable.js +89 -6
  33. package/dist/cjs/product/richtext/richtext.recipe.js +26 -1
  34. package/dist/cjs/product/taskstable/taskstable.recipe.js +10 -1
  35. package/dist/comments.css +80 -0
  36. package/dist/comments.d.ts +7 -0
  37. package/dist/comments.layered.css +82 -0
  38. package/dist/emptystate.css +15 -1
  39. package/dist/emptystate.d.ts +2 -2
  40. package/dist/emptystate.layered.css +15 -1
  41. package/dist/esm/components/chip/chip.js +3 -2
  42. package/dist/esm/components/chip/chip.recipe.js +37 -1
  43. package/dist/esm/components/datatable/datatable.js +488 -111
  44. package/dist/esm/components/emptystate/emptystate.js +4 -3
  45. package/dist/esm/components/emptystate/emptystate.recipe.js +26 -1
  46. package/dist/esm/components/emptystate/index.js +1 -1
  47. package/dist/esm/components/personchip/index.js +1 -0
  48. package/dist/esm/components/personchip/personchip.js +47 -0
  49. package/dist/esm/components/table/table.js +2 -2
  50. package/dist/esm/components/tabletoolbar/tabletoolbar.js +43 -6
  51. package/dist/esm/components/tabs/tabs.js +2 -2
  52. package/dist/esm/components/tabs/tabs.recipe.js +30 -3
  53. package/dist/esm/components/text/index.js +2 -0
  54. package/dist/esm/components/text/text.js +19 -0
  55. package/dist/esm/components/text/text.recipe.js +31 -0
  56. package/dist/esm/index.js +4 -0
  57. package/dist/esm/product/board/board.js +61 -3
  58. package/dist/esm/product/board/board.recipe.js +10 -0
  59. package/dist/esm/product/comments/comments.js +388 -0
  60. package/dist/esm/product/comments/comments.recipe.js +123 -0
  61. package/dist/esm/product/comments/index.js +2 -0
  62. package/dist/esm/product/headerbar/headerbar.recipe.js +9 -1
  63. package/dist/esm/product/listpage/listpage.recipe.js +10 -1
  64. package/dist/esm/product/objectsettings/index.js +2 -0
  65. package/dist/esm/product/objectsettings/objectsettings.js +322 -0
  66. package/dist/esm/product/objectsettings/objectsettings.recipe.js +62 -0
  67. package/dist/esm/product/recordpage/recordpage.recipe.js +10 -1
  68. package/dist/esm/product/recordstable/recordstable.js +90 -8
  69. package/dist/esm/product/richtext/richtext.recipe.js +26 -1
  70. package/dist/esm/product/taskstable/taskstable.recipe.js +10 -1
  71. package/dist/headerbar.css +1 -1
  72. package/dist/headerbar.layered.css +1 -1
  73. package/dist/listpage.css +8 -1
  74. package/dist/listpage.layered.css +8 -1
  75. package/dist/objectsettings.css +49 -0
  76. package/dist/objectsettings.d.ts +7 -0
  77. package/dist/objectsettings.layered.css +51 -0
  78. package/dist/recordpage.css +13 -2
  79. package/dist/recordpage.layered.css +13 -2
  80. package/dist/richtext.css +11 -1
  81. package/dist/richtext.layered.css +11 -1
  82. package/dist/styles.css +224 -10
  83. package/dist/styles.layered.css +224 -10
  84. package/dist/tabs.css +7 -1
  85. package/dist/tabs.d.ts +2 -2
  86. package/dist/tabs.layered.css +7 -1
  87. package/dist/taskstable.css +8 -1
  88. package/dist/taskstable.layered.css +8 -1
  89. package/dist/text.css +9 -0
  90. package/dist/text.d.ts +7 -0
  91. package/dist/text.layered.css +11 -0
  92. package/dist/types/components/chip/chip.d.ts +32 -0
  93. package/dist/types/components/chip/chip.recipe.d.ts +31 -0
  94. package/dist/types/components/datatable/datatable.d.ts +28 -4
  95. package/dist/types/components/emptystate/emptystate.d.ts +29 -4
  96. package/dist/types/components/emptystate/emptystate.recipe.d.ts +2 -0
  97. package/dist/types/components/emptystate/index.d.ts +1 -1
  98. package/dist/types/components/personchip/index.d.ts +1 -0
  99. package/dist/types/components/personchip/personchip.d.ts +37 -0
  100. package/dist/types/components/table/table.d.ts +36 -0
  101. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +8 -2
  102. package/dist/types/components/tabs/tabs.d.ts +4 -2
  103. package/dist/types/components/tabs/tabs.recipe.d.ts +18 -3
  104. package/dist/types/components/text/index.d.ts +2 -0
  105. package/dist/types/components/text/text.d.ts +32 -0
  106. package/dist/types/components/text/text.recipe.d.ts +40 -0
  107. package/dist/types/index.d.ts +4 -0
  108. package/dist/types/product/board/board.d.ts +20 -1
  109. package/dist/types/product/comments/comments.d.ts +192 -0
  110. package/dist/types/product/comments/comments.recipe.d.ts +3 -0
  111. package/dist/types/product/comments/index.d.ts +2 -0
  112. package/dist/types/product/objectsettings/index.d.ts +2 -0
  113. package/dist/types/product/objectsettings/objectsettings.d.ts +261 -0
  114. package/dist/types/product/objectsettings/objectsettings.recipe.d.ts +3 -0
  115. package/dist/types/product/recordstable/recordstable.d.ts +24 -2
  116. package/package.json +21 -1
@@ -4,7 +4,8 @@ exports.EmptyState = EmptyState;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const emptystate_recipe_1 = require("./emptystate.recipe");
6
6
  /** A centred empty-state panel — icon, title, description, optional action. */
7
- function EmptyState({ icon, title, description, action, variant, className, ...rest }) {
8
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, emptystate_recipe_1.emptystateRecipe)({ variant, className }), ...rest, children: [icon && (0, jsx_runtime_1.jsx)("span", { className: "crisp-emptystate-icon", children: icon }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-emptystate-title", children: title }), description != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-emptystate-desc", children: description })), action && (0, jsx_runtime_1.jsx)("div", { className: "crisp-emptystate-action", children: action })] }));
7
+ function EmptyState({ icon, title, description, action, learnMore, variant, className, ...rest }) {
8
+ const learnMoreInner = learnMore && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-emptystate-learnmore-icon", children: learnMore.icon }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-emptystate-learnmore-title", children: learnMore.title })] }));
9
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, emptystate_recipe_1.emptystateRecipe)({ variant, className }), ...rest, children: [icon && (0, jsx_runtime_1.jsx)("span", { className: "crisp-emptystate-icon", children: icon }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-emptystate-title", children: title }), description != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-emptystate-desc", children: description })), action && (0, jsx_runtime_1.jsx)("div", { className: "crisp-emptystate-action", children: action }), learnMore && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-emptystate-learnmore", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-emptystate-learnmore-label", children: learnMore.label ?? "Learn more" }), learnMore.href ? ((0, jsx_runtime_1.jsx)("a", { className: "crisp-emptystate-learnmore-card", href: learnMore.href, onClick: learnMore.onClick, children: learnMoreInner })) : ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-emptystate-learnmore-card", onClick: learnMore.onClick, children: learnMoreInner }))] }))] }));
9
10
  }
10
11
  EmptyState.displayName = "EmptyState";
@@ -10,7 +10,18 @@ const config = (0, core_1.defineRecipe)({
10
10
  & .crisp-emptystate-icon { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; margin-bottom: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.xl}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 5%, transparent); color: ${tokens_1.contract.fgMuted}; }
11
11
  & .crisp-emptystate-title { font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.semibold}; color: ${tokens_1.contract.fgPrimary}; }
12
12
  & .crisp-emptystate-desc { max-width: 320px; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }
13
- & .crisp-emptystate-action { margin-top: ${tokens_1.contract.space["2"]}; }`,
13
+ /* #603: measured 8px gap between "Import CSV"/"Connect data source" --
14
+ inline-flex so one action (the common case) or several lay out the
15
+ same way, wrapping if the container is narrow. */
16
+ & .crisp-emptystate-action { display: inline-flex; align-items: center; flex-wrap: wrap; justify-content: center; gap: ${tokens_1.contract.space["2"]}; margin-top: ${tokens_1.contract.space["2"]}; }
17
+ /* #603: Attio's zero-records "Learn more" card -- a label above a
18
+ clickable icon+title tile. Left/start-aligned, unlike the rest of
19
+ this centred panel, so it overrides the base text-align. */
20
+ & .crisp-emptystate-learnmore { display: flex; flex-direction: column; align-items: stretch; gap: ${tokens_1.contract.space["2"]}; margin-top: ${tokens_1.contract.space["6"]}; text-align: start; }
21
+ & .crisp-emptystate-learnmore-label { font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 50%, transparent); }
22
+ & .crisp-emptystate-learnmore-card { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["2"]}; margin: 0; border: 1px solid ${tokens_1.contract.borderSubtle}; border-radius: 18px; background: ${tokens_1.contract.bgSurface}; font: inherit; text-align: start; text-decoration: none; color: inherit; cursor: pointer; }
23
+ & .crisp-emptystate-learnmore-icon { display: inline-flex; align-items: center; justify-content: center; width: 56px; height: 56px; flex-shrink: 0; border-radius: ${tokens_1.contract.radius.xl}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 2%, transparent); color: ${tokens_1.contract.fgMuted}; }
24
+ & .crisp-emptystate-learnmore-title { font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary}; }`,
14
25
  variants: {
15
26
  variant: {
16
27
  // compact icon tile (default)
@@ -25,6 +36,20 @@ const config = (0, core_1.defineRecipe)({
25
36
  & .crisp-emptystate-title { font-size: 20px; line-height: 24px; }
26
37
  & .crisp-emptystate-desc { max-width: 266px; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; line-height: 20px; }
27
38
  & .crisp-emptystate-action { margin-top: ${tokens_1.contract.space["2"]}; }`,
39
+ // #603: Attio's zero-*records* table state (Users/Workspaces with
40
+ // nothing imported) -- the same dotted-grid backdrop `illustrated`
41
+ // uses, at its own measured 138x138 square (illustrated's is a
42
+ // 132x96 rectangle; different surface, not the same number).
43
+ // Attio's own illustration also draws four small "+" tick marks at
44
+ // the tile's corners -- a single proprietary SVG asset (confirmed
45
+ // by inspecting the live DOM: one <svg> with no separately
46
+ // measurable sub-elements), so not reproduced here per AUTHORING's
47
+ // "never copy Attio's icon set" rule; the icon itself is supplied
48
+ // by the consumer via the existing `icon` prop, same as every
49
+ // other variant.
50
+ records: `& .crisp-emptystate-icon { width: 138px; height: 138px; margin-bottom: ${tokens_1.contract.space["2"]}; border-radius: 0; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 22%, transparent); background: radial-gradient(circle, color-mix(in srgb, ${tokens_1.contract.fgPrimary} 12%, transparent) 1.2px, transparent 1.2px) 0 0 / 16px 16px; }
51
+ & .crisp-emptystate-title { font-size: 20px; line-height: 24px; }
52
+ & .crisp-emptystate-desc { max-width: 266px; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; line-height: 20px; }`,
28
53
  },
29
54
  },
30
55
  defaultVariants: { variant: "default" },
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./personchip"), exports);
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PersonChip = PersonChip;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const tokens_1 = require("../../tokens");
6
+ const avatar_1 = require("../avatar");
7
+ const chip_1 = require("../chip");
8
+ // The same five hashed avatar tones DataTable's own `avatarTone="hashed"`
9
+ // picks from (datatable.tsx's private `TAG_TONES`, transcribed rather than
10
+ // imported — DataTable owns its own file, and this is a one-line pure
11
+ // function, not a visual value that could drift). `neutral`/`brand` are
12
+ // left out: `neutral` is Avatar's own no-photo default (wrong here, every
13
+ // name needs a colour to tell people apart) and `brand` is reserved for an
14
+ // entity that IS the record a cell points at (`RecordChip`'s own tone),
15
+ // which a person mention never is.
16
+ const HASH_TONES = ["brand", "orange", "cyan", "purple", "green"];
17
+ function hashTone(name) {
18
+ let h = 0;
19
+ for (let i = 0; i < name.length; i++)
20
+ h = (h * 31 + name.charCodeAt(i)) >>> 0;
21
+ return HASH_TONES[h % HASH_TONES.length];
22
+ }
23
+ function initials(name) {
24
+ const parts = name.trim().split(/\s+/).filter(Boolean);
25
+ if (parts.length === 0)
26
+ return "?";
27
+ return (parts[0][0] + (parts[1]?.[0] ?? "")).toUpperCase();
28
+ }
29
+ /**
30
+ * A person, as a chip: a small avatar (a photo, or hashed-colour initials)
31
+ * plus their name, truncating with an ellipsis when it doesn't fit — Attio's
32
+ * own actor chip (measured live on `techpartners`, Tasks page, "Assigned to"
33
+ * column): a 16px circular avatar in a neutral `Chip`, `gap: 3px` between
34
+ * avatar and name (now `Chip`'s own base — every multi-child `Chip` gets
35
+ * it). Every other value — the pill's height, padding, radius, ring and text
36
+ * — is already `Chip`'s, unchanged; `PersonChip` adds no recipe of its own.
37
+ *
38
+ * Ellipsis only, no `Tooltip` on hover (#623 — no Attio reference for one;
39
+ * a truncated cell there is revealed by its own click-to-expand, #579, a
40
+ * different, DataTable-specific interaction).
41
+ *
42
+ * Composes `Chip`'s `truncate` CSS directly rather than its string-only
43
+ * auto-wrap (children here is an avatar plus a name, not a single string),
44
+ * so the name gets its own `.crisp-chip-label` span while the avatar stays
45
+ * fixed-size.
46
+ */
47
+ function PersonChip({ name, avatar, muted, className, ...props }) {
48
+ return ((0, jsx_runtime_1.jsxs)(chip_1.Chip, { tone: "neutral", truncate: true, className: className, ...props, children: [(0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "sm", shape: "circle", tone: avatar ? "neutral" : hashTone(name), children: avatar ? (0, jsx_runtime_1.jsx)("img", { src: avatar, alt: "" }) : initials(name) }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-chip-label", style: muted ? { color: tokens_1.contract.fgNeutralMuted } : undefined, children: name })] }));
49
+ }
50
+ PersonChip.displayName = "PersonChip";
@@ -48,11 +48,11 @@ exports.Table = React.forwardRef(({ interactive, surface, className, wrapperClas
48
48
  // on the root for the same reason as Textarea: the wrapper is the box a
49
49
  // consumer sees and the scroll container they want to size. Native
50
50
  // table attributes still reach the <table> below.
51
- style, ...props }, ref) => ((0, jsx_runtime_1.jsx)("div", { className: (0, table_recipe_1.tableRecipe)({
51
+ style, tableStyle, ...props }, ref) => ((0, jsx_runtime_1.jsx)("div", { className: (0, table_recipe_1.tableRecipe)({
52
52
  interactive,
53
53
  surface,
54
54
  className: wrapperClassName,
55
- }), style: style, children: (0, jsx_runtime_1.jsx)("div", { ...scrollProps, className: `crisp-table-scroll${scrollProps?.className ? ` ${scrollProps.className}` : ""}`, children: (0, jsx_runtime_1.jsx)("table", { ref: ref, className: className, ...props, children: children }) }) })));
55
+ }), style: style, children: (0, jsx_runtime_1.jsx)("div", { ...scrollProps, className: `crisp-table-scroll${scrollProps?.className ? ` ${scrollProps.className}` : ""}`, children: (0, jsx_runtime_1.jsx)("table", { ref: ref, className: className, style: tableStyle, ...props, children: children }) }) })));
56
56
  exports.Table.displayName = "Table";
57
57
  exports.TableHeader = React.forwardRef((props, ref) => (0, jsx_runtime_1.jsx)("thead", { ref: ref, ...props }));
58
58
  exports.TableHeader.displayName = "TableHeader";
@@ -46,14 +46,24 @@ const select_1 = require("../select");
46
46
  const tabletoolbar_recipe_1 = require("./tabletoolbar.recipe");
47
47
  const OP_LABELS = {
48
48
  contains: "contains",
49
+ doesnotcontain: "does not contain",
50
+ startswith: "starts with",
51
+ endswith: "ends with",
49
52
  is: "is",
50
53
  isnot: "is not",
51
54
  isanyof: "is any of",
52
55
  empty: "is empty",
53
56
  notempty: "is not empty",
54
57
  };
58
+ // #619: order and label set match Attio's own text-filter operator picker
59
+ // verbatim (measured live, per RecordsTable's own #578 port of the same
60
+ // set: contains · does not contain · starts with · ends with · is · is not ·
61
+ // empty · not empty).
55
62
  const OP_ORDER = [
56
63
  "contains",
64
+ "doesnotcontain",
65
+ "startswith",
66
+ "endswith",
57
67
  "is",
58
68
  "isnot",
59
69
  "empty",
@@ -62,7 +72,12 @@ const OP_ORDER = [
62
72
  // "isanyof" is appended only for a column that supplies `values` — there is
63
73
  // nothing to check off otherwise.
64
74
  const opsFor = (col) => col?.values?.length ? [...OP_ORDER, "isanyof"] : OP_ORDER;
65
- const opNeedsValue = (op) => op === "contains" || op === "is" || op === "isnot";
75
+ const opNeedsValue = (op) => op === "contains" ||
76
+ op === "doesnotcontain" ||
77
+ op === "startswith" ||
78
+ op === "endswith" ||
79
+ op === "is" ||
80
+ op === "isnot";
66
81
  const opNeedsValues = (op) => op === "isanyof";
67
82
  // "Department is any of IT, Finance" past two values becomes "… +N": the
68
83
  // chip is one line, and a field can have far more than two facet values.
@@ -110,9 +125,15 @@ function applyFilters(rows, filters, get = (row, key) => row[key]) {
110
125
  const v = f.value.trim().toLowerCase();
111
126
  if (f.op === "contains")
112
127
  return t.includes(v);
128
+ if (f.op === "doesnotcontain")
129
+ return !t.includes(v);
130
+ if (f.op === "startswith")
131
+ return t.startsWith(v);
132
+ if (f.op === "endswith")
133
+ return t.endsWith(v);
113
134
  if (f.op === "is")
114
135
  return t === v;
115
- return t !== v;
136
+ return t !== v; // isnot
116
137
  }));
117
138
  }
118
139
  /**
@@ -156,7 +177,7 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
156
177
  * this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
157
178
  * Created by), which one shared list can't reproduce.
158
179
  */
159
- function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, ...rest }) {
180
+ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, ...rest }) {
160
181
  // crisp/#471: an explicit prop always wins; missing, it reads the nearest
161
182
  // CrispMessagesProvider's translation of the English default below, or
162
183
  // that default itself with no provider — unchanged from before this
@@ -175,6 +196,8 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
175
196
  const filterFieldLabel = (0, core_1.useCrispMessage)(filterFieldLabelProp, "Filter by field");
176
197
  const filterFieldPlaceholder = (0, core_1.useCrispMessage)(filterFieldPlaceholderProp, "Filter by…");
177
198
  const filterValuePlaceholder = (0, core_1.useCrispMessage)(filterValuePlaceholderProp, "Value…");
199
+ const filterOptionsLabel = (0, core_1.useCrispMessage)(filterOptionsLabelProp, "Filter options");
200
+ const deleteFilterLabel = (0, core_1.useCrispMessage)(deleteFilterLabelProp, "Delete filter");
178
201
  const ascendingLabel = (0, core_1.useCrispMessage)(ascendingLabelProp, "Ascending");
179
202
  const descendingLabel = (0, core_1.useCrispMessage)(descendingLabelProp, "Descending");
180
203
  // OP_LABELS' own words ("contains", "is", "is any of", …) have no prop of
@@ -269,6 +292,19 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
269
292
  putFilters(filters.filter((c) => c.id !== id));
270
293
  setOpenMenu(null);
271
294
  };
295
+ // #594: the filter chip's kebab is a real role="menu" (crisp/#188's shared
296
+ // model — arrows, Home/End, typeahead, Escape), not a hand-rolled div like
297
+ // the sort/condition popovers above (those are role="dialog", which makes
298
+ // no such promise). `openMenu` already only ever names ONE open surface at
299
+ // a time, so one hook call at the top level — not one per filter row,
300
+ // which would break the Rules of Hooks inside `filters.map` below — tracks
301
+ // whichever chip's kebab is currently open via its content ref.
302
+ const kebabMenuRef = React.useRef(null);
303
+ const { onKeyDown: onKebabKeyDown } = (0, menu_1.useMenuKeyboard)({
304
+ open: typeof openMenu === "string" && openMenu.startsWith("fkebab:"),
305
+ close: () => setOpenMenu(null),
306
+ contentRef: kebabMenuRef,
307
+ });
272
308
  const activeFilters = filters.length;
273
309
  // The default phrase only works in English word order, so a caller can
274
310
  // replace the whole thing rather than fill a template — crisp/#471's
@@ -318,10 +354,11 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
318
354
  .map((col) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => addFilter(col.key), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), col.label] }, col.key)))] }))] }), filters.map((cond) => {
319
355
  const col = filterColumns.find((c) => c.key === cond.key);
320
356
  const open = openMenu === `fcond:${cond.id}`;
357
+ const kebabOpen = openMenu === `fkebab:${cond.id}`;
321
358
  const hasValue = (!opNeedsValue(cond.op) || cond.value.trim()) &&
322
359
  (!opNeedsValues(cond.op) || (cond.values?.length ?? 0) > 0);
323
360
  return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-fchip", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-fchip-main", "data-menu-trigger": true, "aria-haspopup": "dialog", onClick: () => setOpenMenu(open ? null : `fcond:${cond.id}`), children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-field", children: col?.label }), hasValue ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: crispMessages[OP_LABELS[cond.op]] ??
324
- OP_LABELS[cond.op] }), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: cond.value })), cond.op === "isanyof" && cond.values && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: isAnyOfSummary(cond.values) }))] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: selectConditionLabel }))] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fchip-x", "aria-label": `${crispMessages.Remove ?? "Remove"} ${col?.label} ${crispMessages.filter ?? "filter"}`, onClick: () => removeFilter(cond.id), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 2 }) })] }), open && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu-head", children: [col?.label, " ", crispMessages.condition ?? "condition"] }), opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(cond.id, { op }), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
361
+ OP_LABELS[cond.op] }), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: cond.value })), cond.op === "isanyof" && cond.values && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: isAnyOfSummary(cond.values) }))] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: selectConditionLabel }))] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fchip-x", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && ((0, jsx_runtime_1.jsx)("div", { ref: kebabMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--right crisp-tabletoolbar-menu--hdr", role: "menu", "aria-label": `${col?.label} ${filterOptionsLabel}`, onKeyDown: onKebabKeyDown, children: (0, jsx_runtime_1.jsx)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => removeFilter(cond.id), children: deleteFilterLabel }) })), open && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu-head", children: [col?.label, " ", crispMessages.condition ?? "condition"] }), opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(cond.id, { op }), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
325
362
  // biome-ignore lint/a11y/noAutofocus: focus the value field
326
363
  autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })), cond.op === "isanyof" && col?.values && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
327
364
  // biome-ignore lint/a11y/noAutofocus: focus the search field
@@ -44,7 +44,7 @@ const tabs_keyboard_1 = require("./tabs-keyboard");
44
44
  * Home/End jump); activation follows focus. Controlled via
45
45
  * `value`/`onValueChange`, or uncontrolled from `defaultValue`.
46
46
  */
47
- function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, className, "aria-label": ariaLabel, ...rest }) {
47
+ function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, className, fill, "aria-label": ariaLabel, ...rest }) {
48
48
  // Scoped to this instance: `tab-${value}` and `panel-${value}` are global,
49
49
  // so two tab bars sharing a value mint the same id twice and every
50
50
  // aria-labelledby resolves to the first one (#227).
@@ -83,7 +83,7 @@ function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, cla
83
83
  select,
84
84
  listRef,
85
85
  });
86
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, tabs_recipe_1.tabsRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { ref: listRef, className: "crisp-tabs-list", role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabs-leading", children: leading }), tabs.map((t) => {
86
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, tabs_recipe_1.tabsRecipe)({ fill, className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { ref: listRef, className: "crisp-tabs-list", role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabs-leading", children: leading }), tabs.map((t) => {
87
87
  const selected = t.value === active;
88
88
  return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "tab", id: tabId(t.value), "aria-selected": selected, "aria-controls": children && selected ? panelId(t.value) : undefined, tabIndex: tabIndexFor(t.value), "data-value": t.value, "data-selected": selected ? "" : undefined, disabled: t.disabled, className: "crisp-tabs-tab", onClick: () => select(t.value), children: [t.icon, (0, jsx_runtime_1.jsx)("span", { className: "tabs-tab-label", children: t.label }), t.badge != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabs-tab-badge", children: t.badge }))] }, t.value));
89
89
  }), indicator && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabs-indicator", style: {
@@ -15,6 +15,22 @@ const tokens_1 = require("../../tokens");
15
15
  // a 16px leading icon and an optional count badge. Measured from the live
16
16
  // app: bar `padding: 0 12px 8px`, 28px pills, 4/8 padding, 6px icon-label
17
17
  // gap, 14/500 labels, 10px badges (rgba(0,0,0,.04)).
18
+ //
19
+ // The panel's own `padding-top` (`c.space["8"]`, 32px) is measured
20
+ // separately, live, on a company record page (Overview tab): the gap from
21
+ // the tab row's own bottom to the content below it (the "Highlights"
22
+ // heading) is a clean 32px with zero margin/padding contributed by either
23
+ // boundary — not the 12px a GRC consumer had been carrying locally, which
24
+ // turned out (checking its own history) to have been measured against that
25
+ // app's own demo rendering, never against Attio itself.
26
+ //
27
+ // The panel is also `display: flex; flex-direction: column` (structural,
28
+ // not measured): a consumer's own content — a table sizing itself to "the
29
+ // rest of the space" — needs a flex-column ancestor with a real height to
30
+ // grow into. A GRC consumer used to supply that itself, wrapping every
31
+ // `children(active)` in its own `<div className="flex h-full min-h-0
32
+ // flex-col">`; dropping that wrapper in favour of `fill` without this would
33
+ // have silently broken every such table the moment the wrapper went away.
18
34
  const config = (0, core_1.defineRecipe)({
19
35
  name: "tabs",
20
36
  base: `
@@ -32,11 +48,22 @@ const config = (0, core_1.defineRecipe)({
32
48
  & .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.sm}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); font-size: ${tokens_1.contract.text.xs}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
33
49
  & .crisp-tabs-tab[data-selected] .crisp-tabs-tab-badge { color: ${tokens_1.contract.fgPrimary}; }
34
50
  & .crisp-tabs-indicator { position: absolute; bottom: 0; left: 0; z-index: 1; height: 1px; background: ${tokens_1.contract.fgPrimary}; border-radius: 2px 2px 0 0; transition: transform .2s cubic-bezier(.4,0,.2,1), width .2s cubic-bezier(.4,0,.2,1); pointer-events: none; }
35
- & .crisp-tabs-panel { flex: 1; min-height: 0; }
51
+ & .crisp-tabs-panel { display: flex; flex-direction: column; flex: 1; min-height: 0; padding-top: ${tokens_1.contract.space["8"]}; }
36
52
  & .crisp-tabs-panel:focus-visible { outline: none; }
37
53
  `,
38
- variants: {},
39
- defaultVariants: {},
54
+ variants: {
55
+ // Opt-in: a `Tabs` dropped into a document-flow layout (a docs page, a
56
+ // demo) should size to its content like anything else. `fill` is for a
57
+ // consumer embedding it in a fixed-height shell — a record page, a full
58
+ // screen — matching the record page's own tab area, which visibly
59
+ // fills all available height (the same live check this file's panel
60
+ // measurement came from).
61
+ fill: {
62
+ true: `height: 100%;`,
63
+ false: "",
64
+ },
65
+ },
66
+ defaultVariants: { fill: false },
40
67
  compoundVariants: [],
41
68
  });
42
69
  exports.tabsRecipe = Object.assign((0, core_1.resolveRecipe)(config), { config });
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.textRecipe = exports.Text = void 0;
4
+ var text_1 = require("./text");
5
+ Object.defineProperty(exports, "Text", { enumerable: true, get: function () { return text_1.Text; } });
6
+ var text_recipe_1 = require("./text.recipe");
7
+ Object.defineProperty(exports, "textRecipe", { enumerable: true, get: function () { return text_recipe_1.textRecipe; } });
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Text = void 0;
37
+ const jsx_runtime_1 = require("react/jsx-runtime");
38
+ const React = __importStar(require("react"));
39
+ const core_1 = require("../../core");
40
+ const text_recipe_1 = require("./text.recipe");
41
+ /**
42
+ * Plain coloured text — `tone="default"` (the app's primary text colour) or
43
+ * `"muted"` — for the runs of text that aren't a Badge, a Chip, a link, or
44
+ * any of crisp's other typed things: a cell's own yes/no, an empty-state
45
+ * caption, a secondary line under a heading. One component so every such
46
+ * spot reaches for crisp's own tokens instead of each hand-writing its own
47
+ * colour utility for the same two values.
48
+ *
49
+ * Renders a `<span>` (inherits its size/weight/line-height from context —
50
+ * see `text.recipe.ts`'s doc for why `Text` sets none of those itself).
51
+ */
52
+ exports.Text = React.forwardRef(({ tone, className, asChild, children, ...props }, ref) => {
53
+ return ((0, jsx_runtime_1.jsx)(core_1.Primitive.span, { ref: ref, asChild: asChild, className: (0, text_recipe_1.textRecipe)({ tone, className }), ...props, children: children }));
54
+ });
55
+ exports.Text.displayName = "Text";
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.spec = exports.textRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // No new measurement — `tone` just names two colour tokens every other
7
+ // component already carries (`fgPrimary`/`fgNeutralMuted`), for the plain
8
+ // runs of text that aren't any of crisp's other typed things (a Badge, a
9
+ // Chip, a link). Font size/weight/line-height are deliberately NOT set here:
10
+ // unlike Badge or Chip, plain text has no single measured size across the
11
+ // app (a table cell, a label, a caption are all different sizes) — `Text`
12
+ // inherits type from its context, the same choice `MiddleTruncate` makes for
13
+ // the same reason (see its own doc).
14
+ const spec = (0, core_1.defineSpec)({
15
+ name: "text",
16
+ // `display: inline` -- a span's own default, stated rather than left
17
+ // implicit: an EMPTY base compiles to an empty string, and
18
+ // `core/resolve.ts`'s `if (base) classes.push(prefixedName)` treats that
19
+ // falsy string as "no base rule", which drops the `crisp-text` class
20
+ // itself, silently, from every instance (caught by this file's own
21
+ // tests going red on that exact class).
22
+ base: { display: "inline" },
23
+ variants: {
24
+ tone: {
25
+ default: { color: tokens_1.contract.fgPrimary },
26
+ muted: { color: tokens_1.contract.fgNeutralMuted },
27
+ },
28
+ },
29
+ defaultVariants: { tone: "default" },
30
+ });
31
+ exports.spec = spec;
32
+ const config = (0, core_1.specToRecipe)(spec);
33
+ exports.textRecipe = Object.assign((0, core_1.resolveRecipe)(config), { config });
34
+ exports.default = config;
package/dist/cjs/index.js CHANGED
@@ -53,6 +53,7 @@ __exportStar(require("./components/mentionchip"), exports);
53
53
  __exportStar(require("./components/menu"), exports);
54
54
  __exportStar(require("./components/middletruncate"), exports);
55
55
  __exportStar(require("./components/multiselect"), exports);
56
+ __exportStar(require("./components/personchip"), exports);
56
57
  __exportStar(require("./components/popover"), exports);
57
58
  __exportStar(require("./components/progress"), exports);
58
59
  __exportStar(require("./components/quantitypicker"), exports);
@@ -70,6 +71,7 @@ __exportStar(require("./components/switch"), exports);
70
71
  __exportStar(require("./components/table"), exports);
71
72
  __exportStar(require("./components/tabletoolbar"), exports);
72
73
  __exportStar(require("./components/tabs"), exports);
74
+ __exportStar(require("./components/text"), exports);
73
75
  __exportStar(require("./components/textarea"), exports);
74
76
  __exportStar(require("./components/textlink"), exports);
75
77
  __exportStar(require("./components/timepicker"), exports);
@@ -87,6 +89,7 @@ __exportStar(require("./product/appshell"), exports);
87
89
  __exportStar(require("./product/authshell"), exports);
88
90
  __exportStar(require("./product/board"), exports);
89
91
  __exportStar(require("./product/command"), exports);
92
+ __exportStar(require("./product/comments"), exports);
90
93
  __exportStar(require("./product/entitycard"), exports);
91
94
  __exportStar(require("./product/flowcanvas"), exports);
92
95
  __exportStar(require("./product/formdialog"), exports);
@@ -95,6 +98,7 @@ __exportStar(require("./product/importdialog"), exports);
95
98
  __exportStar(require("./product/inbox"), exports);
96
99
  __exportStar(require("./product/listpage"), exports);
97
100
  __exportStar(require("./product/navitem"), exports);
101
+ __exportStar(require("./product/objectsettings"), exports);
98
102
  __exportStar(require("./product/onboarding"), exports);
99
103
  __exportStar(require("./product/recordpage"), exports);
100
104
  __exportStar(require("./product/recordpanel"), exports);
@@ -35,20 +35,76 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.Board = Board;
37
37
  const jsx_runtime_1 = require("react/jsx-runtime");
38
+ const lucide_react_1 = require("lucide-react");
38
39
  const React = __importStar(require("react"));
40
+ const menu_1 = require("../../components/menu");
39
41
  const core_1 = require("../../core");
40
42
  const board_recipe_1 = require("./board.recipe");
43
+ const BOARD_AGG_ORDER = [
44
+ "countEmpty",
45
+ "countFilled",
46
+ "percentEmpty",
47
+ "percentFilled",
48
+ "sum",
49
+ "average",
50
+ "max",
51
+ "min",
52
+ ];
53
+ // English default text, looked up through `useCrispMessages()` (crisp/#471)
54
+ // the same way Board's other built-in fragments already are.
55
+ const AGG_LABEL = {
56
+ countEmpty: "Count empty",
57
+ countFilled: "Count filled",
58
+ percentEmpty: "Percent empty",
59
+ percentFilled: "Percent filled",
60
+ sum: "Sum",
61
+ average: "Average",
62
+ max: "Max",
63
+ min: "Min",
64
+ };
65
+ function computeBoardAgg(agg, values) {
66
+ const filled = values.filter((v) => v != null);
67
+ const total = values.length;
68
+ switch (agg) {
69
+ case "countEmpty":
70
+ return String(total - filled.length);
71
+ case "countFilled":
72
+ return String(filled.length);
73
+ case "percentEmpty":
74
+ return total === 0
75
+ ? "0%"
76
+ : `${Math.round(((total - filled.length) / total) * 100)}%`;
77
+ case "percentFilled":
78
+ return total === 0
79
+ ? "0%"
80
+ : `${Math.round((filled.length / total) * 100)}%`;
81
+ case "sum":
82
+ return filled.reduce((a, b) => a + b, 0).toLocaleString();
83
+ case "average":
84
+ return filled.length === 0
85
+ ? "0"
86
+ : (filled.reduce((a, b) => a + b, 0) / filled.length).toLocaleString(undefined, { maximumFractionDigits: 2 });
87
+ case "max":
88
+ return filled.length === 0 ? "—" : Math.max(...filled).toLocaleString();
89
+ case "min":
90
+ return filled.length === 0 ? "—" : Math.min(...filled).toLocaleString();
91
+ }
92
+ }
41
93
  /**
42
94
  * A kanban board with native drag-and-drop between columns. Cards are
43
95
  * `draggable`; dropping one on a column reparents it via `onCardsChange`
44
96
  * (the parent owns the cards, so the move is a controlled state update).
45
97
  * Measured from a deals pipeline.
46
98
  */
47
- function Board({ columns, cards, onCardsChange, renderCard, onNewCard, newCardLabel: newCardLabelProp, getCardLabel, announceMove: announceMoveProp, className, ...rest }) {
99
+ function Board({ columns, cards, onCardsChange, renderCard, onNewCard, newCardLabel: newCardLabelProp, getCardLabel, announceMove: announceMoveProp, calc, className, ...rest }) {
48
100
  // crisp/#471: same resolution order as every other component's own —
49
101
  // explicit prop, then a CrispMessagesProvider's translation of the English
50
102
  // default, then that default itself.
51
103
  const newCardLabel = (0, core_1.useCrispMessage)(newCardLabelProp, "New");
104
+ // #604: which BoardAgg each column's footer shows, keyed by column id.
105
+ // Unset (undefined) means the column still shows the "+ Add calculation"
106
+ // trigger, exactly like the table footer's own per-column state.
107
+ const [colAgg, setColAgg] = React.useState({});
52
108
  const crispMessages = (0, core_1.useCrispMessages)();
53
109
  const announceMove = announceMoveProp ??
54
110
  ((column, card) => card
@@ -128,7 +184,9 @@ function Board({ columns, cards, onCardsChange, renderCard, onNewCard, newCardLa
128
184
  // Ctrl+Right at the last column still reaches the browser
129
185
  if (delta && moveByKey(card, delta))
130
186
  e.preventDefault();
131
- }, children: renderCard(card) }, card.id)))] })] }, col.id));
187
+ }, children: renderCard(card) }, card.id)))] }), calc && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-board-col-footer", children: (0, jsx_runtime_1.jsx)(menu_1.Menu, { align: "start", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-board-col-calc", "data-empty": colAgg[col.id] ? undefined : "", children: colAgg[col.id] ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-board-col-calc-label", children: crispMessages[AGG_LABEL[colAgg[col.id]]] ??
188
+ AGG_LABEL[colAgg[col.id]] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-board-col-calc-value", children: computeBoardAgg(colAgg[col.id], colCards.map((c) => calc.getValue(c))) })] })) : ((crispMessages["+ Add calculation"] ??
189
+ "+ Add calculation")) }), children: BOARD_AGG_ORDER.map((a) => ((0, jsx_runtime_1.jsxs)(menu_1.MenuItem, { onSelect: () => setColAgg((prev) => ({ ...prev, [col.id]: a })), children: [crispMessages[AGG_LABEL[a]] ?? AGG_LABEL[a], colAgg[col.id] === a ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 15, strokeWidth: 1.75 })) : null] }, a))) }) }))] }, col.id));
132
190
  }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-board-live", role: "status", "aria-live": "polite", children: announcement })] }));
133
191
  }
134
192
  Board.displayName = "Board";
@@ -35,6 +35,16 @@ const config = (0, core_1.defineRecipe)({
35
35
  & .crisp-board-card[data-dragging] { opacity: 0.4; }
36
36
  & .crisp-board-card:focus { outline: none; }
37
37
  & .crisp-board-card:focus-visible { outline: 2px solid ${tokens_1.contract.bgBrandSolid}; outline-offset: 1px; }
38
+ /* #604: a per-column calc footer, mirroring the table's own (measured
39
+ live, Deals pipeline). Sits below the scrollable card body, not
40
+ inside it or pinned to it -- Attio's own is position: static, not
41
+ sticky, so it's a plain flex-shrink:0 sibling here too. */
42
+ & .crisp-board-col-footer { flex-shrink: 0; padding: ${tokens_1.contract.space["1"]} 0; }
43
+ & .crisp-board-col-calc { display: flex; align-items: center; justify-content: center; gap: ${tokens_1.contract.space["1"]}; width: 100%; height: 28px; padding: ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["2"]}; margin: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); cursor: pointer; }
44
+ & .crisp-board-col-calc:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); }
45
+ & .crisp-board-col-calc[data-empty] { opacity: 0.7; }
46
+ & .crisp-board-col-calc-label { text-transform: capitalize; }
47
+ & .crisp-board-col-calc-value { color: ${tokens_1.contract.fgPrimary}; font-variant-numeric: tabular-nums; }
38
48
  /* announced, never drawn */
39
49
  & .crisp-board-live {
40
50
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;