@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.
- package/dist/chip.css +7 -1
- package/dist/chip.d.ts +2 -1
- package/dist/chip.layered.css +7 -1
- package/dist/cjs/components/chip/chip.js +3 -2
- package/dist/cjs/components/chip/chip.recipe.js +37 -1
- package/dist/cjs/components/datatable/datatable.js +490 -111
- package/dist/cjs/components/datatable/datatable.recipe.js +4 -0
- package/dist/cjs/components/emptystate/emptystate.js +3 -2
- package/dist/cjs/components/emptystate/emptystate.recipe.js +26 -1
- package/dist/cjs/components/mentionchip/index.js +7 -0
- package/dist/cjs/components/mentionchip/mentionchip.js +57 -0
- package/dist/cjs/components/mentionchip/mentionchip.recipe.js +38 -0
- package/dist/cjs/components/personchip/index.js +17 -0
- package/dist/cjs/components/personchip/personchip.js +50 -0
- package/dist/cjs/components/table/table.js +2 -2
- package/dist/cjs/components/table/table.recipe.js +15 -3
- package/dist/cjs/components/tabs/tabs.js +2 -2
- package/dist/cjs/components/tabs/tabs.recipe.js +30 -3
- package/dist/cjs/components/text/index.js +7 -0
- package/dist/cjs/components/text/text.js +55 -0
- package/dist/cjs/components/text/text.recipe.js +34 -0
- package/dist/cjs/core/floating-stack.js +18 -0
- package/dist/cjs/core/index.js +2 -1
- package/dist/cjs/index.js +8 -0
- package/dist/cjs/product/formdialog/formdialog.js +171 -0
- package/dist/cjs/product/formdialog/formdialog.recipe.js +26 -0
- package/dist/cjs/product/formdialog/index.js +8 -0
- package/dist/cjs/product/headerbar/headerbar.recipe.js +9 -1
- package/dist/cjs/product/importdialog/importdialog.js +134 -0
- package/dist/cjs/product/importdialog/importdialog.recipe.js +92 -0
- package/dist/cjs/product/importdialog/index.js +18 -0
- package/dist/cjs/product/recordpage/recordpage.recipe.js +10 -1
- package/dist/cjs/product/recordstable/recordstable.js +89 -6
- package/dist/cjs/product/richtext/richtext.recipe.js +26 -1
- package/dist/cjs/product/savedviews/index.js +7 -0
- package/dist/cjs/product/savedviews/savedviews.js +207 -0
- package/dist/cjs/product/savedviews/savedviews.recipe.js +103 -0
- package/dist/cjs/product/taskstable/index.js +7 -0
- package/dist/cjs/product/taskstable/taskstable.js +27 -0
- package/dist/cjs/product/taskstable/taskstable.recipe.js +47 -0
- package/dist/cjs/product/themepicker/index.js +17 -0
- package/dist/cjs/product/themepicker/themepicker.js +32 -0
- package/dist/cjs/product/themepicker/themepicker.recipe.js +98 -0
- package/dist/cjs/tokens/palette.js +30 -13
- package/dist/cjs/tokens/semantic.js +6 -0
- package/dist/datatable.css +20 -21
- package/dist/datatable.layered.css +20 -21
- package/dist/emptystate.css +15 -1
- package/dist/emptystate.d.ts +2 -2
- package/dist/emptystate.layered.css +15 -1
- package/dist/esm/components/chip/chip.js +3 -2
- package/dist/esm/components/chip/chip.recipe.js +37 -1
- package/dist/esm/components/datatable/datatable.js +489 -111
- package/dist/esm/components/datatable/datatable.recipe.js +4 -0
- package/dist/esm/components/emptystate/emptystate.js +4 -3
- package/dist/esm/components/emptystate/emptystate.recipe.js +26 -1
- package/dist/esm/components/emptystate/index.js +1 -1
- package/dist/esm/components/mentionchip/index.js +2 -0
- package/dist/esm/components/mentionchip/mentionchip.js +21 -0
- package/dist/esm/components/mentionchip/mentionchip.recipe.js +35 -0
- package/dist/esm/components/personchip/index.js +1 -0
- package/dist/esm/components/personchip/personchip.js +47 -0
- package/dist/esm/components/table/table.js +2 -2
- package/dist/esm/components/table/table.recipe.js +15 -3
- package/dist/esm/components/tabs/tabs.js +2 -2
- package/dist/esm/components/tabs/tabs.recipe.js +30 -3
- package/dist/esm/components/text/index.js +2 -0
- package/dist/esm/components/text/text.js +19 -0
- package/dist/esm/components/text/text.recipe.js +31 -0
- package/dist/esm/core/floating-stack.js +17 -0
- package/dist/esm/core/index.js +1 -1
- package/dist/esm/index.js +8 -0
- package/dist/esm/product/formdialog/formdialog.js +134 -0
- package/dist/esm/product/formdialog/formdialog.recipe.js +23 -0
- package/dist/esm/product/formdialog/index.js +2 -0
- package/dist/esm/product/headerbar/headerbar.recipe.js +9 -1
- package/dist/esm/product/importdialog/importdialog.js +97 -0
- package/dist/esm/product/importdialog/importdialog.recipe.js +89 -0
- package/dist/esm/product/importdialog/index.js +2 -0
- package/dist/esm/product/recordpage/recordpage.recipe.js +10 -1
- package/dist/esm/product/recordstable/recordstable.js +90 -8
- package/dist/esm/product/richtext/richtext.recipe.js +26 -1
- package/dist/esm/product/savedviews/index.js +2 -0
- package/dist/esm/product/savedviews/savedviews.js +171 -0
- package/dist/esm/product/savedviews/savedviews.recipe.js +100 -0
- package/dist/esm/product/taskstable/index.js +2 -0
- package/dist/esm/product/taskstable/taskstable.js +24 -0
- package/dist/esm/product/taskstable/taskstable.recipe.js +44 -0
- package/dist/esm/product/themepicker/index.js +1 -0
- package/dist/esm/product/themepicker/themepicker.js +29 -0
- package/dist/esm/product/themepicker/themepicker.recipe.js +95 -0
- package/dist/esm/tokens/palette.js +30 -13
- package/dist/esm/tokens/semantic.js +6 -0
- package/dist/formdialog.css +6 -0
- package/dist/formdialog.d.ts +7 -0
- package/dist/formdialog.layered.css +8 -0
- package/dist/headerbar.css +1 -1
- package/dist/headerbar.layered.css +1 -1
- package/dist/importdialog.css +38 -0
- package/dist/importdialog.d.ts +7 -0
- package/dist/importdialog.layered.css +40 -0
- package/dist/mentionchip.css +3 -0
- package/dist/mentionchip.d.ts +7 -0
- package/dist/mentionchip.layered.css +5 -0
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +14 -6
- package/dist/recordpage.css +13 -2
- package/dist/recordpage.layered.css +13 -2
- package/dist/richtext.css +11 -1
- package/dist/richtext.layered.css +11 -1
- package/dist/savedviews.css +78 -0
- package/dist/savedviews.d.ts +7 -0
- package/dist/savedviews.layered.css +80 -0
- package/dist/styles.css +287 -37
- package/dist/styles.layered.css +287 -37
- package/dist/table.css +15 -6
- package/dist/table.layered.css +15 -6
- package/dist/tabs.css +7 -1
- package/dist/tabs.d.ts +2 -2
- package/dist/tabs.layered.css +7 -1
- package/dist/taskstable.css +23 -0
- package/dist/taskstable.d.ts +7 -0
- package/dist/taskstable.layered.css +25 -0
- package/dist/text.css +9 -0
- package/dist/text.d.ts +7 -0
- package/dist/text.layered.css +11 -0
- package/dist/themepicker.css +27 -0
- package/dist/themepicker.d.ts +7 -0
- package/dist/themepicker.layered.css +29 -0
- package/dist/tokens.css +7 -3
- package/dist/tokens.json +14 -6
- package/dist/types/components/chip/chip.d.ts +32 -0
- package/dist/types/components/chip/chip.recipe.d.ts +31 -0
- package/dist/types/components/datatable/datatable.d.ts +39 -4
- package/dist/types/components/emptystate/emptystate.d.ts +29 -4
- package/dist/types/components/emptystate/emptystate.recipe.d.ts +2 -0
- package/dist/types/components/emptystate/index.d.ts +1 -1
- package/dist/types/components/mentionchip/index.d.ts +2 -0
- package/dist/types/components/mentionchip/mentionchip.d.ts +20 -0
- package/dist/types/components/mentionchip/mentionchip.recipe.d.ts +18 -0
- package/dist/types/components/personchip/index.d.ts +1 -0
- package/dist/types/components/personchip/personchip.d.ts +37 -0
- package/dist/types/components/table/table.d.ts +36 -0
- package/dist/types/components/tabs/tabs.d.ts +4 -2
- package/dist/types/components/tabs/tabs.recipe.d.ts +18 -3
- package/dist/types/components/text/index.d.ts +2 -0
- package/dist/types/components/text/text.d.ts +32 -0
- package/dist/types/components/text/text.recipe.d.ts +40 -0
- package/dist/types/core/floating-stack.d.ts +14 -0
- package/dist/types/core/index.d.ts +1 -1
- package/dist/types/index.d.ts +8 -0
- package/dist/types/product/formdialog/formdialog.d.ts +57 -0
- package/dist/types/product/formdialog/formdialog.recipe.d.ts +3 -0
- package/dist/types/product/formdialog/index.d.ts +2 -0
- package/dist/types/product/importdialog/importdialog.d.ts +86 -0
- package/dist/types/product/importdialog/importdialog.recipe.d.ts +3 -0
- package/dist/types/product/importdialog/index.d.ts +2 -0
- package/dist/types/product/recordstable/recordstable.d.ts +24 -2
- package/dist/types/product/savedviews/index.d.ts +2 -0
- package/dist/types/product/savedviews/savedviews.d.ts +55 -0
- package/dist/types/product/savedviews/savedviews.recipe.d.ts +3 -0
- package/dist/types/product/taskstable/index.d.ts +2 -0
- package/dist/types/product/taskstable/taskstable.d.ts +54 -0
- package/dist/types/product/taskstable/taskstable.recipe.d.ts +3 -0
- package/dist/types/product/themepicker/index.d.ts +1 -0
- package/dist/types/product/themepicker/themepicker.d.ts +21 -0
- package/dist/types/product/themepicker/themepicker.recipe.d.ts +3 -0
- package/dist/types/tokens/contract.d.ts +2 -0
- package/dist/types/tokens/palette.d.ts +11 -3
- package/dist/types/tokens/semantic.d.ts +6 -0
- 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;
|
|
@@ -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
|
-
|
|
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 = [
|
|
141
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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); }
|