@crisp-ui-kit/crisp 0.54.2 → 0.54.3

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 (74) hide show
  1. package/dist/chip.css +7 -1
  2. package/dist/chip.d.ts +2 -1
  3. package/dist/chip.layered.css +7 -1
  4. package/dist/cjs/components/chip/chip.js +3 -2
  5. package/dist/cjs/components/chip/chip.recipe.js +37 -1
  6. package/dist/cjs/components/datatable/datatable.js +489 -111
  7. package/dist/cjs/components/emptystate/emptystate.js +3 -2
  8. package/dist/cjs/components/emptystate/emptystate.recipe.js +26 -1
  9. package/dist/cjs/components/personchip/index.js +17 -0
  10. package/dist/cjs/components/personchip/personchip.js +50 -0
  11. package/dist/cjs/components/table/table.js +2 -2
  12. package/dist/cjs/components/tabs/tabs.js +2 -2
  13. package/dist/cjs/components/tabs/tabs.recipe.js +30 -3
  14. package/dist/cjs/components/text/index.js +7 -0
  15. package/dist/cjs/components/text/text.js +55 -0
  16. package/dist/cjs/components/text/text.recipe.js +34 -0
  17. package/dist/cjs/index.js +2 -0
  18. package/dist/cjs/product/headerbar/headerbar.recipe.js +9 -1
  19. package/dist/cjs/product/recordpage/recordpage.recipe.js +10 -1
  20. package/dist/cjs/product/recordstable/recordstable.js +89 -6
  21. package/dist/cjs/product/richtext/richtext.recipe.js +26 -1
  22. package/dist/emptystate.css +15 -1
  23. package/dist/emptystate.d.ts +2 -2
  24. package/dist/emptystate.layered.css +15 -1
  25. package/dist/esm/components/chip/chip.js +3 -2
  26. package/dist/esm/components/chip/chip.recipe.js +37 -1
  27. package/dist/esm/components/datatable/datatable.js +488 -111
  28. package/dist/esm/components/emptystate/emptystate.js +4 -3
  29. package/dist/esm/components/emptystate/emptystate.recipe.js +26 -1
  30. package/dist/esm/components/emptystate/index.js +1 -1
  31. package/dist/esm/components/personchip/index.js +1 -0
  32. package/dist/esm/components/personchip/personchip.js +47 -0
  33. package/dist/esm/components/table/table.js +2 -2
  34. package/dist/esm/components/tabs/tabs.js +2 -2
  35. package/dist/esm/components/tabs/tabs.recipe.js +30 -3
  36. package/dist/esm/components/text/index.js +2 -0
  37. package/dist/esm/components/text/text.js +19 -0
  38. package/dist/esm/components/text/text.recipe.js +31 -0
  39. package/dist/esm/index.js +2 -0
  40. package/dist/esm/product/headerbar/headerbar.recipe.js +9 -1
  41. package/dist/esm/product/recordpage/recordpage.recipe.js +10 -1
  42. package/dist/esm/product/recordstable/recordstable.js +90 -8
  43. package/dist/esm/product/richtext/richtext.recipe.js +26 -1
  44. package/dist/headerbar.css +1 -1
  45. package/dist/headerbar.layered.css +1 -1
  46. package/dist/recordpage.css +13 -2
  47. package/dist/recordpage.layered.css +13 -2
  48. package/dist/richtext.css +11 -1
  49. package/dist/richtext.layered.css +11 -1
  50. package/dist/styles.css +64 -7
  51. package/dist/styles.layered.css +64 -7
  52. package/dist/tabs.css +7 -1
  53. package/dist/tabs.d.ts +2 -2
  54. package/dist/tabs.layered.css +7 -1
  55. package/dist/text.css +9 -0
  56. package/dist/text.d.ts +7 -0
  57. package/dist/text.layered.css +11 -0
  58. package/dist/types/components/chip/chip.d.ts +32 -0
  59. package/dist/types/components/chip/chip.recipe.d.ts +31 -0
  60. package/dist/types/components/datatable/datatable.d.ts +28 -4
  61. package/dist/types/components/emptystate/emptystate.d.ts +29 -4
  62. package/dist/types/components/emptystate/emptystate.recipe.d.ts +2 -0
  63. package/dist/types/components/emptystate/index.d.ts +1 -1
  64. package/dist/types/components/personchip/index.d.ts +1 -0
  65. package/dist/types/components/personchip/personchip.d.ts +37 -0
  66. package/dist/types/components/table/table.d.ts +36 -0
  67. package/dist/types/components/tabs/tabs.d.ts +4 -2
  68. package/dist/types/components/tabs/tabs.recipe.d.ts +18 -3
  69. package/dist/types/components/text/index.d.ts +2 -0
  70. package/dist/types/components/text/text.d.ts +32 -0
  71. package/dist/types/components/text/text.recipe.d.ts +40 -0
  72. package/dist/types/index.d.ts +2 -0
  73. package/dist/types/product/recordstable/recordstable.d.ts +24 -2
  74. package/package.json +11 -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";
@@ -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);
@@ -20,10 +20,18 @@ const tokens_1 = require("../../tokens");
20
20
  // `title`/`trailing` slot content a consumer supplies. `size.header` (48px)
21
21
  // is reused rather than a new token, since it's the same "workspace-switcher
22
22
  // header height" measured elsewhere in the sidebar.
23
+ //
24
+ // crisp#576: `border-box` plus a plain `height: ${size.header}` meant the
25
+ // bordered variant's own 1px border was eaten OUT OF the 48px box, leaving
26
+ // only 47px of content -- caught in review of internal/grc's own shell
27
+ // shifting up 1px end-to-end after adopting this component. Re-measured
28
+ // live: Attio's own `<header>` is 49px total (border-box) with the 1px
29
+ // border OUTSIDE the 48px content row, not inside it. `[data-bordered]` now
30
+ // grows the box by the border's own width so content stays 48px either way.
23
31
  const config = (0, core_1.defineRecipe)({
24
32
  name: "headerbar",
25
33
  base: `box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: ${tokens_1.contract.space["2"]}; height: ${tokens_1.contract.size.header}; flex-shrink: 0; padding: 0 ${tokens_1.contract.space["3"]}; font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
26
- &[data-bordered] { border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; }
34
+ &[data-bordered] { height: calc(${tokens_1.contract.size.header} + 1px); border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; }
27
35
  & .crisp-headerbar-leading { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; flex-shrink: 0; min-width: 0; }
28
36
  & .crisp-headerbar-title { flex: 1; min-width: 0; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; overflow: hidden; }
29
37
  & .crisp-headerbar-trailing { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; flex-shrink: 0; }`,
@@ -9,6 +9,14 @@ const tokens_1 = require("../../tokens");
9
9
  // column (transparent, border-right) and a flexible main area whose tab bar
10
10
  // (14/500 labels over a sliding ink underline) switches content — Overview shows
11
11
  // a bordered Highlights stat-grid and an Activity feed.
12
+ //
13
+ // `.crisp-recordpage-panel`'s own `padding-top` (c.space["4"], 16px) was
14
+ // dropped (your-moon/crisp#588): this panel is only ever rendered as Tabs'
15
+ // own children (every branch of `builtinPanel` in recordpage.tsx, all fed to
16
+ // `<Tabs>` below), so it sat inside `.crisp-tabs-panel`, which now carries
17
+ // its own measured 32px top-inset by default — keeping both would have
18
+ // stacked to 48px. Caught by tracing every consumer of `Tabs` before
19
+ // shipping its new default, not by a screenshot.
12
20
  const config = (0, core_1.defineRecipe)({
13
21
  name: "recordpage",
14
22
  base: `
@@ -128,7 +136,8 @@ const config = (0, core_1.defineRecipe)({
128
136
 
129
137
  & .crisp-recordpage-main { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; overflow-y: auto; padding: ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["5"]}; }
130
138
 
131
- & .crisp-recordpage-panel { padding-top: ${tokens_1.contract.space["4"]}; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["4"]}; }
139
+ /* no padding-top: Tabs' own panel supplies it now (#588) */
140
+ & .crisp-recordpage-panel { display: flex; flex-direction: column; gap: ${tokens_1.contract.space["4"]}; }
132
141
  & .crisp-recordpage-card { border: 1px solid ${tokens_1.contract.borderSubtle}; border-radius: ${tokens_1.contract.radius["2xl"]}; background: ${tokens_1.contract.bgSurface}; overflow: hidden; }
133
142
  & .crisp-recordpage-card-title { font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.semibold}; letter-spacing: -0.14px; color: ${tokens_1.contract.fgPrimary}; padding: ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["2"]}; }
134
143
  & .crisp-recordpage-hl-card { border: 1px solid ${tokens_1.contract.borderSubtle}; border-radius: ${tokens_1.contract.radius["2xl"]}; overflow: hidden; }
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.defaultRows = exports.defaultColumns = exports.parseCellDate = exports.formatCellDate = exports.recordsTableLabels = void 0;
37
+ exports.isLabelTruncated = isLabelTruncated;
37
38
  exports.tagTone = tagTone;
38
39
  exports.RecordsTable = RecordsTable;
39
40
  const jsx_runtime_1 = require("react/jsx-runtime");
@@ -52,9 +53,61 @@ const menu_1 = require("../../components/menu");
52
53
  const multiselect_1 = require("../../components/multiselect");
53
54
  const select_1 = require("../../components/select");
54
55
  const switch_1 = require("../../components/switch");
56
+ const tooltip_1 = require("../../components/tooltip");
55
57
  const core_1 = require("../../core");
56
58
  const company_logos_1 = require("./company-logos");
57
59
  const recordstable_recipe_1 = require("./recordstable.recipe");
60
+ // #534: whether an element's own content is being cut off by its own box --
61
+ // the same fact `.crisp-recordstable-th-label`'s CSS ellipsis is silently
62
+ // applying, exported so the comparison itself (not layout) can be
63
+ // mutation-tested without a real browser. Identical to DataTable's own
64
+ // `isLabelTruncated` (components/datatable/datatable.tsx) -- not shared
65
+ // because `product/` can depend on `components/` but not the other way, and
66
+ // this file already duplicates DataTable's other measured header behaviour
67
+ // rather than reach across that boundary.
68
+ function isLabelTruncated(el) {
69
+ return el.scrollWidth > el.clientWidth;
70
+ }
71
+ /**
72
+ * The header label span: plain when it fits, wrapped in the shared `Tooltip`
73
+ * (full text, `asChild` so the wrapper never displaces the ellipsis CSS the
74
+ * span already carries -- see `Tooltip`'s own `asChild` doc) once it's
75
+ * actually cut off. Re-measures on mount and on resize (column drag,
76
+ * viewport change, label change) via `ResizeObserver`; SSR-safe the same way
77
+ * `MiddleTruncate` is -- the DOM is only touched inside an effect, so the
78
+ * server (and the first client paint) renders the plain span.
79
+ */
80
+ function ThLabel({ label }) {
81
+ const ref = React.useRef(null);
82
+ const [overflowing, setOverflowing] = React.useState(false);
83
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `label` isn't read in the body, but a rename (col.label changing with the column's own width unchanged) needs a re-measure a ResizeObserver on `el` won't fire for -- same reasoning as Tabs' own re-measure effect.
84
+ React.useLayoutEffect(() => {
85
+ const el = ref.current;
86
+ if (!el)
87
+ return;
88
+ const measure = () => setOverflowing(isLabelTruncated(el));
89
+ measure();
90
+ if (typeof ResizeObserver === "undefined")
91
+ return;
92
+ const ro = new ResizeObserver(measure);
93
+ ro.observe(el);
94
+ return () => ro.disconnect();
95
+ }, [label]);
96
+ // `Tooltip` always wraps the SAME span (never swapped in/out of the tree
97
+ // based on `overflowing`) so the span's own DOM node identity survives
98
+ // every toggle -- a real-browser-only bug (happy-dom has no
99
+ // ResizeObserver, so the test suite never exercised this path): swapping
100
+ // between a bare `<span>` and `<Tooltip asChild><span/></Tooltip>` changes
101
+ // the element type at this position, so React unmounts the old span and
102
+ // mounts a new one; the effect above never re-runs (its deps are
103
+ // `[label]`, unchanged), so its ResizeObserver keeps watching the now
104
+ // -detached old node, whose deferred initial callback then reports 0x0 and
105
+ // flips `overflowing` straight back to false -- the tooltip would never
106
+ // actually show. `open={false}` when it fits keeps the Tooltip mounted
107
+ // (asChild costs no extra DOM either way) but pins it permanently closed,
108
+ // so hovering a non-overflowing header still opens nothing.
109
+ return ((0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { content: label, asChild: true, open: overflowing ? undefined : false, children: (0, jsx_runtime_1.jsx)("span", { ref: ref, className: "crisp-recordstable-th-label", children: label }) }));
110
+ }
58
111
  const EDITABLE = new Set(["text", "number", "url", "currency"]);
59
112
  // types whose cell is an always-interactive picker (not double-click-to-edit)
60
113
  const PICKER = new Set(["status", "reference", "date", "tags"]);
@@ -138,6 +191,8 @@ exports.recordsTableLabels = {
138
191
  setRecordPlaceholder: "Set a record…",
139
192
  noRecords: "No records yet",
140
193
  untitled: "Untitled",
194
+ importCsv: "Import CSV",
195
+ connectDataSource: "Connect data source",
141
196
  agg: {
142
197
  none: "None",
143
198
  count: "Count all",
@@ -149,6 +204,9 @@ exports.recordsTableLabels = {
149
204
  },
150
205
  op: {
151
206
  contains: "contains",
207
+ doesnotcontain: "does not contain",
208
+ startswith: "starts with",
209
+ endswith: "ends with",
152
210
  is: "is",
153
211
  isnot: "is not",
154
212
  empty: "is empty",
@@ -175,8 +233,17 @@ const compact = new Intl.NumberFormat("en", {
175
233
  const CHECK_W = 40;
176
234
  const ADDCOL_W = 44;
177
235
  const colWidth = (col) => col.width ?? (col.type === "entity" ? 240 : 180);
178
- const OP_ORDER = ["contains", "is", "isnot", "empty", "notempty"];
179
- const opNeedsValue = (op) => op === "contains" || op === "is" || op === "isnot";
236
+ const OP_ORDER = [
237
+ "contains",
238
+ "doesnotcontain",
239
+ "startswith",
240
+ "endswith",
241
+ "is",
242
+ "isnot",
243
+ "empty",
244
+ "notempty",
245
+ ];
246
+ const opNeedsValue = (op) => op !== "empty" && op !== "notempty";
180
247
  function matchesFilter(col, row, cond) {
181
248
  if (cond.op === "empty")
182
249
  return !isFilled(col, row);
@@ -190,6 +257,12 @@ function matchesFilter(col, row, cond) {
190
257
  return true;
191
258
  if (cond.op === "contains")
192
259
  return t.includes(v);
260
+ if (cond.op === "doesnotcontain")
261
+ return !t.includes(v);
262
+ if (cond.op === "startswith")
263
+ return t.startsWith(v);
264
+ if (cond.op === "endswith")
265
+ return t.endsWith(v);
193
266
  if (cond.op === "is")
194
267
  return t === v;
195
268
  return t !== v; // isnot
@@ -472,7 +545,7 @@ function CreateRecordDialog({ open, onOpenChange, columns, entityLabel, onCreate
472
545
  * and a floating bulk-action bar (delete / deselect) when rows are selected.
473
546
  * Selection can be controlled via `selectedIds`/`onSelectionChange`.
474
547
  */
475
- function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, rows: rowsProp = exports.defaultRows, selectedIds, onSelectionChange, showNew = true, allowAddColumn = true, onOpenRecord, onOpenRecordPage, avatarTone = "brand", className, ...rest }) {
548
+ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, rows: rowsProp = exports.defaultRows, selectedIds, onSelectionChange, showNew = true, allowAddColumn = true, onOpenRecord, onOpenRecordPage, avatarTone = "brand", emptyStateExtra, className, ...rest }) {
476
549
  const [cols, setCols] = React.useState(columnsProp);
477
550
  const [data, setData] = React.useState(rowsProp);
478
551
  // The table owns `cols`/`data` so it can rename a column, edit a cell, or
@@ -1140,7 +1213,7 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
1140
1213
  setDragCol(col.key);
1141
1214
  e.dataTransfer.effectAllowed = "move";
1142
1215
  }
1143
- : undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 16, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-th-label", children: col.label }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-sort", children: (0, jsx_runtime_1.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "m4 6 4 4 4-4" }) }) })] })), openMenu === `col:${col.key}` &&
1216
+ : undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 16, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)(ThLabel, { label: col.label }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-sort", children: (0, jsx_runtime_1.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "m4 6 4 4 4-4" }) }) })] })), openMenu === `col:${col.key}` &&
1144
1217
  rootRef.current &&
1145
1218
  (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsxs)("div", { ref: menuRef, className: "crisp-recordstable-menu crisp-recordstable-menu--fixed crisp-recordstable-menu--hdr", role: "menu", "aria-label": `${col.label} column`, style: menuPos ?? undefined, onKeyDown: onMenuKeyDown, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "asc"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUp, { size: 15, strokeWidth: 1.75 }), t.sortAscending] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "desc"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDown, { size: 15, strokeWidth: 1.75 }), t.sortDescending] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i <= 1, onClick: () => moveColumn(col.key, -1), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveLeft] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i === 0 || i >= cols.length - 1, onClick: () => moveColumn(col.key, 1), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowRight, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveRight] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordstable-menu-sep" }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => {
1146
1219
  setRenamingCol(col.key);
@@ -1209,9 +1282,19 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
1209
1282
  }, children: res ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-sum-label", children: t.agg[a] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-sum-value", children: res.value })] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-sum-label", children: t.calculate })) }), sumOpen &&
1210
1283
  rootRef.current &&
1211
1284
  (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { ref: menuRef, className: "crisp-recordstable-menu crisp-recordstable-menu--fixed crisp-recordstable-menu--up", role: "menu", "aria-label": `${col.label} summary`, style: menuPos ?? undefined, onKeyDown: onMenuKeyDown, children: aggOptions(col).map((opt) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitemradio", tabIndex: -1, "aria-checked": a === opt, "data-on": a === opt ? "" : undefined, onClick: () => setAggValue(col.key, opt), children: [t.agg[opt], a === opt ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-recordstable-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, opt))) }), rootRef.current)] }) }, col.key));
1212
- }), (0, jsx_runtime_1.jsx)("td", { className: "crisp-recordstable-sum crisp-recordstable-addcol" })] }) })] }), visibleRows.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordstable-empty", children: (0, jsx_runtime_1.jsx)(emptystate_1.EmptyState, { variant: "page", icon: activeFilters ? ((0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Inbox, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })), title: activeFilters ? "No records match that filter" : t.noRecords, description: activeFilters
1285
+ }), (0, jsx_runtime_1.jsx)("td", { className: "crisp-recordstable-sum crisp-recordstable-addcol" })] }) })] }), visibleRows.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordstable-empty", children: (0, jsx_runtime_1.jsx)(emptystate_1.EmptyState
1286
+ // #603: the unfiltered zero-records state, when the consumer
1287
+ // opts into the extra CTAs, uses Attio's own larger measured
1288
+ // tile (Users/Workspaces, 138px) instead of the 45px
1289
+ // filtered-to-zero ring -- two different Attio surfaces.
1290
+ , {
1291
+ // #603: the unfiltered zero-records state, when the consumer
1292
+ // opts into the extra CTAs, uses Attio's own larger measured
1293
+ // tile (Users/Workspaces, 138px) instead of the 45px
1294
+ // filtered-to-zero ring -- two different Attio surfaces.
1295
+ variant: !activeFilters && emptyStateExtra ? "records" : "page", icon: activeFilters ? ((0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Inbox, { size: !activeFilters && emptyStateExtra ? 40 : 18, strokeWidth: 1.25, "aria-hidden": "true" })), title: activeFilters ? "No records match that filter" : t.noRecords, description: activeFilters
1213
1296
  ? "None of your records match the filters that are set."
1214
- : `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "sm", onClick: addRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] })) }) })) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-addrow", onClick: addRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordstable-live", role: "status", "aria-live": "polite", children: selectionAnnouncement }), selected.size > 0 && ((0, jsx_runtime_1.jsxs)(bulkbar_1.BulkBar, { count: selected.size, countLabel: t.selectedCount, onClear: () => setSelected(new Set()), children: [(0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.moreActions, trigger: (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: t.exportCsv }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete })] })] })), (0, jsx_runtime_1.jsx)(CreateRecordDialog, { labels: t, open: creating, onOpenChange: setCreating, columns: cols, entityLabel: cols.find((c) => c.type === "entity")?.label ?? "record", onCreate: (row) => setData((prev) => [...prev, row]) }), ctxMenu &&
1297
+ : `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "sm", onClick: addRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] }), emptyStateExtra?.onImportCsv && ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "neutral", size: "sm", onClick: emptyStateExtra.onImportCsv, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Upload, { size: 15, strokeWidth: 1.75 }), t.importCsv] })), emptyStateExtra?.onConnectDataSource && ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "neutral", size: "sm", onClick: emptyStateExtra.onConnectDataSource, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plug, { size: 15, strokeWidth: 1.75 }), t.connectDataSource] }))] })), learnMore: !activeFilters ? emptyStateExtra?.learnMore : undefined }) })) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-addrow", onClick: addRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordstable-live", role: "status", "aria-live": "polite", children: selectionAnnouncement }), selected.size > 0 && ((0, jsx_runtime_1.jsxs)(bulkbar_1.BulkBar, { count: selected.size, countLabel: t.selectedCount, onClear: () => setSelected(new Set()), children: [(0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.moreActions, trigger: (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: t.exportCsv }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete })] })] })), (0, jsx_runtime_1.jsx)(CreateRecordDialog, { labels: t, open: creating, onOpenChange: setCreating, columns: cols, entityLabel: cols.find((c) => c.type === "entity")?.label ?? "record", onCreate: (row) => setData((prev) => [...prev, row]) }), ctxMenu &&
1215
1298
  (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsxs)("div", { ref: ctxRef, className: "crisp-recordstable-menu crisp-recordstable-ctx", role: "menu", "aria-label": t.recordActions, style: { position: "fixed", top: ctxMenu.y, left: ctxMenu.x }, onKeyDown: onCtxKeyDown, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => copyRow(ctxMenu.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }), t.copy] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => duplicateRow(ctxMenu.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CopyPlus, { size: 15, strokeWidth: 1.75 }), t.duplicate] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => pinRow(ctxMenu.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }), t.addToList] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item crisp-recordstable-menu-item--danger", role: "menuitem", tabIndex: -1, onClick: () => deleteRow(ctxMenu.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), t.delete] })] }), rootRef.current ?? document.body)] }));
1216
1299
  }
1217
1300
  exports.defaultColumns = [
@@ -43,7 +43,32 @@ const config = (0, core_1.defineRecipe)({
43
43
  & .crisp-rte-body h1 { font-size: ${tokens_1.contract.text.xl}; line-height: 28px; font-weight: ${tokens_1.contract.weight.semibold}; letter-spacing: -0.3px; margin-top: ${tokens_1.contract.space["4"]}; }
44
44
  & .crisp-rte-body h2 { font-size: 17px; line-height: 24px; font-weight: ${tokens_1.contract.weight.semibold}; letter-spacing: -0.2px; margin-top: ${tokens_1.contract.space["3"]}; }
45
45
  & .crisp-rte-body h3 { font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.semibold}; margin-top: ${tokens_1.contract.space["2.5"]}; }
46
- & .crisp-rte-body ul, & .crisp-rte-body ol { padding-inline-start: ${tokens_1.contract.space["5"]}; }
46
+ /* #575: a consumer's CSS reset (e.g. Tailwind preflight's un-scoped
47
+ ul, ol { list-style: none }, specificity 0,0,1) was winning every
48
+ list marker outright -- this rule set padding but never declared
49
+ list-style-type, so there was no higher-specificity value (this
50
+ selector is 0,1,1) to beat the reset with. Split into two
51
+ single-selector rules, each declaring its own marker type, rather
52
+ than the old single comma-joined rule ( .crisp-rte-body ul, &
53
+ .crisp-rte-body ol ) the issue flagged as malformed.
54
+ #611: padding-inline-start re-measured live once CRUD on the
55
+ measurement workspace was allowed (a "zz-parity" note with a
56
+ bulleted list, a nested bulleted list, and a numbered list) --
57
+ Attio's own <ul>/<ol> measures 36px (getComputedStyle,
58
+ ~36.1353px), not the 20px placeholder #575 shipped with no real
59
+ content to check against. 36px isn't in tokens/scale.ts (steps
60
+ stop at 24px, jump to 32px) and is a single component-intrinsic
61
+ value, so it stays a raw px per AUTHORING.md's golden rule 4b
62
+ rather than being rounded to an existing token or added to the
63
+ shared scale. Marker colour (currentColor via the already-measured
64
+ fgPrimary, confirmed to equal Attio's measured rgb(16,17,18) ==
65
+ #101112 exactly) and the ~4px li-to-li gap (the existing li
66
+ margin-top rule below) were re-checked at the same time and
67
+ already matched -- only the padding was wrong. Attio doesn't vary
68
+ marker type for a nested list either (nested <ul> is disc too,
69
+ not circle), which this already doesn't attempt. */
70
+ & .crisp-rte-body ul { list-style-type: disc; padding-inline-start: 36px; }
71
+ & .crisp-rte-body ol { list-style-type: decimal; padding-inline-start: 36px; }
47
72
  & .crisp-rte-body li { margin-top: ${tokens_1.contract.space["1"]}; }
48
73
  & .crisp-rte-body blockquote { padding-inline-start: ${tokens_1.contract.space["3"]}; border-inline-start: 2px solid ${tokens_1.contract.borderDefault}; color: ${tokens_1.contract.fgMuted}; }
49
74
  & .crisp-rte-body code { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: ${tokens_1.contract.text.md}; padding: 1px ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.sm}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); }