@crisp-ui-kit/crisp 0.53.1 → 0.53.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 (35) hide show
  1. package/dist/cjs/components/breadcrumb/breadcrumb.js +6 -1
  2. package/dist/cjs/components/columncalc/columncalc.js +13 -4
  3. package/dist/cjs/components/composewindow/composewindow.js +5 -1
  4. package/dist/cjs/components/contextmenu/contextmenu.js +3 -1
  5. package/dist/cjs/components/filldoc/filldoc.js +6 -1
  6. package/dist/cjs/components/spinner/spinner.js +4 -1
  7. package/dist/cjs/index.js +1 -0
  8. package/dist/cjs/product/statlist/index.js +17 -0
  9. package/dist/cjs/product/statlist/statlist.js +20 -0
  10. package/dist/cjs/product/statlist/statlist.recipe.js +32 -0
  11. package/dist/esm/components/breadcrumb/breadcrumb.js +6 -1
  12. package/dist/esm/components/columncalc/columncalc.js +13 -4
  13. package/dist/esm/components/composewindow/composewindow.js +6 -2
  14. package/dist/esm/components/contextmenu/contextmenu.js +4 -2
  15. package/dist/esm/components/filldoc/filldoc.js +7 -2
  16. package/dist/esm/components/spinner/spinner.js +4 -1
  17. package/dist/esm/index.js +1 -0
  18. package/dist/esm/product/statlist/index.js +1 -0
  19. package/dist/esm/product/statlist/statlist.js +17 -0
  20. package/dist/esm/product/statlist/statlist.recipe.js +29 -0
  21. package/dist/statlist.css +8 -0
  22. package/dist/statlist.d.ts +7 -0
  23. package/dist/statlist.layered.css +10 -0
  24. package/dist/styles.css +9 -0
  25. package/dist/styles.layered.css +9 -0
  26. package/dist/types/components/breadcrumb/breadcrumb.d.ts +1 -1
  27. package/dist/types/components/columncalc/columncalc.d.ts +1 -1
  28. package/dist/types/components/composewindow/composewindow.d.ts +1 -1
  29. package/dist/types/components/contextmenu/contextmenu.d.ts +1 -1
  30. package/dist/types/components/spinner/spinner.d.ts +1 -1
  31. package/dist/types/index.d.ts +1 -0
  32. package/dist/types/product/statlist/index.d.ts +1 -0
  33. package/dist/types/product/statlist/statlist.d.ts +34 -0
  34. package/dist/types/product/statlist/statlist.recipe.d.ts +3 -0
  35. package/package.json +6 -1
@@ -36,12 +36,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.Breadcrumb = Breadcrumb;
37
37
  const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const React = __importStar(require("react"));
39
+ const core_1 = require("../../core");
39
40
  const breadcrumb_recipe_1 = require("./breadcrumb.recipe");
40
41
  /**
41
42
  * A breadcrumb trail — measured from a record-page nav. The last item is
42
43
  * marked `aria-current="page"`; earlier items render as links or buttons.
43
44
  */
44
- function Breadcrumb({ items, separator, "aria-label": ariaLabel = "Breadcrumb", className, ...rest }) {
45
+ function Breadcrumb({ items, separator, "aria-label": ariaLabelProp, className, ...rest }) {
46
+ // crisp/#471: an explicit prop always wins; missing, it reads the nearest
47
+ // CrispMessagesProvider's translation of the English default, or that
48
+ // default itself with no provider — unchanged from before this existed.
49
+ const ariaLabel = (0, core_1.useCrispMessage)(ariaLabelProp, "Breadcrumb");
45
50
  // Live-measured (app.attio.com/<ws>/home, /companies,
46
51
  // /company/<id>/overview): a plain rounded divider, not a chevron -- a
47
52
  // symmetric shape needs no RTL mirroring, unlike the ChevronRight this
@@ -40,6 +40,7 @@ exports.ColumnCalc = ColumnCalc;
40
40
  const jsx_runtime_1 = require("react/jsx-runtime");
41
41
  const lucide_react_1 = require("lucide-react");
42
42
  const React = __importStar(require("react"));
43
+ const core_1 = require("../../core");
43
44
  const menu_1 = require("../menu");
44
45
  const columncalc_recipe_1 = require("./columncalc.recipe");
45
46
  /** The four calcs offered on every column, in their order. */
@@ -117,10 +118,18 @@ function computeCalc(values, op) {
117
118
  const defaultFormat = (n, op) => op === "percentEmpty" || op === "percentFilled"
118
119
  ? `${Math.round(n)}%`
119
120
  : n.toLocaleString(undefined, { maximumFractionDigits: 2 });
120
- function ColumnCalc({ rows, value, numeric = false, defaultOp = "none", op: opProp, onOpChange, addLabel = "Add calculation", sectionLabel = "Aggregations", format = defaultFormat, align, opLabels, className, ...rest }) {
121
- // a partial override, so a consumer can translate one op without
122
- // restating all nine
123
- const labelFor = (o) => opLabels?.[o] ?? exports.CALC_LABEL[o];
121
+ function ColumnCalc({ rows, value, numeric = false, defaultOp = "none", op: opProp, onOpChange, addLabel: addLabelProp, sectionLabel: sectionLabelProp, format = defaultFormat, align, opLabels, className, ...rest }) {
122
+ // crisp/#471: an explicit prop always wins; missing, it reads the nearest
123
+ // CrispMessagesProvider's translation of the English default, or that
124
+ // default itself with no provider — unchanged from before this existed.
125
+ const addLabel = (0, core_1.useCrispMessage)(addLabelProp, "Add calculation");
126
+ const sectionLabel = (0, core_1.useCrispMessage)(sectionLabelProp, "Aggregations");
127
+ const crispMessages = (0, core_1.useCrispMessages)();
128
+ // opLabels (a per-op partial override) wins, then the provider's own
129
+ // translation of CALC_LABEL's English default, then that default itself —
130
+ // a consumer can translate one op via opLabels without restating all
131
+ // nine, AND get the rest from a provider with no per-instance prop at all.
132
+ const labelFor = (o) => opLabels?.[o] ?? crispMessages[exports.CALC_LABEL[o]] ?? exports.CALC_LABEL[o];
124
133
  const [internal, setInternal] = React.useState(defaultOp);
125
134
  const op = opProp ?? internal;
126
135
  const setOp = (next) => {
@@ -93,7 +93,11 @@ function useComposeWindowInstanceId() {
93
93
  * measured screenshot. `trapFocus` follows the same call — a picture-in-
94
94
  * picture window, not a modal, so Tab is not caught inside it.
95
95
  */
96
- function ComposeWindow({ open, onOpenChange, breadcrumb, minimized: minimizedProp, defaultMinimized, onMinimizedChange, pinned: pinnedProp, defaultPinned, onPinnedChange, width = 968, "aria-label": ariaLabel, minimizeLabel = "Minimize", pinLabel = "Pin to screen", closeLabel = "Close", children, className, style, "aria-describedby": describedBy, ...rest }) {
96
+ function ComposeWindow({ open, onOpenChange, breadcrumb, minimized: minimizedProp, defaultMinimized, onMinimizedChange, pinned: pinnedProp, defaultPinned, onPinnedChange, width = 968, "aria-label": ariaLabel, minimizeLabel: minimizeLabelProp, pinLabel: pinLabelProp, closeLabel: closeLabelProp, children, className, style, "aria-describedby": describedBy, ...rest }) {
97
+ // crisp/#471: same resolution order as Breadcrumb's own.
98
+ const minimizeLabel = (0, core_1.useCrispMessage)(minimizeLabelProp, "Minimize");
99
+ const pinLabel = (0, core_1.useCrispMessage)(pinLabelProp, "Pin to screen");
100
+ const closeLabel = (0, core_1.useCrispMessage)(closeLabelProp, "Close");
97
101
  const id = useComposeWindowInstanceId();
98
102
  const [internalMinimized, setInternalMinimized] = React.useState(defaultMinimized ?? false);
99
103
  const minimized = minimizedProp ?? internalMinimized;
@@ -45,7 +45,9 @@ const contextmenu_recipe_1 = require("./contextmenu.recipe");
45
45
  * Wrap the target; right-clicking it opens the menu (flipping at the viewport
46
46
  * edges), closing on select, outside pointer-down, and Escape.
47
47
  */
48
- function ContextMenu({ children, items, menuLabel = "Context menu", className, asChild = false, }) {
48
+ function ContextMenu({ children, items, menuLabel: menuLabelProp, className, asChild = false, }) {
49
+ // crisp/#471: same resolution order as Breadcrumb's own.
50
+ const menuLabel = (0, core_1.useCrispMessage)(menuLabelProp, "Context menu");
49
51
  const rootRef = React.useRef(null);
50
52
  const menuRef = React.useRef(null);
51
53
  const returnFocusTo = React.useRef(null);
@@ -72,7 +72,12 @@ const emptyTable = () => ({
72
72
  * page covered in handles and grips stops looking like a page, which is the
73
73
  * whole thing this is for.
74
74
  */
75
- exports.FillDoc = React.forwardRef(function FillDoc({ blocks, onBlocksChange, mode = "edit", page = "a4", label, addTextLabel = "Text", addHeadingLabel = "Heading", addTableLabel = "Table", removeLabel = "Remove", className, style, ...rest }, forwardedRef) {
75
+ exports.FillDoc = React.forwardRef(function FillDoc({ blocks, onBlocksChange, mode = "edit", page = "a4", label, addTextLabel: addTextLabelProp, addHeadingLabel: addHeadingLabelProp, addTableLabel: addTableLabelProp, removeLabel: removeLabelProp, className, style, ...rest }, forwardedRef) {
76
+ // crisp/#471: same resolution order as Breadcrumb's own.
77
+ const addTextLabel = (0, core_1.useCrispMessage)(addTextLabelProp, "Text");
78
+ const addHeadingLabel = (0, core_1.useCrispMessage)(addHeadingLabelProp, "Heading");
79
+ const addTableLabel = (0, core_1.useCrispMessage)(addTableLabelProp, "Table");
80
+ const removeLabel = (0, core_1.useCrispMessage)(removeLabelProp, "Remove");
76
81
  const editable = mode === "edit";
77
82
  const replace = (i, block) => {
78
83
  const next = blocks.slice();
@@ -2,9 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Spinner = Spinner;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const core_1 = require("../../core");
5
6
  const spinner_recipe_1 = require("./spinner.recipe");
6
7
  /** A spinning ring indicator. Reports `role="status"` for assistive tech. */
7
- function Spinner({ size = 16, "aria-label": ariaLabel = "Loading", className, ...rest }) {
8
+ function Spinner({ size = 16, "aria-label": ariaLabelProp, className, ...rest }) {
9
+ // crisp/#471: same resolution order as Breadcrumb's own.
10
+ const ariaLabel = (0, core_1.useCrispMessage)(ariaLabelProp, "Loading");
8
11
  return ((0, jsx_runtime_1.jsx)("span", { role: "status", "aria-label": ariaLabel, className: (0, spinner_recipe_1.spinnerRecipe)({ className }), ...rest, style: {
9
12
  width: size,
10
13
  height: size,
package/dist/cjs/index.js CHANGED
@@ -94,6 +94,7 @@ __exportStar(require("./product/richtext"), exports);
94
94
  __exportStar(require("./product/settingssection"), exports);
95
95
  __exportStar(require("./product/settingsshell"), exports);
96
96
  __exportStar(require("./product/sidebar"), exports);
97
+ __exportStar(require("./product/statlist"), exports);
97
98
  __exportStar(require("./product/tasklist"), exports);
98
99
  __exportStar(require("./product/trialbanner"), exports);
99
100
  __exportStar(require("./product/viewtoggle"), exports);
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./statlist"), exports);
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StatList = StatList;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const badge_1 = require("../../components/badge/badge");
6
+ const statlist_recipe_1 = require("./statlist.recipe");
7
+ /**
8
+ * A list of label + trailing-count rows — a dashboard's own findings
9
+ * summary ("Open risks with no owner: 3") or an audit-readiness check list,
10
+ * each row optionally a link. Not measured against one specific Attio
11
+ * widget (see the recipe's own doc comment for why); composed from
12
+ * already-measured crisp values throughout.
13
+ */
14
+ function StatList({ items, emptyLabel = "Nothing to show", className, ...rest }) {
15
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, statlist_recipe_1.statlistRecipe)({ className }), ...rest, children: items.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-statlist-empty", children: emptyLabel })) : (items.map((item) => {
16
+ const content = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-statlist-label", children: item.label }), (0, jsx_runtime_1.jsx)(badge_1.Badge, { tone: item.count === 0 ? "neutral" : (item.tone ?? "neutral"), children: item.count })] }));
17
+ return item.href ? ((0, jsx_runtime_1.jsx)("a", { href: item.href, onClick: item.onClick, className: "crisp-statlist-row", children: content }, item.id)) : ((0, jsx_runtime_1.jsx)("div", { className: "crisp-statlist-row", children: content }, item.id));
18
+ })) }));
19
+ }
20
+ StatList.displayName = "StatList";
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.statlistRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // A list of label + trailing-count rows, each optionally a link — the
7
+ // "N findings of this kind" shape a dashboard's own status summary and an
8
+ // audit-readiness check list both need. No direct Attio widget measures
9
+ // this exact row (Attio's own Reports/Dashboards surface in this workspace
10
+ // has bar/line/number report tiles, not a findings list), so this reuses
11
+ // already-measured crisp values throughout rather than a fresh guess:
12
+ // TaskList's own row-divider structure (border-top, no border on the first
13
+ // row), the type scale's own `md`/`sm` sizes, and Badge's own tone colours
14
+ // for the trailing count — no new pixel or colour value is introduced here.
15
+ const config = (0, core_1.defineRecipe)({
16
+ name: "statlist",
17
+ base: `display: flex; flex-direction: column; box-sizing: border-box; font-family: ${tokens_1.contract.font.sans};
18
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
19
+
20
+ & .crisp-statlist-row { display: flex; align-items: center; justify-content: space-between; gap: ${tokens_1.contract.space["3"]}; padding: ${tokens_1.contract.space["2.5"]} 0; border-top: 1px solid ${tokens_1.contract.borderSubtle}; font-size: ${tokens_1.contract.text.md}; color: inherit; text-decoration: none; }
21
+ & .crisp-statlist-row:first-child { border-top: none; }
22
+ & a.crisp-statlist-row:hover .crisp-statlist-label { color: ${tokens_1.contract.fgBrandSoft}; }
23
+
24
+ & .crisp-statlist-label { color: ${tokens_1.contract.fgMuted}; }
25
+
26
+ & .crisp-statlist-empty { padding: ${tokens_1.contract.space["4"]} 0; text-align: center; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }`,
27
+ variants: {},
28
+ defaultVariants: {},
29
+ compoundVariants: [],
30
+ });
31
+ exports.statlistRecipe = (0, core_1.resolveRecipe)(config);
32
+ exports.default = config;
@@ -1,11 +1,16 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
+ import { useCrispMessage } from "../../core/index.js";
3
4
  import { breadcrumbRecipe } from "./breadcrumb.recipe.js";
4
5
  /**
5
6
  * A breadcrumb trail — measured from a record-page nav. The last item is
6
7
  * marked `aria-current="page"`; earlier items render as links or buttons.
7
8
  */
8
- export function Breadcrumb({ items, separator, "aria-label": ariaLabel = "Breadcrumb", className, ...rest }) {
9
+ export function Breadcrumb({ items, separator, "aria-label": ariaLabelProp, className, ...rest }) {
10
+ // crisp/#471: an explicit prop always wins; missing, it reads the nearest
11
+ // CrispMessagesProvider's translation of the English default, or that
12
+ // default itself with no provider — unchanged from before this existed.
13
+ const ariaLabel = useCrispMessage(ariaLabelProp, "Breadcrumb");
9
14
  // Live-measured (app.attio.com/<ws>/home, /companies,
10
15
  // /company/<id>/overview): a plain rounded divider, not a chevron -- a
11
16
  // symmetric shape needs no RTL mirroring, unlike the ChevronRight this
@@ -1,6 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ChevronDown, Plus } from "lucide-react";
3
3
  import * as React from "react";
4
+ import { useCrispMessage, useCrispMessages } from "../../core/index.js";
4
5
  import { Menu, MenuItem } from "../menu/index.js";
5
6
  import { columncalcRecipe } from "./columncalc.recipe.js";
6
7
  /** The four calcs offered on every column, in their order. */
@@ -78,10 +79,18 @@ export function computeCalc(values, op) {
78
79
  const defaultFormat = (n, op) => op === "percentEmpty" || op === "percentFilled"
79
80
  ? `${Math.round(n)}%`
80
81
  : n.toLocaleString(undefined, { maximumFractionDigits: 2 });
81
- export function ColumnCalc({ rows, value, numeric = false, defaultOp = "none", op: opProp, onOpChange, addLabel = "Add calculation", sectionLabel = "Aggregations", format = defaultFormat, align, opLabels, className, ...rest }) {
82
- // a partial override, so a consumer can translate one op without
83
- // restating all nine
84
- const labelFor = (o) => opLabels?.[o] ?? CALC_LABEL[o];
82
+ export function ColumnCalc({ rows, value, numeric = false, defaultOp = "none", op: opProp, onOpChange, addLabel: addLabelProp, sectionLabel: sectionLabelProp, format = defaultFormat, align, opLabels, className, ...rest }) {
83
+ // crisp/#471: an explicit prop always wins; missing, it reads the nearest
84
+ // CrispMessagesProvider's translation of the English default, or that
85
+ // default itself with no provider — unchanged from before this existed.
86
+ const addLabel = useCrispMessage(addLabelProp, "Add calculation");
87
+ const sectionLabel = useCrispMessage(sectionLabelProp, "Aggregations");
88
+ const crispMessages = useCrispMessages();
89
+ // opLabels (a per-op partial override) wins, then the provider's own
90
+ // translation of CALC_LABEL's English default, then that default itself —
91
+ // a consumer can translate one op via opLabels without restating all
92
+ // nine, AND get the rest from a provider with no per-instance prop at all.
93
+ const labelFor = (o) => opLabels?.[o] ?? crispMessages[CALC_LABEL[o]] ?? CALC_LABEL[o];
85
94
  const [internal, setInternal] = React.useState(defaultOp);
86
95
  const op = opProp ?? internal;
87
96
  const setOp = (next) => {
@@ -3,7 +3,7 @@ import * as dialog from "@zag-js/dialog";
3
3
  import { normalizeProps, Portal, useMachine } from "@zag-js/react";
4
4
  import { Minus, Pin, X } from "lucide-react";
5
5
  import * as React from "react";
6
- import { cx, withDescribedBy } from "../../core/index.js";
6
+ import { cx, useCrispMessage, withDescribedBy } from "../../core/index.js";
7
7
  import { composewindowRecipe } from "./composewindow.recipe.js";
8
8
  // A globally unique id per mounted instance — see command.tsx/dialog.tsx for
9
9
  // the rationale (the machine defers its dismissal listeners a frame, so a
@@ -56,7 +56,11 @@ function useComposeWindowInstanceId() {
56
56
  * measured screenshot. `trapFocus` follows the same call — a picture-in-
57
57
  * picture window, not a modal, so Tab is not caught inside it.
58
58
  */
59
- export function ComposeWindow({ open, onOpenChange, breadcrumb, minimized: minimizedProp, defaultMinimized, onMinimizedChange, pinned: pinnedProp, defaultPinned, onPinnedChange, width = 968, "aria-label": ariaLabel, minimizeLabel = "Minimize", pinLabel = "Pin to screen", closeLabel = "Close", children, className, style, "aria-describedby": describedBy, ...rest }) {
59
+ export function ComposeWindow({ open, onOpenChange, breadcrumb, minimized: minimizedProp, defaultMinimized, onMinimizedChange, pinned: pinnedProp, defaultPinned, onPinnedChange, width = 968, "aria-label": ariaLabel, minimizeLabel: minimizeLabelProp, pinLabel: pinLabelProp, closeLabel: closeLabelProp, children, className, style, "aria-describedby": describedBy, ...rest }) {
60
+ // crisp/#471: same resolution order as Breadcrumb's own.
61
+ const minimizeLabel = useCrispMessage(minimizeLabelProp, "Minimize");
62
+ const pinLabel = useCrispMessage(pinLabelProp, "Pin to screen");
63
+ const closeLabel = useCrispMessage(closeLabelProp, "Close");
60
64
  const id = useComposeWindowInstanceId();
61
65
  const [internalMinimized, setInternalMinimized] = React.useState(defaultMinimized ?? false);
62
66
  const minimized = minimizedProp ?? internalMinimized;
@@ -1,7 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Slot } from "@radix-ui/react-slot";
3
3
  import * as React from "react";
4
- import { FloatingPortal, isInsideLaterFloatingLayer, registerFloatingLayer, } from "../../core/index.js";
4
+ import { FloatingPortal, isInsideLaterFloatingLayer, registerFloatingLayer, useCrispMessage, } from "../../core/index.js";
5
5
  import { useMenuKeyboard } from "../menu/index.js";
6
6
  import { contextmenuRecipe } from "./contextmenu.recipe.js";
7
7
  /**
@@ -9,7 +9,9 @@ import { contextmenuRecipe } from "./contextmenu.recipe.js";
9
9
  * Wrap the target; right-clicking it opens the menu (flipping at the viewport
10
10
  * edges), closing on select, outside pointer-down, and Escape.
11
11
  */
12
- export function ContextMenu({ children, items, menuLabel = "Context menu", className, asChild = false, }) {
12
+ export function ContextMenu({ children, items, menuLabel: menuLabelProp, className, asChild = false, }) {
13
+ // crisp/#471: same resolution order as Breadcrumb's own.
14
+ const menuLabel = useCrispMessage(menuLabelProp, "Context menu");
13
15
  const rootRef = React.useRef(null);
14
16
  const menuRef = React.useRef(null);
15
17
  const returnFocusTo = React.useRef(null);
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { cx } from "../../core/index.js";
4
+ import { cx, useCrispMessage } from "../../core/index.js";
5
5
  import { FillSheet } from "../fillsheet/index.js";
6
6
  import { filldocRecipe } from "./filldoc.recipe.js";
7
7
  /** Size a textarea to its content; see FillSheet's autoGrow for the reset. */
@@ -36,7 +36,12 @@ const emptyTable = () => ({
36
36
  * page covered in handles and grips stops looking like a page, which is the
37
37
  * whole thing this is for.
38
38
  */
39
- export const FillDoc = React.forwardRef(function FillDoc({ blocks, onBlocksChange, mode = "edit", page = "a4", label, addTextLabel = "Text", addHeadingLabel = "Heading", addTableLabel = "Table", removeLabel = "Remove", className, style, ...rest }, forwardedRef) {
39
+ export const FillDoc = React.forwardRef(function FillDoc({ blocks, onBlocksChange, mode = "edit", page = "a4", label, addTextLabel: addTextLabelProp, addHeadingLabel: addHeadingLabelProp, addTableLabel: addTableLabelProp, removeLabel: removeLabelProp, className, style, ...rest }, forwardedRef) {
40
+ // crisp/#471: same resolution order as Breadcrumb's own.
41
+ const addTextLabel = useCrispMessage(addTextLabelProp, "Text");
42
+ const addHeadingLabel = useCrispMessage(addHeadingLabelProp, "Heading");
43
+ const addTableLabel = useCrispMessage(addTableLabelProp, "Table");
44
+ const removeLabel = useCrispMessage(removeLabelProp, "Remove");
40
45
  const editable = mode === "edit";
41
46
  const replace = (i, block) => {
42
47
  const next = blocks.slice();
@@ -1,7 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCrispMessage } from "../../core/index.js";
2
3
  import { spinnerRecipe } from "./spinner.recipe.js";
3
4
  /** A spinning ring indicator. Reports `role="status"` for assistive tech. */
4
- export function Spinner({ size = 16, "aria-label": ariaLabel = "Loading", className, ...rest }) {
5
+ export function Spinner({ size = 16, "aria-label": ariaLabelProp, className, ...rest }) {
6
+ // crisp/#471: same resolution order as Breadcrumb's own.
7
+ const ariaLabel = useCrispMessage(ariaLabelProp, "Loading");
5
8
  return (_jsx("span", { role: "status", "aria-label": ariaLabel, className: spinnerRecipe({ className }), ...rest, style: {
6
9
  width: size,
7
10
  height: size,
package/dist/esm/index.js CHANGED
@@ -78,6 +78,7 @@ export * from "./product/richtext/index.js";
78
78
  export * from "./product/settingssection/index.js";
79
79
  export * from "./product/settingsshell/index.js";
80
80
  export * from "./product/sidebar/index.js";
81
+ export * from "./product/statlist/index.js";
81
82
  export * from "./product/tasklist/index.js";
82
83
  export * from "./product/trialbanner/index.js";
83
84
  export * from "./product/viewtoggle/index.js";
@@ -0,0 +1 @@
1
+ export * from "./statlist.js";
@@ -0,0 +1,17 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Badge } from "../../components/badge/badge.js";
3
+ import { statlistRecipe } from "./statlist.recipe.js";
4
+ /**
5
+ * A list of label + trailing-count rows — a dashboard's own findings
6
+ * summary ("Open risks with no owner: 3") or an audit-readiness check list,
7
+ * each row optionally a link. Not measured against one specific Attio
8
+ * widget (see the recipe's own doc comment for why); composed from
9
+ * already-measured crisp values throughout.
10
+ */
11
+ export function StatList({ items, emptyLabel = "Nothing to show", className, ...rest }) {
12
+ return (_jsx("div", { className: statlistRecipe({ className }), ...rest, children: items.length === 0 ? (_jsx("div", { className: "crisp-statlist-empty", children: emptyLabel })) : (items.map((item) => {
13
+ const content = (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-statlist-label", children: item.label }), _jsx(Badge, { tone: item.count === 0 ? "neutral" : (item.tone ?? "neutral"), children: item.count })] }));
14
+ return item.href ? (_jsx("a", { href: item.href, onClick: item.onClick, className: "crisp-statlist-row", children: content }, item.id)) : (_jsx("div", { className: "crisp-statlist-row", children: content }, item.id));
15
+ })) }));
16
+ }
17
+ StatList.displayName = "StatList";
@@ -0,0 +1,29 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // A list of label + trailing-count rows, each optionally a link — the
4
+ // "N findings of this kind" shape a dashboard's own status summary and an
5
+ // audit-readiness check list both need. No direct Attio widget measures
6
+ // this exact row (Attio's own Reports/Dashboards surface in this workspace
7
+ // has bar/line/number report tiles, not a findings list), so this reuses
8
+ // already-measured crisp values throughout rather than a fresh guess:
9
+ // TaskList's own row-divider structure (border-top, no border on the first
10
+ // row), the type scale's own `md`/`sm` sizes, and Badge's own tone colours
11
+ // for the trailing count — no new pixel or colour value is introduced here.
12
+ const config = defineRecipe({
13
+ name: "statlist",
14
+ base: `display: flex; flex-direction: column; box-sizing: border-box; font-family: ${c.font.sans};
15
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
16
+
17
+ & .crisp-statlist-row { display: flex; align-items: center; justify-content: space-between; gap: ${c.space["3"]}; padding: ${c.space["2.5"]} 0; border-top: 1px solid ${c.borderSubtle}; font-size: ${c.text.md}; color: inherit; text-decoration: none; }
18
+ & .crisp-statlist-row:first-child { border-top: none; }
19
+ & a.crisp-statlist-row:hover .crisp-statlist-label { color: ${c.fgBrandSoft}; }
20
+
21
+ & .crisp-statlist-label { color: ${c.fgMuted}; }
22
+
23
+ & .crisp-statlist-empty { padding: ${c.space["4"]} 0; text-align: center; font-size: ${c.text.md}; color: ${c.fgMuted}; }`,
24
+ variants: {},
25
+ defaultVariants: {},
26
+ compoundVariants: [],
27
+ });
28
+ export const statlistRecipe = resolveRecipe(config);
29
+ export default config;
@@ -0,0 +1,8 @@
1
+ .crisp-statlist {
2
+ display: flex; flex-direction: column; box-sizing: border-box; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
3
+ & .crisp-statlist-row { display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-3); padding: var(--crisp-space-2-5) 0; border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-md); color: inherit; text-decoration: none; }
4
+ & .crisp-statlist-row:first-child { border-top: none; }
5
+ & a.crisp-statlist-row:hover .crisp-statlist-label { color: var(--crisp-fg-brand-soft); }
6
+ & .crisp-statlist-label { color: var(--crisp-fg-muted); }
7
+ & .crisp-statlist-empty { padding: var(--crisp-space-4) 0; text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
8
+ }
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface StatlistVariants {
4
+
5
+ }
6
+
7
+ export declare const statlistRecipe: RecipeFn<{ }>;
@@ -0,0 +1,10 @@
1
+ @layer crisp-components {
2
+ .crisp-statlist {
3
+ display: flex; flex-direction: column; box-sizing: border-box; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
4
+ & .crisp-statlist-row { display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-3); padding: var(--crisp-space-2-5) 0; border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-md); color: inherit; text-decoration: none; }
5
+ & .crisp-statlist-row:first-child { border-top: none; }
6
+ & a.crisp-statlist-row:hover .crisp-statlist-label { color: var(--crisp-fg-brand-soft); }
7
+ & .crisp-statlist-label { color: var(--crisp-fg-muted); }
8
+ & .crisp-statlist-empty { padding: var(--crisp-space-4) 0; text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
9
+ }
10
+ }
package/dist/styles.css CHANGED
@@ -2729,6 +2729,15 @@
2729
2729
  }
2730
2730
  }
2731
2731
 
2732
+ .crisp-statlist {
2733
+ display: flex; flex-direction: column; box-sizing: border-box; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
2734
+ & .crisp-statlist-row { display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-3); padding: var(--crisp-space-2-5) 0; border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-md); color: inherit; text-decoration: none; }
2735
+ & .crisp-statlist-row:first-child { border-top: none; }
2736
+ & a.crisp-statlist-row:hover .crisp-statlist-label { color: var(--crisp-fg-brand-soft); }
2737
+ & .crisp-statlist-label { color: var(--crisp-fg-muted); }
2738
+ & .crisp-statlist-empty { padding: var(--crisp-space-4) 0; text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
2739
+ }
2740
+
2732
2741
  .crisp-tasklist {
2733
2742
  display: flex; flex-direction: column; box-sizing: border-box; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
2734
2743
  & .crisp-tasklist-group { display: flex; align-items: center; gap: var(--crisp-space-1-5); height: 36px; padding: 0 var(--crisp-space-2); font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
@@ -2730,6 +2730,15 @@
2730
2730
  }
2731
2731
  }
2732
2732
 
2733
+ .crisp-statlist {
2734
+ display: flex; flex-direction: column; box-sizing: border-box; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
2735
+ & .crisp-statlist-row { display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-3); padding: var(--crisp-space-2-5) 0; border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-md); color: inherit; text-decoration: none; }
2736
+ & .crisp-statlist-row:first-child { border-top: none; }
2737
+ & a.crisp-statlist-row:hover .crisp-statlist-label { color: var(--crisp-fg-brand-soft); }
2738
+ & .crisp-statlist-label { color: var(--crisp-fg-muted); }
2739
+ & .crisp-statlist-empty { padding: var(--crisp-space-4) 0; text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
2740
+ }
2741
+
2733
2742
  .crisp-tasklist {
2734
2743
  display: flex; flex-direction: column; box-sizing: border-box; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
2735
2744
  & .crisp-tasklist-group { display: flex; align-items: center; gap: var(--crisp-space-1-5); height: 36px; padding: 0 var(--crisp-space-2); font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
@@ -16,7 +16,7 @@ export type BreadcrumbProps = React.ComponentPropsWithoutRef<"nav"> & {
16
16
  * A breadcrumb trail — measured from a record-page nav. The last item is
17
17
  * marked `aria-current="page"`; earlier items render as links or buttons.
18
18
  */
19
- export declare function Breadcrumb({ items, separator, "aria-label": ariaLabel, className, ...rest }: BreadcrumbProps): React.JSX.Element;
19
+ export declare function Breadcrumb({ items, separator, "aria-label": ariaLabelProp, className, ...rest }: BreadcrumbProps): React.JSX.Element;
20
20
  export declare namespace Breadcrumb {
21
21
  var displayName: string;
22
22
  }
@@ -50,4 +50,4 @@ export type ColumnCalcProps<T> = Omit<React.ComponentPropsWithoutRef<"span">, "v
50
50
  align?: "left" | "right";
51
51
  className?: string;
52
52
  };
53
- export declare function ColumnCalc<T>({ rows, value, numeric, defaultOp, op: opProp, onOpChange, addLabel, sectionLabel, format, align, opLabels, className, ...rest }: ColumnCalcProps<T>): React.JSX.Element;
53
+ export declare function ColumnCalc<T>({ rows, value, numeric, defaultOp, op: opProp, onOpChange, addLabel: addLabelProp, sectionLabel: sectionLabelProp, format, align, opLabels, className, ...rest }: ColumnCalcProps<T>): React.JSX.Element;
@@ -70,7 +70,7 @@ export type ComposeWindowProps = Omit<React.ComponentPropsWithoutRef<"div">, "ch
70
70
  * measured screenshot. `trapFocus` follows the same call — a picture-in-
71
71
  * picture window, not a modal, so Tab is not caught inside it.
72
72
  */
73
- export declare function ComposeWindow({ open, onOpenChange, breadcrumb, minimized: minimizedProp, defaultMinimized, onMinimizedChange, pinned: pinnedProp, defaultPinned, onPinnedChange, width, "aria-label": ariaLabel, minimizeLabel, pinLabel, closeLabel, children, className, style, "aria-describedby": describedBy, ...rest }: ComposeWindowProps): React.JSX.Element | null;
73
+ export declare function ComposeWindow({ open, onOpenChange, breadcrumb, minimized: minimizedProp, defaultMinimized, onMinimizedChange, pinned: pinnedProp, defaultPinned, onPinnedChange, width, "aria-label": ariaLabel, minimizeLabel: minimizeLabelProp, pinLabel: pinLabelProp, closeLabel: closeLabelProp, children, className, style, "aria-describedby": describedBy, ...rest }: ComposeWindowProps): React.JSX.Element | null;
74
74
  export declare namespace ComposeWindow {
75
75
  var displayName: string;
76
76
  }
@@ -33,7 +33,7 @@ export type ContextMenuProps = {
33
33
  * Wrap the target; right-clicking it opens the menu (flipping at the viewport
34
34
  * edges), closing on select, outside pointer-down, and Escape.
35
35
  */
36
- export declare function ContextMenu({ children, items, menuLabel, className, asChild, }: ContextMenuProps): React.JSX.Element;
36
+ export declare function ContextMenu({ children, items, menuLabel: menuLabelProp, className, asChild, }: ContextMenuProps): React.JSX.Element;
37
37
  export declare namespace ContextMenu {
38
38
  var displayName: string;
39
39
  }
@@ -5,7 +5,7 @@ export type SpinnerProps = React.ComponentPropsWithoutRef<"span"> & {
5
5
  className?: string;
6
6
  };
7
7
  /** A spinning ring indicator. Reports `role="status"` for assistive tech. */
8
- export declare function Spinner({ size, "aria-label": ariaLabel, className, ...rest }: SpinnerProps): import("react").JSX.Element;
8
+ export declare function Spinner({ size, "aria-label": ariaLabelProp, className, ...rest }: SpinnerProps): import("react").JSX.Element;
9
9
  export declare namespace Spinner {
10
10
  var displayName: string;
11
11
  }
@@ -70,6 +70,7 @@ export * from "./product/richtext";
70
70
  export * from "./product/settingssection";
71
71
  export * from "./product/settingsshell";
72
72
  export * from "./product/sidebar";
73
+ export * from "./product/statlist";
73
74
  export * from "./product/tasklist";
74
75
  export * from "./product/trialbanner";
75
76
  export * from "./product/viewtoggle";
@@ -0,0 +1 @@
1
+ export * from "./statlist";
@@ -0,0 +1,34 @@
1
+ import type * as React from "react";
2
+ export type StatListItem = {
3
+ id: string;
4
+ label: React.ReactNode;
5
+ /** The trailing count. Rendered as a Badge; 0 always reads "neutral",
6
+ * regardless of `tone` — a zero is never itself a finding. */
7
+ count: number;
8
+ /** Badge tone once `count > 0`. Default "neutral". */
9
+ tone?: "neutral" | "success" | "warning" | "danger" | "brand";
10
+ /** Real anchor href — gives right-click / middle-click / ctrl-click their
11
+ * ordinary browser behaviour. Omit for a non-navigable row. */
12
+ href?: string;
13
+ /** Called on a plain left click; pair with `href` and `event.preventDefault()`
14
+ * for client-side routing (e.g. react-router), the same division of
15
+ * labour its own `Link` uses internally. */
16
+ onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
17
+ };
18
+ export type StatListProps = Omit<React.ComponentPropsWithoutRef<"div">, "children"> & {
19
+ items: StatListItem[];
20
+ /** Shown when `items` is empty. */
21
+ emptyLabel?: React.ReactNode;
22
+ className?: string;
23
+ };
24
+ /**
25
+ * A list of label + trailing-count rows — a dashboard's own findings
26
+ * summary ("Open risks with no owner: 3") or an audit-readiness check list,
27
+ * each row optionally a link. Not measured against one specific Attio
28
+ * widget (see the recipe's own doc comment for why); composed from
29
+ * already-measured crisp values throughout.
30
+ */
31
+ export declare function StatList({ items, emptyLabel, className, ...rest }: StatListProps): React.JSX.Element;
32
+ export declare namespace StatList {
33
+ var displayName: string;
34
+ }
@@ -0,0 +1,3 @@
1
+ declare const config: import("../..").RecipeConfig<{}>;
2
+ export declare const statlistRecipe: import("../..").RecipeFn<{}>;
3
+ export default config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.53.1",
3
+ "version": "0.53.3",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -388,6 +388,11 @@
388
388
  "import": "./dist/esm/product/sidebar/index.js",
389
389
  "require": "./dist/cjs/product/sidebar/index.js"
390
390
  },
391
+ "./statlist": {
392
+ "types": "./dist/types/product/statlist/index.d.ts",
393
+ "import": "./dist/esm/product/statlist/index.js",
394
+ "require": "./dist/cjs/product/statlist/index.js"
395
+ },
391
396
  "./tasklist": {
392
397
  "types": "./dist/types/product/tasklist/index.d.ts",
393
398
  "import": "./dist/esm/product/tasklist/index.js",