@crisp-ui-kit/crisp 0.54.1 → 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 (171) 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 +490 -111
  7. package/dist/cjs/components/datatable/datatable.recipe.js +4 -0
  8. package/dist/cjs/components/emptystate/emptystate.js +3 -2
  9. package/dist/cjs/components/emptystate/emptystate.recipe.js +26 -1
  10. package/dist/cjs/components/mentionchip/index.js +7 -0
  11. package/dist/cjs/components/mentionchip/mentionchip.js +57 -0
  12. package/dist/cjs/components/mentionchip/mentionchip.recipe.js +38 -0
  13. package/dist/cjs/components/personchip/index.js +17 -0
  14. package/dist/cjs/components/personchip/personchip.js +50 -0
  15. package/dist/cjs/components/table/table.js +2 -2
  16. package/dist/cjs/components/table/table.recipe.js +15 -3
  17. package/dist/cjs/components/tabs/tabs.js +2 -2
  18. package/dist/cjs/components/tabs/tabs.recipe.js +30 -3
  19. package/dist/cjs/components/text/index.js +7 -0
  20. package/dist/cjs/components/text/text.js +55 -0
  21. package/dist/cjs/components/text/text.recipe.js +34 -0
  22. package/dist/cjs/core/floating-stack.js +18 -0
  23. package/dist/cjs/core/index.js +2 -1
  24. package/dist/cjs/index.js +8 -0
  25. package/dist/cjs/product/formdialog/formdialog.js +171 -0
  26. package/dist/cjs/product/formdialog/formdialog.recipe.js +26 -0
  27. package/dist/cjs/product/formdialog/index.js +8 -0
  28. package/dist/cjs/product/headerbar/headerbar.recipe.js +9 -1
  29. package/dist/cjs/product/importdialog/importdialog.js +134 -0
  30. package/dist/cjs/product/importdialog/importdialog.recipe.js +92 -0
  31. package/dist/cjs/product/importdialog/index.js +18 -0
  32. package/dist/cjs/product/recordpage/recordpage.recipe.js +10 -1
  33. package/dist/cjs/product/recordstable/recordstable.js +89 -6
  34. package/dist/cjs/product/richtext/richtext.recipe.js +26 -1
  35. package/dist/cjs/product/savedviews/index.js +7 -0
  36. package/dist/cjs/product/savedviews/savedviews.js +207 -0
  37. package/dist/cjs/product/savedviews/savedviews.recipe.js +103 -0
  38. package/dist/cjs/product/taskstable/index.js +7 -0
  39. package/dist/cjs/product/taskstable/taskstable.js +27 -0
  40. package/dist/cjs/product/taskstable/taskstable.recipe.js +47 -0
  41. package/dist/cjs/product/themepicker/index.js +17 -0
  42. package/dist/cjs/product/themepicker/themepicker.js +32 -0
  43. package/dist/cjs/product/themepicker/themepicker.recipe.js +98 -0
  44. package/dist/cjs/tokens/palette.js +30 -13
  45. package/dist/cjs/tokens/semantic.js +6 -0
  46. package/dist/datatable.css +20 -21
  47. package/dist/datatable.layered.css +20 -21
  48. package/dist/emptystate.css +15 -1
  49. package/dist/emptystate.d.ts +2 -2
  50. package/dist/emptystate.layered.css +15 -1
  51. package/dist/esm/components/chip/chip.js +3 -2
  52. package/dist/esm/components/chip/chip.recipe.js +37 -1
  53. package/dist/esm/components/datatable/datatable.js +489 -111
  54. package/dist/esm/components/datatable/datatable.recipe.js +4 -0
  55. package/dist/esm/components/emptystate/emptystate.js +4 -3
  56. package/dist/esm/components/emptystate/emptystate.recipe.js +26 -1
  57. package/dist/esm/components/emptystate/index.js +1 -1
  58. package/dist/esm/components/mentionchip/index.js +2 -0
  59. package/dist/esm/components/mentionchip/mentionchip.js +21 -0
  60. package/dist/esm/components/mentionchip/mentionchip.recipe.js +35 -0
  61. package/dist/esm/components/personchip/index.js +1 -0
  62. package/dist/esm/components/personchip/personchip.js +47 -0
  63. package/dist/esm/components/table/table.js +2 -2
  64. package/dist/esm/components/table/table.recipe.js +15 -3
  65. package/dist/esm/components/tabs/tabs.js +2 -2
  66. package/dist/esm/components/tabs/tabs.recipe.js +30 -3
  67. package/dist/esm/components/text/index.js +2 -0
  68. package/dist/esm/components/text/text.js +19 -0
  69. package/dist/esm/components/text/text.recipe.js +31 -0
  70. package/dist/esm/core/floating-stack.js +17 -0
  71. package/dist/esm/core/index.js +1 -1
  72. package/dist/esm/index.js +8 -0
  73. package/dist/esm/product/formdialog/formdialog.js +134 -0
  74. package/dist/esm/product/formdialog/formdialog.recipe.js +23 -0
  75. package/dist/esm/product/formdialog/index.js +2 -0
  76. package/dist/esm/product/headerbar/headerbar.recipe.js +9 -1
  77. package/dist/esm/product/importdialog/importdialog.js +97 -0
  78. package/dist/esm/product/importdialog/importdialog.recipe.js +89 -0
  79. package/dist/esm/product/importdialog/index.js +2 -0
  80. package/dist/esm/product/recordpage/recordpage.recipe.js +10 -1
  81. package/dist/esm/product/recordstable/recordstable.js +90 -8
  82. package/dist/esm/product/richtext/richtext.recipe.js +26 -1
  83. package/dist/esm/product/savedviews/index.js +2 -0
  84. package/dist/esm/product/savedviews/savedviews.js +171 -0
  85. package/dist/esm/product/savedviews/savedviews.recipe.js +100 -0
  86. package/dist/esm/product/taskstable/index.js +2 -0
  87. package/dist/esm/product/taskstable/taskstable.js +24 -0
  88. package/dist/esm/product/taskstable/taskstable.recipe.js +44 -0
  89. package/dist/esm/product/themepicker/index.js +1 -0
  90. package/dist/esm/product/themepicker/themepicker.js +29 -0
  91. package/dist/esm/product/themepicker/themepicker.recipe.js +95 -0
  92. package/dist/esm/tokens/palette.js +30 -13
  93. package/dist/esm/tokens/semantic.js +6 -0
  94. package/dist/formdialog.css +6 -0
  95. package/dist/formdialog.d.ts +7 -0
  96. package/dist/formdialog.layered.css +8 -0
  97. package/dist/headerbar.css +1 -1
  98. package/dist/headerbar.layered.css +1 -1
  99. package/dist/importdialog.css +38 -0
  100. package/dist/importdialog.d.ts +7 -0
  101. package/dist/importdialog.layered.css +40 -0
  102. package/dist/mentionchip.css +3 -0
  103. package/dist/mentionchip.d.ts +7 -0
  104. package/dist/mentionchip.layered.css +5 -0
  105. package/dist/native/tokens.d.ts +1 -1
  106. package/dist/native/tokens.js +14 -6
  107. package/dist/recordpage.css +13 -2
  108. package/dist/recordpage.layered.css +13 -2
  109. package/dist/richtext.css +11 -1
  110. package/dist/richtext.layered.css +11 -1
  111. package/dist/savedviews.css +78 -0
  112. package/dist/savedviews.d.ts +7 -0
  113. package/dist/savedviews.layered.css +80 -0
  114. package/dist/styles.css +287 -37
  115. package/dist/styles.layered.css +287 -37
  116. package/dist/table.css +15 -6
  117. package/dist/table.layered.css +15 -6
  118. package/dist/tabs.css +7 -1
  119. package/dist/tabs.d.ts +2 -2
  120. package/dist/tabs.layered.css +7 -1
  121. package/dist/taskstable.css +23 -0
  122. package/dist/taskstable.d.ts +7 -0
  123. package/dist/taskstable.layered.css +25 -0
  124. package/dist/text.css +9 -0
  125. package/dist/text.d.ts +7 -0
  126. package/dist/text.layered.css +11 -0
  127. package/dist/themepicker.css +27 -0
  128. package/dist/themepicker.d.ts +7 -0
  129. package/dist/themepicker.layered.css +29 -0
  130. package/dist/tokens.css +7 -3
  131. package/dist/tokens.json +14 -6
  132. package/dist/types/components/chip/chip.d.ts +32 -0
  133. package/dist/types/components/chip/chip.recipe.d.ts +31 -0
  134. package/dist/types/components/datatable/datatable.d.ts +39 -4
  135. package/dist/types/components/emptystate/emptystate.d.ts +29 -4
  136. package/dist/types/components/emptystate/emptystate.recipe.d.ts +2 -0
  137. package/dist/types/components/emptystate/index.d.ts +1 -1
  138. package/dist/types/components/mentionchip/index.d.ts +2 -0
  139. package/dist/types/components/mentionchip/mentionchip.d.ts +20 -0
  140. package/dist/types/components/mentionchip/mentionchip.recipe.d.ts +18 -0
  141. package/dist/types/components/personchip/index.d.ts +1 -0
  142. package/dist/types/components/personchip/personchip.d.ts +37 -0
  143. package/dist/types/components/table/table.d.ts +36 -0
  144. package/dist/types/components/tabs/tabs.d.ts +4 -2
  145. package/dist/types/components/tabs/tabs.recipe.d.ts +18 -3
  146. package/dist/types/components/text/index.d.ts +2 -0
  147. package/dist/types/components/text/text.d.ts +32 -0
  148. package/dist/types/components/text/text.recipe.d.ts +40 -0
  149. package/dist/types/core/floating-stack.d.ts +14 -0
  150. package/dist/types/core/index.d.ts +1 -1
  151. package/dist/types/index.d.ts +8 -0
  152. package/dist/types/product/formdialog/formdialog.d.ts +57 -0
  153. package/dist/types/product/formdialog/formdialog.recipe.d.ts +3 -0
  154. package/dist/types/product/formdialog/index.d.ts +2 -0
  155. package/dist/types/product/importdialog/importdialog.d.ts +86 -0
  156. package/dist/types/product/importdialog/importdialog.recipe.d.ts +3 -0
  157. package/dist/types/product/importdialog/index.d.ts +2 -0
  158. package/dist/types/product/recordstable/recordstable.d.ts +24 -2
  159. package/dist/types/product/savedviews/index.d.ts +2 -0
  160. package/dist/types/product/savedviews/savedviews.d.ts +55 -0
  161. package/dist/types/product/savedviews/savedviews.recipe.d.ts +3 -0
  162. package/dist/types/product/taskstable/index.d.ts +2 -0
  163. package/dist/types/product/taskstable/taskstable.d.ts +54 -0
  164. package/dist/types/product/taskstable/taskstable.recipe.d.ts +3 -0
  165. package/dist/types/product/themepicker/index.d.ts +1 -0
  166. package/dist/types/product/themepicker/themepicker.d.ts +21 -0
  167. package/dist/types/product/themepicker/themepicker.recipe.d.ts +3 -0
  168. package/dist/types/tokens/contract.d.ts +2 -0
  169. package/dist/types/tokens/palette.d.ts +11 -3
  170. package/dist/types/tokens/semantic.d.ts +6 -0
  171. package/package.json +41 -1
@@ -17,10 +17,18 @@ import { contract as c } from "../../tokens/index.js";
17
17
  // `title`/`trailing` slot content a consumer supplies. `size.header` (48px)
18
18
  // is reused rather than a new token, since it's the same "workspace-switcher
19
19
  // header height" measured elsewhere in the sidebar.
20
+ //
21
+ // crisp#576: `border-box` plus a plain `height: ${size.header}` meant the
22
+ // bordered variant's own 1px border was eaten OUT OF the 48px box, leaving
23
+ // only 47px of content -- caught in review of internal/grc's own shell
24
+ // shifting up 1px end-to-end after adopting this component. Re-measured
25
+ // live: Attio's own `<header>` is 49px total (border-box) with the 1px
26
+ // border OUTSIDE the 48px content row, not inside it. `[data-bordered]` now
27
+ // grows the box by the border's own width so content stays 48px either way.
20
28
  const config = defineRecipe({
21
29
  name: "headerbar",
22
30
  base: `box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: ${c.space["2"]}; height: ${c.size.header}; flex-shrink: 0; padding: 0 ${c.space["3"]}; font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
23
- &[data-bordered] { border-bottom: 1px solid ${c.borderSubtle}; }
31
+ &[data-bordered] { height: calc(${c.size.header} + 1px); border-bottom: 1px solid ${c.borderSubtle}; }
24
32
  & .crisp-headerbar-leading { display: flex; align-items: center; gap: ${c.space["2"]}; flex-shrink: 0; min-width: 0; }
25
33
  & .crisp-headerbar-title { flex: 1; min-width: 0; display: flex; align-items: center; gap: ${c.space["2"]}; overflow: hidden; }
26
34
  & .crisp-headerbar-trailing { display: flex; align-items: center; gap: ${c.space["2"]}; flex-shrink: 0; }`,
@@ -0,0 +1,97 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useFileUpload } from "@seed-design/react-file-upload";
4
+ import * as dialog from "@zag-js/dialog";
5
+ import { normalizeProps, Portal, useMachine } from "@zag-js/react";
6
+ import { X } from "lucide-react";
7
+ import * as React from "react";
8
+ import { Button } from "../../components/button/index.js";
9
+ import { cx, hasOpenFloatingLayer, useCrispMessage, withDescribedBy, } from "../../core/index.js";
10
+ import { importdialogRecipe } from "./importdialog.recipe.js";
11
+ let instanceCounter = 0;
12
+ function useImportDialogInstanceId() {
13
+ const idRef = React.useRef(null);
14
+ if (idRef.current == null) {
15
+ instanceCounter += 1;
16
+ idRef.current = `crisp-importdialog-${instanceCounter}`;
17
+ }
18
+ return idRef.current;
19
+ }
20
+ /**
21
+ * A full-viewport step wizard — Attio's own CSV import flow (measured live:
22
+ * `1584x871` filling a ~1600x900 viewport, `border-radius: 0`, no shadow,
23
+ * opaque white). Composes Zag's dialog machine for focus-trap/scroll-lock/
24
+ * Escape exactly like `Dialog`/`Drawer`, with its own recipe for the
25
+ * full-bleed chrome neither of those two share (see importdialog.recipe.ts).
26
+ *
27
+ * The step header and footer are generic — the wizard doesn't know about
28
+ * CSV. Step 1's body is typically `ImportDialogDropzone`; steps 2-4
29
+ * (map columns / review values / preview import) are consumer-supplied
30
+ * `children`, since Attio's own column-mapping UI wasn't part of this
31
+ * measurement pass.
32
+ */
33
+ export function ImportDialog({ open, onOpenChange, title = "Import", steps, activeStep, onStepClick, children, primaryLabel = "Next", onPrimary, primaryDisabled, "aria-label": ariaLabel, closeLabel: closeLabelProp, className, style, "aria-describedby": describedBy, ...rest }) {
34
+ const closeLabel = useCrispMessage(closeLabelProp, "Close");
35
+ const id = useImportDialogInstanceId();
36
+ const service = useMachine(dialog.machine, {
37
+ id,
38
+ open,
39
+ onOpenChange(details) {
40
+ onOpenChange(details.open);
41
+ },
42
+ "aria-label": typeof title === "string" ? title : ariaLabel,
43
+ // Defer to a nested Menu/Select/Popover/ContextMenu's own Escape
44
+ // handling (#361) -- see Dialog's identical comment; this composes the
45
+ // same Zag machine.
46
+ onEscapeKeyDown(event) {
47
+ if (hasOpenFloatingLayer())
48
+ event.preventDefault();
49
+ },
50
+ });
51
+ const api = dialog.connect(service, normalizeProps);
52
+ if (!api.open)
53
+ return null;
54
+ return (_jsx(Portal, { children: _jsxs("div", { className: importdialogRecipe(), children: [_jsx("div", { ...api.getBackdropProps(), className: "crisp-importdialog-backdrop" }), _jsx("div", { ...api.getPositionerProps(), className: "crisp-importdialog-positioner", children: _jsxs("div", { ...rest, ...withDescribedBy(api.getContentProps(), describedBy), className: cx("crisp-importdialog-content", className), style: style, "data-crisp-importdialog": id, children: [_jsxs("div", { className: "crisp-importdialog-header", children: [_jsx("h2", { ...api.getTitleProps(), className: "crisp-importdialog-title", children: title }), _jsx("button", { type: "button", ...api.getCloseTriggerProps(), className: "crisp-importdialog-close", "aria-label": closeLabel, children: _jsx(X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] }), _jsx("nav", { className: "crisp-importdialog-steps", "aria-label": (typeof title === "string" ? title : ariaLabel) ?? "Steps", children: steps.map((step) => {
55
+ const isActive = step.id === activeStep;
56
+ const commonProps = {
57
+ className: "crisp-importdialog-step",
58
+ "data-active": isActive ? "" : undefined,
59
+ "aria-current": isActive ? "step" : undefined,
60
+ };
61
+ return onStepClick ? (_jsx("button", { type: "button", ...commonProps, onClick: () => onStepClick(step.id), children: step.label }, step.id)) : (_jsx("span", { ...commonProps, children: step.label }, step.id));
62
+ }) }), _jsx("div", { className: "crisp-importdialog-body", children: children }), _jsx("div", { className: "crisp-importdialog-footer", children: _jsx(Button, { type: "button", intent: "primary", size: "md", disabled: primaryDisabled, onClick: onPrimary, children: primaryLabel }) })] }) })] }) }));
63
+ }
64
+ ImportDialog.displayName = "ImportDialog";
65
+ /**
66
+ * The step-1 drop target — measured chrome (`border: 1px dashed
67
+ * rgb(230,231,234)`, `border-radius: 20px`, `background: rgb(251,251,251)`)
68
+ * over the SAME headless drag/drop + file-picker engine `FileUpload` uses
69
+ * (`@seed-design/react-file-upload`'s `useFileUpload`), rather than a fork
70
+ * of it. `FileUpload`'s own rendered markup (a single button carrying one
71
+ * title + one hint) can't host this dropzone's four separate pieces of copy
72
+ * — a title, an "or" divider, a "Choose a .CSV file" trigger, and a "Learn
73
+ * more" link that needs its own `<a href>` — so this composes the hook
74
+ * directly instead of wrapping `<FileUpload>`, and the engine (not this
75
+ * file) still owns every bit of drag-state/accept/reject behaviour.
76
+ */
77
+ export function ImportDialogDropzone({ accept = ".csv", disabled, maxFileSize, onFileAccept, onFileReject, dropLabel = "Drop your .CSV file onto this area to upload", orLabel = "or", chooseLabel = "Choose a .CSV file", learnMoreHref, learnMoreLabel = "Learn more", className, id, ...rest }) {
78
+ const [rejections, setRejections] = React.useState([]);
79
+ const { dropzoneProps, triggerProps, hiddenInputProps, inputRef } = useFileUpload({
80
+ accept,
81
+ disabled,
82
+ maxFileSize,
83
+ maxFiles: 1,
84
+ onFileReject: (r) => {
85
+ setRejections(r);
86
+ onFileReject?.(r);
87
+ },
88
+ onFileAccept: (entries) => {
89
+ setRejections([]);
90
+ const file = entries[entries.length - 1]?.file;
91
+ if (file)
92
+ onFileAccept?.(file);
93
+ },
94
+ });
95
+ return (_jsxs("div", { ...rest, ...dropzoneProps, className: cx("crisp-importdialog-dropzone", className), children: [_jsx("input", { ...hiddenInputProps, id: id ?? hiddenInputProps.id, ref: inputRef, className: "crisp-importdialog-dropzone-input" }), _jsx("span", { className: "crisp-importdialog-dropzone-title", children: dropLabel }), _jsx("span", { className: "crisp-importdialog-dropzone-or", children: orLabel }), _jsx("button", { ...triggerProps, className: "crisp-importdialog-dropzone-choose", children: chooseLabel }), learnMoreHref && (_jsx("a", { className: "crisp-importdialog-dropzone-learnmore", href: learnMoreHref, target: "_blank", rel: "noreferrer", children: learnMoreLabel })), _jsx("ul", { className: "crisp-importdialog-dropzone-errors", "aria-live": "polite", children: rejections.map((r) => (_jsx("li", { className: "crisp-importdialog-dropzone-error", children: r.file.name }, r.file.name))) })] }));
96
+ }
97
+ ImportDialogDropzone.displayName = "ImportDialogDropzone";
@@ -0,0 +1,89 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // A full-viewport wizard takeover — measured live from Attio's own Import CSV
4
+ // flow (app.attio.com/techpartners/companies -> toolbar "Import / Export" ->
5
+ // "Import CSV"): panel `1584x871` in a ~1600x900 viewport (i.e. it fills the
6
+ // content area, not a small centred modal like `Dialog`), `border-radius: 0`,
7
+ // `box-shadow: none`, opaque `rgb(255,255,255)` background. Unlike
8
+ // Dialog/Drawer's frosted, edge-gutter surfaces (AUTHORING Lesson 30), a
9
+ // full-viewport takeover has no edge to float off of, so it gets its own
10
+ // recipe rather than composing either.
11
+ //
12
+ // The 4-step header renders each step as a 28px pill (`size.row`), 8px
13
+ // corner (`radius.md`), transparent at rest. Attio's own active-step
14
+ // treatment wasn't captured in the measurement pass, so the current step
15
+ // reuses crisp's existing brand-tint pattern (14%/82% `color-mix` — the same
16
+ // ratio `RecordsTable`'s brand-tone tag and `MultiSelect`'s brand chip
17
+ // already use) rather than inventing a new colour — a documented scope
18
+ // decision, not a guess.
19
+ //
20
+ // The dropzone is `border: 1px dashed rgb(230,231,234)` (`borderDropzone`,
21
+ // see tokens/palette.ts), `border-radius: 20px`, `background: rgb(251,251,251)`
22
+ // (`bgSubtle`). 20px sits between the scale's `2xl` (16px) and `full` (999px)
23
+ // with no other measured use yet — left as a raw px per AUTHORING 4b's
24
+ // "genuine one-off" exception rather than added to the shared scale for a
25
+ // single consumer.
26
+ //
27
+ // The primary "Next" button is composed from crisp's own `Button` (`intent`
28
+ // `primary`, `size` `md` — 28px tall, 8px corner, brand solid background —
29
+ // matches the measurement exactly), not a hand-rolled button.
30
+ const config = defineRecipe({
31
+ name: "importdialog",
32
+ base: `display: contents; font-family: ${c.font.sans};
33
+ & .crisp-importdialog-backdrop { position: fixed; inset: 0; z-index: 60; background: transparent; }
34
+ & .crisp-importdialog-positioner { position: fixed; inset: 0; z-index: 61; display: flex; }
35
+ & .crisp-importdialog-content {
36
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
37
+ position: relative; display: flex; flex-direction: column; box-sizing: border-box;
38
+ width: 100%; height: 100%; border-radius: 0; background: ${c.bgSurface}; box-shadow: none;
39
+ transform: none; opacity: 1; transition: opacity 0.2s ease;
40
+ @starting-style { opacity: 0; }
41
+ }
42
+ & .crisp-importdialog-header { display: flex; align-items: center; gap: ${c.space["2"]}; flex-shrink: 0; height: 52px; padding: 0 ${c.space["2"]} 0 ${c.space["6"]}; border-bottom: 1px solid ${c.borderSubtle}; }
43
+ & .crisp-importdialog-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.xl}; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; margin: 0; }
44
+ & .crisp-importdialog-close { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: none; color: ${c.fgMuted}; font-family: inherit; cursor: pointer; }
45
+ & .crisp-importdialog-close:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
46
+
47
+ & .crisp-importdialog-steps { display: flex; align-items: center; gap: ${c.space["2"]}; flex-shrink: 0; padding: ${c.space["4"]} ${c.space["6"]} 0; }
48
+ & .crisp-importdialog-step {
49
+ display: inline-flex; align-items: center; height: ${c.size.row}; padding: 0 ${c.space["2.5"]};
50
+ border: none; border-radius: ${c.radius.md}; background: transparent; color: ${c.fgMuted};
51
+ font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; cursor: default;
52
+ transition: background-color .12s, color .12s;
53
+ }
54
+ & button.crisp-importdialog-step { cursor: pointer; }
55
+ & .crisp-importdialog-step[data-active] { background: color-mix(in srgb, ${c.bgBrandSolid} 14%, transparent); color: color-mix(in srgb, ${c.bgBrandSolid} 82%, ${c.fgPrimary}); }
56
+ & .crisp-importdialog-step:focus-visible { outline: 2px solid ${c.strokeFocusRing}; outline-offset: 1px; }
57
+
58
+ & .crisp-importdialog-body { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow-y: auto; padding: ${c.space["6"]}; }
59
+
60
+ & .crisp-importdialog-footer { display: flex; align-items: center; justify-content: flex-end; gap: ${c.space["3"]}; flex-shrink: 0; min-height: 56px; padding: 0 ${c.space["6"]}; border-top: 1px solid ${c.borderSubtle}; }
61
+
62
+ & .crisp-importdialog-dropzone {
63
+ box-sizing: border-box; position: relative; flex: 1; min-height: 0;
64
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
65
+ gap: ${c.space["1"]}; width: 100%;
66
+ border: 1px dashed ${c.borderDropzone}; border-radius: 20px; background: ${c.bgSubtle};
67
+ text-align: center; transition: background-color .12s, border-color .12s;
68
+ }
69
+ & .crisp-importdialog-dropzone[data-dragging-over] { border-color: ${c.bgBrandSolid}; background: color-mix(in srgb, ${c.bgBrandSolid} 6%, transparent); }
70
+ & .crisp-importdialog-dropzone[data-invalid] { border-color: ${c.bgDanger}; }
71
+ & .crisp-importdialog-dropzone-title { color: ${c.fgPrimary}; font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium}; }
72
+ & .crisp-importdialog-dropzone-or { color: ${c.fgMuted}; font-size: ${c.text.md}; line-height: 18px; }
73
+ & .crisp-importdialog-dropzone-choose {
74
+ border: none; background: none; padding: 0; margin: 0; font-family: inherit;
75
+ font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.bgBrandSolid}; cursor: pointer;
76
+ }
77
+ & .crisp-importdialog-dropzone-choose:hover { color: ${c.bgBrandSolidPressed}; }
78
+ & .crisp-importdialog-dropzone-choose:focus-visible { outline: 2px solid ${c.strokeFocusRing}; outline-offset: 2px; border-radius: ${c.radius.sm}; }
79
+ & .crisp-importdialog-dropzone-learnmore { color: ${c.fgMuted}; font-size: ${c.text.md}; text-decoration: underline; }
80
+ & .crisp-importdialog-dropzone-input { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
81
+ & .crisp-importdialog-dropzone-errors { margin: 0; padding: 0; list-style: none; }
82
+ & .crisp-importdialog-dropzone-errors:empty { position: absolute; }
83
+ & .crisp-importdialog-dropzone-error { color: ${c.bgDanger}; font-size: ${c.text.md}; line-height: 18px; }`,
84
+ variants: {},
85
+ defaultVariants: {},
86
+ compoundVariants: [],
87
+ });
88
+ export const importdialogRecipe = resolveRecipe(config);
89
+ export default config;
@@ -0,0 +1,2 @@
1
+ export * from "./importdialog.js";
2
+ export * from "./importdialog.recipe.js";
@@ -6,6 +6,14 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
6
6
  // column (transparent, border-right) and a flexible main area whose tab bar
7
7
  // (14/500 labels over a sliding ink underline) switches content — Overview shows
8
8
  // a bordered Highlights stat-grid and an Activity feed.
9
+ //
10
+ // `.crisp-recordpage-panel`'s own `padding-top` (c.space["4"], 16px) was
11
+ // dropped (your-moon/crisp#588): this panel is only ever rendered as Tabs'
12
+ // own children (every branch of `builtinPanel` in recordpage.tsx, all fed to
13
+ // `<Tabs>` below), so it sat inside `.crisp-tabs-panel`, which now carries
14
+ // its own measured 32px top-inset by default — keeping both would have
15
+ // stacked to 48px. Caught by tracing every consumer of `Tabs` before
16
+ // shipping its new default, not by a screenshot.
9
17
  const config = defineRecipe({
10
18
  name: "recordpage",
11
19
  base: `
@@ -125,7 +133,8 @@ const config = defineRecipe({
125
133
 
126
134
  & .crisp-recordpage-main { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; overflow-y: auto; padding: ${c.space["3"]} ${c.space["5"]}; }
127
135
 
128
- & .crisp-recordpage-panel { padding-top: ${c.space["4"]}; display: flex; flex-direction: column; gap: ${c.space["4"]}; }
136
+ /* no padding-top: Tabs' own panel supplies it now (#588) */
137
+ & .crisp-recordpage-panel { display: flex; flex-direction: column; gap: ${c.space["4"]}; }
129
138
  & .crisp-recordpage-card { border: 1px solid ${c.borderSubtle}; border-radius: ${c.radius["2xl"]}; background: ${c.bgSurface}; overflow: hidden; }
130
139
  & .crisp-recordpage-card-title { font-size: ${c.text.lg}; font-weight: ${c.weight.semibold}; letter-spacing: -0.14px; color: ${c.fgPrimary}; padding: ${c.space["3"]} ${c.space["3"]} ${c.space["2"]}; }
131
140
  & .crisp-recordpage-hl-card { border: 1px solid ${c.borderSubtle}; border-radius: ${c.radius["2xl"]}; overflow: hidden; }
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ArrowDown, ArrowDownWideNarrow, ArrowLeft, ArrowRight, ArrowUp, ArrowUpDown, ArrowUpNarrowWide, ArrowUpRight, AtSign, Briefcase, Building2, Calendar, Check, CircleHelp, CircleUserRound, Copy, CopyPlus, CornerDownLeft, Download, EyeOff, Globe, GripVertical, Hash, Inbox, ListFilter, ListPlus, Mail, MapPin, MoreHorizontal, Pencil, Plus, Star, Tag, Trash2, TrendingUp, Users, Workflow, X, } from "lucide-react";
2
+ import { ArrowDown, ArrowDownWideNarrow, ArrowLeft, ArrowRight, ArrowUp, ArrowUpDown, ArrowUpNarrowWide, ArrowUpRight, AtSign, Briefcase, Building2, Calendar, Check, CircleHelp, CircleUserRound, Copy, CopyPlus, CornerDownLeft, Download, EyeOff, Globe, GripVertical, Hash, Inbox, ListFilter, ListPlus, Mail, MapPin, MoreHorizontal, Pencil, Plug, Plus, Star, Tag, Trash2, TrendingUp, Upload, Users, Workflow, X, } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { createPortal } from "react-dom";
5
5
  import { Avatar } from "../../components/avatar/index.js";
@@ -8,15 +8,67 @@ import { Button } from "../../components/button/index.js";
8
8
  import { Checkbox } from "../../components/checkbox/index.js";
9
9
  import { DatePickerField } from "../../components/datepickerfield/index.js";
10
10
  import { Dialog, DialogBody, DialogFooter } from "../../components/dialog/index.js";
11
- import { EmptyState } from "../../components/emptystate/index.js";
11
+ import { EmptyState, } from "../../components/emptystate/index.js";
12
12
  import { Input } from "../../components/input/index.js";
13
13
  import { Menu, MenuItem, useMenuKeyboard } from "../../components/menu/index.js";
14
14
  import { MultiSelect } from "../../components/multiselect/index.js";
15
15
  import { Select } from "../../components/select/index.js";
16
16
  import { Switch } from "../../components/switch/index.js";
17
+ import { Tooltip } from "../../components/tooltip/index.js";
17
18
  import { isInsideLaterFloatingLayer, isRtlElement, useCrispComponentDefaults, usePrefersReducedMotion, } from "../../core/index.js";
18
19
  import { companyLogos } from "./company-logos.js";
19
20
  import { recordstableRecipe } from "./recordstable.recipe.js";
21
+ // #534: whether an element's own content is being cut off by its own box --
22
+ // the same fact `.crisp-recordstable-th-label`'s CSS ellipsis is silently
23
+ // applying, exported so the comparison itself (not layout) can be
24
+ // mutation-tested without a real browser. Identical to DataTable's own
25
+ // `isLabelTruncated` (components/datatable/datatable.tsx) -- not shared
26
+ // because `product/` can depend on `components/` but not the other way, and
27
+ // this file already duplicates DataTable's other measured header behaviour
28
+ // rather than reach across that boundary.
29
+ export function isLabelTruncated(el) {
30
+ return el.scrollWidth > el.clientWidth;
31
+ }
32
+ /**
33
+ * The header label span: plain when it fits, wrapped in the shared `Tooltip`
34
+ * (full text, `asChild` so the wrapper never displaces the ellipsis CSS the
35
+ * span already carries -- see `Tooltip`'s own `asChild` doc) once it's
36
+ * actually cut off. Re-measures on mount and on resize (column drag,
37
+ * viewport change, label change) via `ResizeObserver`; SSR-safe the same way
38
+ * `MiddleTruncate` is -- the DOM is only touched inside an effect, so the
39
+ * server (and the first client paint) renders the plain span.
40
+ */
41
+ function ThLabel({ label }) {
42
+ const ref = React.useRef(null);
43
+ const [overflowing, setOverflowing] = React.useState(false);
44
+ // 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.
45
+ React.useLayoutEffect(() => {
46
+ const el = ref.current;
47
+ if (!el)
48
+ return;
49
+ const measure = () => setOverflowing(isLabelTruncated(el));
50
+ measure();
51
+ if (typeof ResizeObserver === "undefined")
52
+ return;
53
+ const ro = new ResizeObserver(measure);
54
+ ro.observe(el);
55
+ return () => ro.disconnect();
56
+ }, [label]);
57
+ // `Tooltip` always wraps the SAME span (never swapped in/out of the tree
58
+ // based on `overflowing`) so the span's own DOM node identity survives
59
+ // every toggle -- a real-browser-only bug (happy-dom has no
60
+ // ResizeObserver, so the test suite never exercised this path): swapping
61
+ // between a bare `<span>` and `<Tooltip asChild><span/></Tooltip>` changes
62
+ // the element type at this position, so React unmounts the old span and
63
+ // mounts a new one; the effect above never re-runs (its deps are
64
+ // `[label]`, unchanged), so its ResizeObserver keeps watching the now
65
+ // -detached old node, whose deferred initial callback then reports 0x0 and
66
+ // flips `overflowing` straight back to false -- the tooltip would never
67
+ // actually show. `open={false}` when it fits keeps the Tooltip mounted
68
+ // (asChild costs no extra DOM either way) but pins it permanently closed,
69
+ // so hovering a non-overflowing header still opens nothing.
70
+ return (_jsx(Tooltip, { content: label, asChild: true, open: overflowing ? undefined : false, children: _jsx("span", { ref: ref, className: "crisp-recordstable-th-label", children: label }) }));
71
+ }
20
72
  const EDITABLE = new Set(["text", "number", "url", "currency"]);
21
73
  // types whose cell is an always-interactive picker (not double-click-to-edit)
22
74
  const PICKER = new Set(["status", "reference", "date", "tags"]);
@@ -100,6 +152,8 @@ export const recordsTableLabels = {
100
152
  setRecordPlaceholder: "Set a record…",
101
153
  noRecords: "No records yet",
102
154
  untitled: "Untitled",
155
+ importCsv: "Import CSV",
156
+ connectDataSource: "Connect data source",
103
157
  agg: {
104
158
  none: "None",
105
159
  count: "Count all",
@@ -111,6 +165,9 @@ export const recordsTableLabels = {
111
165
  },
112
166
  op: {
113
167
  contains: "contains",
168
+ doesnotcontain: "does not contain",
169
+ startswith: "starts with",
170
+ endswith: "ends with",
114
171
  is: "is",
115
172
  isnot: "is not",
116
173
  empty: "is empty",
@@ -137,8 +194,17 @@ const compact = new Intl.NumberFormat("en", {
137
194
  const CHECK_W = 40;
138
195
  const ADDCOL_W = 44;
139
196
  const colWidth = (col) => col.width ?? (col.type === "entity" ? 240 : 180);
140
- const OP_ORDER = ["contains", "is", "isnot", "empty", "notempty"];
141
- const opNeedsValue = (op) => op === "contains" || op === "is" || op === "isnot";
197
+ const OP_ORDER = [
198
+ "contains",
199
+ "doesnotcontain",
200
+ "startswith",
201
+ "endswith",
202
+ "is",
203
+ "isnot",
204
+ "empty",
205
+ "notempty",
206
+ ];
207
+ const opNeedsValue = (op) => op !== "empty" && op !== "notempty";
142
208
  function matchesFilter(col, row, cond) {
143
209
  if (cond.op === "empty")
144
210
  return !isFilled(col, row);
@@ -152,6 +218,12 @@ function matchesFilter(col, row, cond) {
152
218
  return true;
153
219
  if (cond.op === "contains")
154
220
  return t.includes(v);
221
+ if (cond.op === "doesnotcontain")
222
+ return !t.includes(v);
223
+ if (cond.op === "startswith")
224
+ return t.startsWith(v);
225
+ if (cond.op === "endswith")
226
+ return t.endsWith(v);
155
227
  if (cond.op === "is")
156
228
  return t === v;
157
229
  return t !== v; // isnot
@@ -433,7 +505,7 @@ function CreateRecordDialog({ open, onOpenChange, columns, entityLabel, onCreate
433
505
  * and a floating bulk-action bar (delete / deselect) when rows are selected.
434
506
  * Selection can be controlled via `selectedIds`/`onSelectionChange`.
435
507
  */
436
- export function RecordsTable({ labels, columns: columnsProp = defaultColumns, rows: rowsProp = defaultRows, selectedIds, onSelectionChange, showNew = true, allowAddColumn = true, onOpenRecord, onOpenRecordPage, avatarTone = "brand", className, ...rest }) {
508
+ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, rows: rowsProp = defaultRows, selectedIds, onSelectionChange, showNew = true, allowAddColumn = true, onOpenRecord, onOpenRecordPage, avatarTone = "brand", emptyStateExtra, className, ...rest }) {
437
509
  const [cols, setCols] = React.useState(columnsProp);
438
510
  const [data, setData] = React.useState(rowsProp);
439
511
  // The table owns `cols`/`data` so it can rename a column, edit a cell, or
@@ -1101,7 +1173,7 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
1101
1173
  setDragCol(col.key);
1102
1174
  e.dataTransfer.effectAllowed = "move";
1103
1175
  }
1104
- : undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? _jsx(Icon, { size: 16, strokeWidth: 1.75 }) : null, _jsx("span", { className: "crisp-recordstable-th-label", children: col.label }), _jsx("span", { className: "crisp-recordstable-sort", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: _jsx("path", { d: "m4 6 4 4 4-4" }) }) })] })), openMenu === `col:${col.key}` &&
1176
+ : undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? _jsx(Icon, { size: 16, strokeWidth: 1.75 }) : null, _jsx(ThLabel, { label: col.label }), _jsx("span", { className: "crisp-recordstable-sort", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: _jsx("path", { d: "m4 6 4 4 4-4" }) }) })] })), openMenu === `col:${col.key}` &&
1105
1177
  rootRef.current &&
1106
1178
  createPortal(_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: [_jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "asc"), children: [_jsx(ArrowUp, { size: 15, strokeWidth: 1.75 }), t.sortAscending] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "desc"), children: [_jsx(ArrowDown, { size: 15, strokeWidth: 1.75 }), t.sortDescending] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i <= 1, onClick: () => moveColumn(col.key, -1), children: [_jsx(ArrowLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveLeft] }), _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: [_jsx(ArrowRight, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveRight] }), _jsx("div", { className: "crisp-recordstable-menu-sep" }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => {
1107
1179
  setRenamingCol(col.key);
@@ -1170,9 +1242,19 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
1170
1242
  }, children: res ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-recordstable-sum-label", children: t.agg[a] }), _jsx("span", { className: "crisp-recordstable-sum-value", children: res.value })] })) : (_jsx("span", { className: "crisp-recordstable-sum-label", children: t.calculate })) }), sumOpen &&
1171
1243
  rootRef.current &&
1172
1244
  createPortal(_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) => (_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 ? (_jsx(Check, { className: "crisp-recordstable-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, opt))) }), rootRef.current)] }) }, col.key));
1173
- }), _jsx("td", { className: "crisp-recordstable-sum crisp-recordstable-addcol" })] }) })] }), visibleRows.length === 0 ? (_jsx("div", { className: "crisp-recordstable-empty", children: _jsx(EmptyState, { variant: "page", icon: activeFilters ? (_jsx(ListFilter, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })) : (_jsx(Inbox, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })), title: activeFilters ? "No records match that filter" : t.noRecords, description: activeFilters
1245
+ }), _jsx("td", { className: "crisp-recordstable-sum crisp-recordstable-addcol" })] }) })] }), visibleRows.length === 0 ? (_jsx("div", { className: "crisp-recordstable-empty", children: _jsx(EmptyState
1246
+ // #603: the unfiltered zero-records state, when the consumer
1247
+ // opts into the extra CTAs, uses Attio's own larger measured
1248
+ // tile (Users/Workspaces, 138px) instead of the 45px
1249
+ // filtered-to-zero ring -- two different Attio surfaces.
1250
+ , {
1251
+ // #603: the unfiltered zero-records state, when the consumer
1252
+ // opts into the extra CTAs, uses Attio's own larger measured
1253
+ // tile (Users/Workspaces, 138px) instead of the 45px
1254
+ // filtered-to-zero ring -- two different Attio surfaces.
1255
+ variant: !activeFilters && emptyStateExtra ? "records" : "page", icon: activeFilters ? (_jsx(ListFilter, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })) : (_jsx(Inbox, { size: !activeFilters && emptyStateExtra ? 40 : 18, strokeWidth: 1.25, "aria-hidden": "true" })), title: activeFilters ? "No records match that filter" : t.noRecords, description: activeFilters
1174
1256
  ? "None of your records match the filters that are set."
1175
- : `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? (_jsxs(Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [_jsx(ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : (_jsxs(Button, { intent: "primary", size: "sm", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] })) }) })) : (_jsxs("button", { type: "button", className: "crisp-recordstable-addrow", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), _jsx("div", { className: "crisp-recordstable-live", role: "status", "aria-live": "polite", children: selectionAnnouncement }), selected.size > 0 && (_jsxs(BulkBar, { count: selected.size, countLabel: t.selectedCount, onClear: () => setSelected(new Set()), children: [_jsx(BulkBarButton, { icon: _jsx(ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), _jsx(BulkBarButton, { icon: _jsx(Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), _jsx(BulkBarButton, { icon: _jsx(Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), _jsxs(Menu, { align: "end", "aria-label": t.moreActions, trigger: _jsx(BulkBarButton, { icon: _jsx(MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: [_jsx(MenuItem, { icon: _jsx(Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: t.exportCsv }), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete })] })] })), _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 &&
1257
+ : `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? (_jsxs(Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [_jsx(ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : (_jsxs(_Fragment, { children: [_jsxs(Button, { intent: "primary", size: "sm", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] }), emptyStateExtra?.onImportCsv && (_jsxs(Button, { intent: "neutral", size: "sm", onClick: emptyStateExtra.onImportCsv, children: [_jsx(Upload, { size: 15, strokeWidth: 1.75 }), t.importCsv] })), emptyStateExtra?.onConnectDataSource && (_jsxs(Button, { intent: "neutral", size: "sm", onClick: emptyStateExtra.onConnectDataSource, children: [_jsx(Plug, { size: 15, strokeWidth: 1.75 }), t.connectDataSource] }))] })), learnMore: !activeFilters ? emptyStateExtra?.learnMore : undefined }) })) : (_jsxs("button", { type: "button", className: "crisp-recordstable-addrow", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), _jsx("div", { className: "crisp-recordstable-live", role: "status", "aria-live": "polite", children: selectionAnnouncement }), selected.size > 0 && (_jsxs(BulkBar, { count: selected.size, countLabel: t.selectedCount, onClear: () => setSelected(new Set()), children: [_jsx(BulkBarButton, { icon: _jsx(ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), _jsx(BulkBarButton, { icon: _jsx(Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), _jsx(BulkBarButton, { icon: _jsx(Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), _jsxs(Menu, { align: "end", "aria-label": t.moreActions, trigger: _jsx(BulkBarButton, { icon: _jsx(MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: [_jsx(MenuItem, { icon: _jsx(Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: t.exportCsv }), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete })] })] })), _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 &&
1176
1258
  createPortal(_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: [_jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => copyRow(ctxMenu.id), children: [_jsx(Copy, { size: 15, strokeWidth: 1.75 }), t.copy] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => duplicateRow(ctxMenu.id), children: [_jsx(CopyPlus, { size: 15, strokeWidth: 1.75 }), t.duplicate] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => pinRow(ctxMenu.id), children: [_jsx(Star, { size: 15, strokeWidth: 1.75 }), t.addToList] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item crisp-recordstable-menu-item--danger", role: "menuitem", tabIndex: -1, onClick: () => deleteRow(ctxMenu.id), children: [_jsx(Trash2, { size: 15, strokeWidth: 1.75 }), t.delete] })] }), rootRef.current ?? document.body)] }));
1177
1259
  }
1178
1260
  export const defaultColumns = [
@@ -40,7 +40,32 @@ const config = defineRecipe({
40
40
  & .crisp-rte-body h1 { font-size: ${c.text.xl}; line-height: 28px; font-weight: ${c.weight.semibold}; letter-spacing: -0.3px; margin-top: ${c.space["4"]}; }
41
41
  & .crisp-rte-body h2 { font-size: 17px; line-height: 24px; font-weight: ${c.weight.semibold}; letter-spacing: -0.2px; margin-top: ${c.space["3"]}; }
42
42
  & .crisp-rte-body h3 { font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.semibold}; margin-top: ${c.space["2.5"]}; }
43
- & .crisp-rte-body ul, & .crisp-rte-body ol { padding-inline-start: ${c.space["5"]}; }
43
+ /* #575: a consumer's CSS reset (e.g. Tailwind preflight's un-scoped
44
+ ul, ol { list-style: none }, specificity 0,0,1) was winning every
45
+ list marker outright -- this rule set padding but never declared
46
+ list-style-type, so there was no higher-specificity value (this
47
+ selector is 0,1,1) to beat the reset with. Split into two
48
+ single-selector rules, each declaring its own marker type, rather
49
+ than the old single comma-joined rule ( .crisp-rte-body ul, &
50
+ .crisp-rte-body ol ) the issue flagged as malformed.
51
+ #611: padding-inline-start re-measured live once CRUD on the
52
+ measurement workspace was allowed (a "zz-parity" note with a
53
+ bulleted list, a nested bulleted list, and a numbered list) --
54
+ Attio's own <ul>/<ol> measures 36px (getComputedStyle,
55
+ ~36.1353px), not the 20px placeholder #575 shipped with no real
56
+ content to check against. 36px isn't in tokens/scale.ts (steps
57
+ stop at 24px, jump to 32px) and is a single component-intrinsic
58
+ value, so it stays a raw px per AUTHORING.md's golden rule 4b
59
+ rather than being rounded to an existing token or added to the
60
+ shared scale. Marker colour (currentColor via the already-measured
61
+ fgPrimary, confirmed to equal Attio's measured rgb(16,17,18) ==
62
+ #101112 exactly) and the ~4px li-to-li gap (the existing li
63
+ margin-top rule below) were re-checked at the same time and
64
+ already matched -- only the padding was wrong. Attio doesn't vary
65
+ marker type for a nested list either (nested <ul> is disc too,
66
+ not circle), which this already doesn't attempt. */
67
+ & .crisp-rte-body ul { list-style-type: disc; padding-inline-start: 36px; }
68
+ & .crisp-rte-body ol { list-style-type: decimal; padding-inline-start: 36px; }
44
69
  & .crisp-rte-body li { margin-top: ${c.space["1"]}; }
45
70
  & .crisp-rte-body blockquote { padding-inline-start: ${c.space["3"]}; border-inline-start: 2px solid ${c.borderDefault}; color: ${c.fgMuted}; }
46
71
  & .crisp-rte-body code { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: ${c.text.md}; padding: 1px ${c.space["1"]}; border-radius: ${c.radius.sm}; background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); }
@@ -0,0 +1,2 @@
1
+ export { SavedViews, } from "./savedviews.js";
2
+ export { savedviewsRecipe } from "./savedviews.recipe.js";