@crisp-ui-kit/crisp 0.52.6 → 0.53.1
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/breadcrumb.css +2 -1
- package/dist/breadcrumb.layered.css +2 -1
- package/dist/button.css +2 -2
- package/dist/button.layered.css +2 -2
- package/dist/cjs/components/breadcrumb/breadcrumb.js +5 -2
- package/dist/cjs/components/breadcrumb/breadcrumb.recipe.js +5 -2
- package/dist/cjs/components/button/button.recipe.js +10 -2
- package/dist/cjs/components/datatable/datatable.js +55 -10
- package/dist/cjs/components/datatable/datatable.recipe.js +2 -0
- package/dist/cjs/components/datepickerfield/datepickerfield.js +5 -1
- package/dist/cjs/components/dialog/dialog.js +2 -1
- package/dist/cjs/components/drawer/drawer.js +2 -1
- package/dist/cjs/components/multiselect/multiselect.js +5 -1
- package/dist/cjs/components/quantitypicker/quantitypicker.js +4 -1
- package/dist/cjs/components/select/select.js +7 -1
- package/dist/cjs/components/statcard/statcard.js +3 -2
- package/dist/cjs/components/statcard/statcard.recipe.js +10 -0
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +40 -7
- package/dist/cjs/components/toast/toast.js +3 -1
- package/dist/cjs/core/i18n.js +75 -0
- package/dist/cjs/core/index.js +5 -1
- package/dist/datatable.css +2 -0
- package/dist/datatable.layered.css +2 -0
- package/dist/esm/components/breadcrumb/breadcrumb.js +5 -2
- package/dist/esm/components/breadcrumb/breadcrumb.recipe.js +5 -2
- package/dist/esm/components/button/button.recipe.js +10 -2
- package/dist/esm/components/datatable/datatable.js +56 -11
- package/dist/esm/components/datatable/datatable.recipe.js +2 -0
- package/dist/esm/components/datepickerfield/datepickerfield.js +5 -1
- package/dist/esm/components/dialog/dialog.js +3 -2
- package/dist/esm/components/drawer/drawer.js +3 -2
- package/dist/esm/components/multiselect/multiselect.js +6 -2
- package/dist/esm/components/quantitypicker/quantitypicker.js +5 -2
- package/dist/esm/components/select/select.js +8 -2
- package/dist/esm/components/statcard/statcard.js +3 -2
- package/dist/esm/components/statcard/statcard.recipe.js +10 -0
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +41 -8
- package/dist/esm/components/toast/toast.js +4 -2
- package/dist/esm/core/i18n.js +37 -0
- package/dist/esm/core/index.js +1 -0
- package/dist/statcard.css +8 -0
- package/dist/statcard.layered.css +8 -0
- package/dist/styles.css +14 -3
- package/dist/styles.layered.css +14 -3
- package/dist/types/components/breadcrumb/breadcrumb.d.ts +1 -1
- package/dist/types/components/button/button.recipe.d.ts +2 -2
- package/dist/types/components/datatable/datatable.d.ts +31 -1
- package/dist/types/components/datepickerfield/datepickerfield.d.ts +1 -1
- package/dist/types/components/dialog/dialog.d.ts +1 -1
- package/dist/types/components/drawer/drawer.d.ts +1 -1
- package/dist/types/components/multiselect/multiselect.d.ts +1 -1
- package/dist/types/components/quantitypicker/quantitypicker.d.ts +1 -1
- package/dist/types/components/select/select.d.ts +1 -1
- package/dist/types/components/statcard/statcard.d.ts +5 -1
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +5 -1
- package/dist/types/components/toast/toast.d.ts +1 -1
- package/dist/types/core/i18n.d.ts +58 -0
- package/dist/types/core/index.d.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CrispMessagesProvider = CrispMessagesProvider;
|
|
37
|
+
exports.useCrispMessage = useCrispMessage;
|
|
38
|
+
exports.useCrispMessages = useCrispMessages;
|
|
39
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
40
|
+
const React = __importStar(require("react"));
|
|
41
|
+
const CrispMessagesContext = React.createContext({});
|
|
42
|
+
function CrispMessagesProvider({ messages, children, }) {
|
|
43
|
+
return ((0, jsx_runtime_1.jsx)(CrispMessagesContext.Provider, { value: messages, children: children }));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolves one translatable string for a component: `explicit` is whatever
|
|
47
|
+
* the caller passed for that prop (`undefined` when they didn't), and
|
|
48
|
+
* `englishDefault` is the literal that shipped as that prop's default value
|
|
49
|
+
* before crisp/#471 — still what renders with no provider in the tree.
|
|
50
|
+
*
|
|
51
|
+
* A plain function, not a second context read inside a loop or a render
|
|
52
|
+
* helper — components call this once per string, the same shape
|
|
53
|
+
* `useLabelAssociation`/`useFormReset` and this codebase's other small
|
|
54
|
+
* `core/` hooks already take.
|
|
55
|
+
*/
|
|
56
|
+
function useCrispMessage(explicit, englishDefault) {
|
|
57
|
+
const messages = React.useContext(CrispMessagesContext);
|
|
58
|
+
if (explicit !== undefined)
|
|
59
|
+
return explicit;
|
|
60
|
+
return messages[englishDefault] ?? englishDefault;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The same resolution as `useCrispMessage`, for a component that builds its
|
|
64
|
+
* own default text internally (TableToolbar's "Sorted by {field}", a
|
|
65
|
+
* DatePicker's month/weekday names) rather than taking one string prop per
|
|
66
|
+
* piece — every literal fragment of that built-in text still needs its own
|
|
67
|
+
* translation lookup, and a component with many such fragments (twelve
|
|
68
|
+
* month names, seven weekday names) would otherwise call `useCrispMessage`
|
|
69
|
+
* in a loop, which breaks the rules of hooks. `useCrispMessages` returns the
|
|
70
|
+
* raw dictionary once; call sites do their own `dict[english] ?? english`
|
|
71
|
+
* per fragment, same fallback order, no hook-in-a-loop.
|
|
72
|
+
*/
|
|
73
|
+
function useCrispMessages() {
|
|
74
|
+
return React.useContext(CrispMessagesContext);
|
|
75
|
+
}
|
package/dist/cjs/core/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.usePrefersReducedMotion = exports.useMentionTrigger = exports.useLabelAssociation = exports.useFormReset = exports.useFocusAfterRemoval = exports.specToRecipe = exports.defineSpec = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.matchMentionTrigger = exports.formValueStyle = exports.registerFloatingLayer = exports.isInsideLaterFloatingLayer = exports.hasOpenFloatingLayer = exports.FloatingPortal = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = exports.parseCellDate = exports.formatCellDate = exports.cellDatePrecision = exports.caretRectInField = void 0;
|
|
17
|
+
exports.usePrefersReducedMotion = exports.useMentionTrigger = exports.useLabelAssociation = exports.useFormReset = exports.useFocusAfterRemoval = exports.specToRecipe = exports.defineSpec = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.matchMentionTrigger = exports.useCrispMessages = exports.useCrispMessage = exports.CrispMessagesProvider = exports.formValueStyle = exports.registerFloatingLayer = exports.isInsideLaterFloatingLayer = exports.hasOpenFloatingLayer = exports.FloatingPortal = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = exports.parseCellDate = exports.formatCellDate = exports.cellDatePrecision = exports.caretRectInField = void 0;
|
|
18
18
|
var caret_rect_1 = require("./caret-rect");
|
|
19
19
|
Object.defineProperty(exports, "caretRectInField", { enumerable: true, get: function () { return caret_rect_1.caretRectInField; } });
|
|
20
20
|
var cell_date_1 = require("./cell-date");
|
|
@@ -37,6 +37,10 @@ Object.defineProperty(exports, "isInsideLaterFloatingLayer", { enumerable: true,
|
|
|
37
37
|
Object.defineProperty(exports, "registerFloatingLayer", { enumerable: true, get: function () { return floating_stack_1.registerFloatingLayer; } });
|
|
38
38
|
var form_value_1 = require("./form-value");
|
|
39
39
|
Object.defineProperty(exports, "formValueStyle", { enumerable: true, get: function () { return form_value_1.formValueStyle; } });
|
|
40
|
+
var i18n_1 = require("./i18n");
|
|
41
|
+
Object.defineProperty(exports, "CrispMessagesProvider", { enumerable: true, get: function () { return i18n_1.CrispMessagesProvider; } });
|
|
42
|
+
Object.defineProperty(exports, "useCrispMessage", { enumerable: true, get: function () { return i18n_1.useCrispMessage; } });
|
|
43
|
+
Object.defineProperty(exports, "useCrispMessages", { enumerable: true, get: function () { return i18n_1.useCrispMessages; } });
|
|
40
44
|
var mention_1 = require("./mention");
|
|
41
45
|
Object.defineProperty(exports, "matchMentionTrigger", { enumerable: true, get: function () { return mention_1.matchMentionTrigger; } });
|
|
42
46
|
var place_floating_1 = require("./place-floating");
|
package/dist/datatable.css
CHANGED
|
@@ -93,6 +93,8 @@
|
|
|
93
93
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
94
94
|
& .crisp-datatable-entity { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
|
|
95
95
|
& .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
96
|
+
& .crisp-datatable-entity:hover .crisp-datatable-entity-name { text-decoration: underline; }
|
|
97
|
+
& button.crisp-datatable-entity-name--link { margin: 0; padding: 0; border: none; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-align: start; cursor: pointer; max-width: 100%; }
|
|
96
98
|
& .crisp-datatable-avatar { width: var(--crisp-size-icon); height: var(--crisp-size-icon); border-radius: var(--crisp-radius-sm); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: var(--crisp-weight-semibold); text-transform: uppercase; color: var(--crisp-fg-on-brand); background: var(--crisp-bg-brand-solid); }
|
|
97
99
|
& .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
|
|
98
100
|
& .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
|
|
@@ -94,6 +94,8 @@
|
|
|
94
94
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
95
95
|
& .crisp-datatable-entity { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
|
|
96
96
|
& .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
97
|
+
& .crisp-datatable-entity:hover .crisp-datatable-entity-name { text-decoration: underline; }
|
|
98
|
+
& button.crisp-datatable-entity-name--link { margin: 0; padding: 0; border: none; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-align: start; cursor: pointer; max-width: 100%; }
|
|
97
99
|
& .crisp-datatable-avatar { width: var(--crisp-size-icon); height: var(--crisp-size-icon); border-radius: var(--crisp-radius-sm); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: var(--crisp-weight-semibold); text-transform: uppercase; color: var(--crisp-fg-on-brand); background: var(--crisp-bg-brand-solid); }
|
|
98
100
|
& .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
|
|
99
101
|
& .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ChevronRight } from "lucide-react";
|
|
3
2
|
import * as React from "react";
|
|
4
3
|
import { breadcrumbRecipe } from "./breadcrumb.recipe.js";
|
|
5
4
|
/**
|
|
@@ -7,7 +6,11 @@ import { breadcrumbRecipe } from "./breadcrumb.recipe.js";
|
|
|
7
6
|
* marked `aria-current="page"`; earlier items render as links or buttons.
|
|
8
7
|
*/
|
|
9
8
|
export function Breadcrumb({ items, separator, "aria-label": ariaLabel = "Breadcrumb", className, ...rest }) {
|
|
10
|
-
|
|
9
|
+
// Live-measured (app.attio.com/<ws>/home, /companies,
|
|
10
|
+
// /company/<id>/overview): a plain rounded divider, not a chevron -- a
|
|
11
|
+
// symmetric shape needs no RTL mirroring, unlike the ChevronRight this
|
|
12
|
+
// used to default to.
|
|
13
|
+
const sep = separator ?? (_jsx("span", { className: "crisp-breadcrumb-sep-dot", "aria-hidden": "true" }));
|
|
11
14
|
return (_jsx("nav", { "aria-label": ariaLabel, className: breadcrumbRecipe({ className }), ...rest, children: items.map((item, i) => {
|
|
12
15
|
const last = i === items.length - 1;
|
|
13
16
|
const content = (_jsxs(_Fragment, { children: [item.icon, item.label] }));
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { defineRecipe, resolveRecipe, rtlFlipCss } from "../../core/index.js";
|
|
2
2
|
import { contract as c } from "../../tokens/index.js";
|
|
3
|
-
// A breadcrumb trail — record-page nav ("Companies
|
|
3
|
+
// A breadcrumb trail — record-page nav ("Companies · Google"), the segments
|
|
4
|
+
// joined by a small rounded divider (crisp#498), not the literal "/" this
|
|
5
|
+
// comment's own example used to imply.
|
|
4
6
|
const config = defineRecipe({
|
|
5
7
|
name: "breadcrumb",
|
|
6
8
|
base: `display: flex; align-items: center; gap: ${c.space["0.5"]}; font-family: ${c.font.sans}; font-size: ${c.text.lg};
|
|
@@ -9,7 +11,8 @@ const config = defineRecipe({
|
|
|
9
11
|
& a.crisp-breadcrumb-item:hover, & button.crisp-breadcrumb-item:hover { color: ${c.fgPrimary}; background: color-mix(in srgb, ${c.fgPrimary} 5%, transparent); }
|
|
10
12
|
& button.crisp-breadcrumb-item { border: none; background: none; font: inherit; cursor: pointer; }
|
|
11
13
|
& .crisp-breadcrumb-item[aria-current] { color: ${c.fgPrimary}; font-weight: ${c.weight.medium}; }
|
|
12
|
-
& .crisp-breadcrumb-sep { flex-shrink: 0;
|
|
14
|
+
& .crisp-breadcrumb-sep { flex-shrink: 0; display: inline-flex; }
|
|
15
|
+
& .crisp-breadcrumb-sep-dot { flex-shrink: 0; width: 3px; height: 10px; border-radius: ${c.radius.full}; background: color-mix(in srgb, ${c.fgPrimary} 28%, transparent); }
|
|
13
16
|
${rtlFlipCss}
|
|
14
17
|
`,
|
|
15
18
|
variants: {},
|
|
@@ -25,7 +25,12 @@ const spec = defineSpec({
|
|
|
25
25
|
decls: {
|
|
26
26
|
background: c.bgBrandSolid,
|
|
27
27
|
color: c.fgOnBrand,
|
|
28
|
-
|
|
28
|
+
// crisp#494: the inset ring is a black tint, not white -- live-
|
|
29
|
+
// measured on Attio's own "New Company" button as
|
|
30
|
+
// rgba(0,0,0,0.06), not the rgba(255,255,255,0.07) both solid
|
|
31
|
+
// intents shared. The other two shadow layers already matched
|
|
32
|
+
// exactly.
|
|
33
|
+
boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08)",
|
|
29
34
|
},
|
|
30
35
|
parts: [
|
|
31
36
|
{ selector: "&:hover", decls: { background: c.bgBrandSolidPressed } },
|
|
@@ -47,7 +52,10 @@ const spec = defineSpec({
|
|
|
47
52
|
decls: {
|
|
48
53
|
background: c.bgDangerSolid,
|
|
49
54
|
color: c.fgOnBrand,
|
|
50
|
-
|
|
55
|
+
// Same corrected inset ring as primary (crisp#494) -- shared
|
|
56
|
+
// literal, same solid-fill button family; the coloured layers
|
|
57
|
+
// themselves weren't independently re-measured this round.
|
|
58
|
+
boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(186,37,37,0.20), 0 3px 6px -2px rgba(186,37,37,0.12)",
|
|
51
59
|
},
|
|
52
60
|
parts: [
|
|
53
61
|
{ selector: "&:hover", decls: { filter: "brightness(0.96)" } },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ArrowDown, ArrowUp, Check, ChevronsUpDown, CircleUserRound, GripVertical, Plus, Search, SlidersHorizontal, } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { cellDatePrecision, formatCellDate, isRtlElement, parseCellDate, } from "../../core/index.js";
|
|
4
|
+
import { cellDatePrecision, formatCellDate, isRtlElement, parseCellDate, useCrispMessage, useCrispMessages, } from "../../core/index.js";
|
|
5
5
|
import { Avatar } from "../avatar/index.js";
|
|
6
6
|
import { Checkbox } from "../checkbox/index.js";
|
|
7
7
|
import { Menu, MenuItem } from "../menu/index.js";
|
|
@@ -165,7 +165,7 @@ function formatCell(type, v, symbol, locale = "en-US") {
|
|
|
165
165
|
// silently drop the cell's data. Each branch decides for itself where a
|
|
166
166
|
// display string belongs -- as an entity's name, a status' label -- while the
|
|
167
167
|
// raw value keeps driving structure and tone.
|
|
168
|
-
function renderTyped(col, value, avatarTone, display, locale = "en-US") {
|
|
168
|
+
function renderTyped(col, value, avatarTone, row, display, locale = "en-US") {
|
|
169
169
|
// Attio's own empty cell is genuinely blank -- no placeholder character at
|
|
170
170
|
// all, for any column type (#412).
|
|
171
171
|
const empty = null;
|
|
@@ -177,7 +177,14 @@ function renderTyped(col, value, avatarTone, display, locale = "en-US") {
|
|
|
177
177
|
const e = display ? { ...raw, name: display } : raw;
|
|
178
178
|
if (!e.name)
|
|
179
179
|
return empty;
|
|
180
|
-
return (_jsxs("span", { className: "crisp-datatable-entity", children: [_jsx("span", { className: "crisp-datatable-avatar", "data-tone": avatarTone === "hashed" ? toneFor(raw.name) : undefined, children: e.avatar ? (_jsx("img", { src: e.avatar, alt: "" })) : (initials(e.name).slice(0, 2)) }), _jsx("
|
|
180
|
+
return (_jsxs("span", { className: "crisp-datatable-entity", children: [_jsx("span", { className: "crisp-datatable-avatar", "data-tone": avatarTone === "hashed" ? toneFor(raw.name) : undefined, children: e.avatar ? (_jsx("img", { src: e.avatar, alt: "" })) : (initials(e.name).slice(0, 2)) }), col.onEntityClick ? (_jsx("button", { type: "button", className: "crisp-datatable-entity-name crisp-datatable-entity-name--link", onClick: (ev) => {
|
|
181
|
+
// the cell's own click already just selects (see the
|
|
182
|
+
// TableCell onClick above) -- this must additionally stop
|
|
183
|
+
// the row's onRowClick from firing, since this name points
|
|
184
|
+
// at a DIFFERENT record than the row itself (#501).
|
|
185
|
+
ev.stopPropagation();
|
|
186
|
+
col.onEntityClick?.(row);
|
|
187
|
+
}, children: e.name })) : (_jsx("span", { className: "crisp-datatable-entity-name", children: e.name }))] }));
|
|
181
188
|
}
|
|
182
189
|
case "tags": {
|
|
183
190
|
// asking for a format on a tag column means "render it your way" -- show
|
|
@@ -294,9 +301,47 @@ function DataReferenceEditor({ col, value, isPending, onCommit, onCancel, }) {
|
|
|
294
301
|
onCommit(v);
|
|
295
302
|
} }) }));
|
|
296
303
|
}
|
|
297
|
-
export function DataTable({ columns: columnsProp, data, selectable = false, selectedIds, onSelectionChange, selectAllLabel
|
|
298
|
-
|
|
299
|
-
|
|
304
|
+
export function DataTable({ columns: columnsProp, data, selectable = false, selectedIds, onSelectionChange, selectAllLabel: selectAllLabelProp, selectRowLabel = (row) => `Select row ${row.id}`, selectionAnnouncement, columnMenu = false, onColumnsChange, groupBy, onRowsReorder, error, sort: sortProp, defaultSort, onSortChange, filterable = false, searchPlaceholder: searchPlaceholderProp, searchLabel: searchLabelProp, onEdit, stickyFirst = true, stickyLast = false, locale: localeProp, avatarTone = "brand", surface, interactive = false, onRowClick, onAddRow, addRowLabel: addRowLabelProp, empty: emptyProp, showCount = true, countLabel, toolbar, className, reorderRowLabel: reorderRowLabelProp, sortAscendingLabel: sortAscendingLabelProp, sortDescendingLabel: sortDescendingLabelProp, moveColumnLeftLabel: moveColumnLeftLabelProp, moveColumnRightLabel: moveColumnRightLabelProp, editColumnLabel: editColumnLabelProp, hideColumnLabel: hideColumnLabelProp, renameColumnLabel: renameColumnLabelProp, viewSettingsLabel: viewSettingsLabelProp, columnsGroupLabel: columnsGroupLabelProp, ...rest }) {
|
|
305
|
+
// crisp/#471: same resolution order as TableToolbar's own — an explicit
|
|
306
|
+
// prop wins, then the provider's translation of the built-in English
|
|
307
|
+
// default, then that default itself with no provider in the tree.
|
|
308
|
+
const crispMessages = useCrispMessages();
|
|
309
|
+
const selectAllLabel = useCrispMessage(selectAllLabelProp, "Select all rows");
|
|
310
|
+
const searchPlaceholder = useCrispMessage(searchPlaceholderProp, "Search…");
|
|
311
|
+
const searchLabel = useCrispMessage(searchLabelProp, "Search table");
|
|
312
|
+
// Also `React.ReactNode`-typed, same reasoning as `empty` below.
|
|
313
|
+
const addRowLabel = addRowLabelProp ?? crispMessages["New record"] ?? "New record";
|
|
314
|
+
// `empty` is typed `React.ReactNode`, not `string` — a consumer can pass a
|
|
315
|
+
// whole custom empty-state element, not just replacement text, so it
|
|
316
|
+
// can't go through `useCrispMessage` (string-only). A ReactNode override
|
|
317
|
+
// passes straight through unchanged (it's already whatever the consumer
|
|
318
|
+
// wants shown); only the built-in string default translates.
|
|
319
|
+
const empty = emptyProp ?? crispMessages["No results."] ?? "No results.";
|
|
320
|
+
const reorderRowLabel = useCrispMessage(reorderRowLabelProp, "Reorder row");
|
|
321
|
+
const sortAscendingLabel = useCrispMessage(sortAscendingLabelProp, "Sort ascending");
|
|
322
|
+
const sortDescendingLabel = useCrispMessage(sortDescendingLabelProp, "Sort descending");
|
|
323
|
+
const moveColumnLeftLabel = useCrispMessage(moveColumnLeftLabelProp, "Move left");
|
|
324
|
+
const moveColumnRightLabel = useCrispMessage(moveColumnRightLabelProp, "Move right");
|
|
325
|
+
const editColumnLabel = useCrispMessage(editColumnLabelProp, "Edit column label");
|
|
326
|
+
const hideColumnLabel = useCrispMessage(hideColumnLabelProp, "Hide from view");
|
|
327
|
+
const renameColumnLabel = useCrispMessage(renameColumnLabelProp, "Rename column");
|
|
328
|
+
const viewSettingsLabel = useCrispMessage(viewSettingsLabelProp, "View settings");
|
|
329
|
+
const columnsGroupLabel = useCrispMessage(columnsGroupLabelProp, "Columns");
|
|
330
|
+
// countLabel/selectionAnnouncement stay formatter props, same escape hatch
|
|
331
|
+
// as TableToolbar's sortTriggerLabel — word order and pluralisation rules
|
|
332
|
+
// differ too much across languages for a template. Their BUILT-IN default
|
|
333
|
+
// (when the consumer supplies neither) still translates its own static
|
|
334
|
+
// fragments through the provider, same as TableToolbar's "Sorted by".
|
|
335
|
+
const resolvedCountLabel = countLabel ??
|
|
336
|
+
((shown, total) => shown === total
|
|
337
|
+
? `${total} ${total === 1
|
|
338
|
+
? (crispMessages.row ?? "row")
|
|
339
|
+
: (crispMessages.rows ?? "rows")}`
|
|
340
|
+
: `${shown} ${crispMessages.of ?? "of"} ${total}`);
|
|
341
|
+
const resolvedSelectionAnnouncement = selectionAnnouncement ??
|
|
342
|
+
((count) => count === 0
|
|
343
|
+
? (crispMessages["Selection cleared"] ?? "Selection cleared")
|
|
344
|
+
: `${count} ${crispMessages.selected ?? "selected"}`);
|
|
300
345
|
// Two different defaults on purpose. FORMATTING falls back to "en-US", which
|
|
301
346
|
// is what the built-in date/number/currency renderers have always produced,
|
|
302
347
|
// so no existing output moves. SORTING falls back to `undefined`, which is
|
|
@@ -390,7 +435,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
390
435
|
const [selectionText, setSelectionText] = React.useState("");
|
|
391
436
|
if (prevSelectedCount.current !== selected.size) {
|
|
392
437
|
prevSelectedCount.current = selected.size;
|
|
393
|
-
setSelectionText(
|
|
438
|
+
setSelectionText(resolvedSelectionAnnouncement(selected.size));
|
|
394
439
|
}
|
|
395
440
|
const [editing, setEditing] = React.useState(null);
|
|
396
441
|
// Set only while an async `onEdit` is in flight (#385) -- a sync `onEdit`
|
|
@@ -736,7 +781,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
736
781
|
return dir === "asc" ? (_jsx(ArrowUp, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 })) : (_jsx(ArrowDown, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 }));
|
|
737
782
|
return (_jsx(ChevronsUpDown, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 }));
|
|
738
783
|
}
|
|
739
|
-
return (_jsxs("div", { ref: gridRef, className: datatableRecipe({ className }), tabIndex: -1, onKeyDown: onGridKeyDown, ...rest, children: [_jsx("div", { className: "crisp-datatable-live", role: "status", "aria-live": "polite", children: selectionText }), (filterable || showCount || toolbar || columnMenu) && (_jsxs("div", { className: "crisp-datatable-toolbar", children: [columnMenu && (_jsxs(Menu, { align: "start", trigger: _jsxs("button", { type: "button", className: "crisp-datatable-viewsettings", children: [_jsx(SlidersHorizontal, { size: 14, strokeWidth: 1.75 }),
|
|
784
|
+
return (_jsxs("div", { ref: gridRef, className: datatableRecipe({ className }), tabIndex: -1, onKeyDown: onGridKeyDown, ...rest, children: [_jsx("div", { className: "crisp-datatable-live", role: "status", "aria-live": "polite", children: selectionText }), (filterable || showCount || toolbar || columnMenu) && (_jsxs("div", { className: "crisp-datatable-toolbar", children: [columnMenu && (_jsxs(Menu, { align: "start", trigger: _jsxs("button", { type: "button", className: "crisp-datatable-viewsettings", children: [_jsx(SlidersHorizontal, { size: 14, strokeWidth: 1.75 }), viewSettingsLabel] }), children: [_jsx("div", { className: "crisp-datatable-group-label", children: columnsGroupLabel }), allColumns.map((c) => (_jsxs(MenuItem, { onSelect: () => c.hidden ? showColumn(c.key) : hideColumn(c.key), children: [_jsx(Check, { size: 14, strokeWidth: 2, style: { opacity: c.hidden ? 0 : 1 } }), typeof c.header === "string" ? c.header : c.key] }, c.key)))] })), filterable && (_jsxs("span", { className: "crisp-datatable-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("input", { type: "search", placeholder: searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), "aria-label": searchLabel })] })), showCount && (_jsx("span", { className: "crisp-datatable-count", children: resolvedCountLabel(rows.length, data.length) })), toolbar] })), _jsxs(Table, { interactive: interactive, surface: surface, style: { minWidth }, scrollProps: {
|
|
740
785
|
ref: scrollRef,
|
|
741
786
|
onScroll: (e) => {
|
|
742
787
|
const el = e.currentTarget;
|
|
@@ -766,7 +811,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
766
811
|
const inner = (_jsxs("span", { className: "crisp-datatable-th-inner", children: [Icon ? _jsx(Icon, { size: 16, strokeWidth: 1.75 }) : null, _jsx("span", { className: "crisp-datatable-th-label", children: col.header }), col.sortable ? _jsx(ChevronFor, { colKey: col.key }) : null] }));
|
|
767
812
|
return (_jsxs(TableHead, { align: alignFor(col), "aria-sort": ariaSort, ...dataStickyCol(i), children: [renaming === col.key ? (_jsx("input", {
|
|
768
813
|
// biome-ignore lint/a11y/noAutofocus: the rename item opens this field
|
|
769
|
-
autoFocus: true, className: "crisp-datatable-th-rename", defaultValue: typeof col.header === "string" ? col.header : "", "aria-label":
|
|
814
|
+
autoFocus: true, className: "crisp-datatable-th-rename", defaultValue: typeof col.header === "string" ? col.header : "", "aria-label": renameColumnLabel, onBlur: (e) => {
|
|
770
815
|
renameColumn(col.key, e.currentTarget.value);
|
|
771
816
|
setRenaming(null);
|
|
772
817
|
}, onKeyDown: (e) => {
|
|
@@ -778,7 +823,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
778
823
|
setRenaming(null);
|
|
779
824
|
} })) : columnMenu ? (_jsxs(Menu, { align: "start", trigger: _jsx("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), "aria-label": `${typeof col.header === "string"
|
|
780
825
|
? col.header
|
|
781
|
-
: col.key} column options`, children: inner }), children: [_jsx(MenuItem, { onSelect: () => setSort({ key: col.key, dir: "asc" }), children:
|
|
826
|
+
: col.key} ${crispMessages["column options"] ?? "column options"}`, children: inner }), children: [_jsx(MenuItem, { onSelect: () => setSort({ key: col.key, dir: "asc" }), children: sortAscendingLabel }), _jsx(MenuItem, { onSelect: () => setSort({ key: col.key, dir: "desc" }), children: sortDescendingLabel }), col.managed === false ? null : (_jsxs(_Fragment, { children: [_jsx(MenuItem, { onSelect: () => moveColumn(col.key, -1), children: moveColumnLeftLabel }), _jsx(MenuItem, { onSelect: () => moveColumn(col.key, 1), children: moveColumnRightLabel }), _jsx(MenuItem, { onSelect: () => setRenaming(col.key), children: editColumnLabel }), _jsx(MenuItem, { onSelect: () => hideColumn(col.key), children: hideColumnLabel })] }))] })) : col.sortable ? (_jsx("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), onClick: () => cycleSort(col.key), children: inner })) : (inner), col.resizable !== false && (_jsx("span", { className: "crisp-datatable-resize", "data-resizing": resizingKey === col.key ? "" : undefined, onPointerDown: (e) => startResize(col, e), onPointerMove: moveResize, onPointerUp: endResize }))] }, col.key));
|
|
782
827
|
})] }) }), _jsx(TableBody, { children: error ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: totalCols, children: _jsx("div", { className: "crisp-datatable-empty", children: error }) }) })) : rows.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: totalCols, children: _jsx("div", { className: "crisp-datatable-empty", children: empty }) }) })) : (groupedRows.map(({ group, rows: groupRows }) => (_jsxs(React.Fragment, { children: [group !== null && (_jsx(TableRow, { children: _jsxs(TableCell, { colSpan: totalCols, className: "crisp-datatable-group", children: [_jsx("span", { className: "crisp-datatable-group-label", children: group }), _jsx("span", { className: "crisp-datatable-group-count", children: groupRows.length })] }) })), groupRows.map((row) => (_jsxs(TableRow, { "data-selected": selected.has(row.id) ? "" : undefined, "data-drop-target": showGrip &&
|
|
783
828
|
dragRowId &&
|
|
784
829
|
dragRowId !== row.id &&
|
|
@@ -876,7 +921,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
876
921
|
? () => openEditing(row.id, col.key)
|
|
877
922
|
: undefined, children: [col.render
|
|
878
923
|
? col.render(value, row)
|
|
879
|
-
: renderTyped(col, value, avatarTone, col.format?.(value, row), locale), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
924
|
+
: renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
880
925
|
})] }, row.id)))] }, group ?? "__ungrouped")))) }), (hasSummary || onAddRow) && (_jsxs(TableFooter, { children: [hasSummary && (_jsxs(TableRow, { children: [showGrip && _jsx(TableCell, { ...stickyCol(-2) }), selectable && _jsx(TableCell, { ...stickyCol(-1) }), columns.map((col, i) => (_jsx(TableCell, { align: alignFor(col), ...dataStickyCol(i), children: col.summary ? col.summary(rows) : null }, col.key)))] })), onAddRow && (_jsx(TableRow, { children: _jsx(TableCell, { className: "crisp-datatable-addcell", colSpan: totalCols, children: _jsxs("button", { type: "button", className: "crisp-datatable-addrow", onClick: onAddRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), addRowLabel] }) }) }))] }))] })] }));
|
|
881
926
|
}
|
|
882
927
|
DataTable.displayName = "DataTable";
|
|
@@ -121,6 +121,8 @@ const config = defineRecipe({
|
|
|
121
121
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
122
122
|
& .crisp-datatable-entity { display: flex; align-items: center; gap: ${c.space["1.5"]}; min-width: 0; color: ${c.fgPrimary}; font-weight: ${c.weight.medium}; }
|
|
123
123
|
& .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
124
|
+
& .crisp-datatable-entity:hover .crisp-datatable-entity-name { text-decoration: underline; }
|
|
125
|
+
& button.crisp-datatable-entity-name--link { margin: 0; padding: 0; border: none; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-align: start; cursor: pointer; max-width: 100%; }
|
|
124
126
|
& .crisp-datatable-avatar { width: ${c.size.icon}; height: ${c.size.icon}; border-radius: ${c.radius.sm}; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: ${c.weight.semibold}; text-transform: uppercase; color: ${c.fgOnBrand}; background: ${c.bgBrandSolid}; }
|
|
125
127
|
& .crisp-datatable-avatar[data-tone="success"] { background: ${c.bgSuccess}; } & .crisp-datatable-avatar[data-tone="warning"] { background: ${c.bgWarning}; } & .crisp-datatable-avatar[data-tone="danger"] { background: ${c.bgDanger}; } & .crisp-datatable-avatar[data-tone="purple"] { background: ${c.bgPurple}; } & .crisp-datatable-avatar[data-tone="cyan"] { background: ${c.bgCyan}; } & .crisp-datatable-avatar[data-tone="orange"] { background: ${c.bgOrange}; }
|
|
126
128
|
& .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { CalendarDays, X } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useCrispMessage } from "../../core/index.js";
|
|
4
5
|
import { DatePicker, } from "../datepicker/index.js";
|
|
5
6
|
import { Popover } from "../popover/index.js";
|
|
6
7
|
import { datepickerfieldRecipe } from "./datepickerfield.recipe.js";
|
|
@@ -21,7 +22,10 @@ const fromDate = (d) => ({
|
|
|
21
22
|
* popover. This is what `DatePicker` used to be; `DatePicker` is now
|
|
22
23
|
* seed-design's bare calendar, and this composes it.
|
|
23
24
|
*/
|
|
24
|
-
export function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder
|
|
25
|
+
export function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder: placeholderProp, variant = "default", format = defaultFormat, clearable = true, clearLabel: clearLabelProp, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }) {
|
|
26
|
+
// crisp/#471: same resolution order as Select's own.
|
|
27
|
+
const placeholder = useCrispMessage(placeholderProp, "Set date…");
|
|
28
|
+
const clearLabel = useCrispMessage(clearLabelProp, "Clear date");
|
|
25
29
|
const [internal, setInternal] = React.useState(defaultValue ?? null);
|
|
26
30
|
const selected = value ?? internal;
|
|
27
31
|
const [open, setOpen] = React.useState(false);
|
|
@@ -3,7 +3,7 @@ import * as dialog from "@zag-js/dialog";
|
|
|
3
3
|
import { normalizeProps, Portal, useMachine } from "@zag-js/react";
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
5
|
import * as React from "react";
|
|
6
|
-
import { cx, hasOpenFloatingLayer, withDescribedBy } from "../../core/index.js";
|
|
6
|
+
import { cx, hasOpenFloatingLayer, useCrispMessage, withDescribedBy, } from "../../core/index.js";
|
|
7
7
|
import { dialogRecipe } from "./dialog.recipe.js";
|
|
8
8
|
// A globally unique id per mounted instance (see command.tsx for the rationale —
|
|
9
9
|
// the machine defers its dismissal listeners a frame, so tree-relative useId
|
|
@@ -22,7 +22,8 @@ function useDialogInstanceId() {
|
|
|
22
22
|
* Escape / outside-click dismissal) with the measured modal surface.
|
|
23
23
|
* Compose the body with `DialogBody` and actions with `DialogFooter`.
|
|
24
24
|
*/
|
|
25
|
-
export function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ariaLabel, closeLabel
|
|
25
|
+
export function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ariaLabel, closeLabel: closeLabelProp, children, className, style, "aria-describedby": describedBy, ...rest }) {
|
|
26
|
+
const closeLabel = useCrispMessage(closeLabelProp, "Close");
|
|
26
27
|
const id = useDialogInstanceId();
|
|
27
28
|
const service = useMachine(dialog.machine, {
|
|
28
29
|
id,
|
|
@@ -3,7 +3,7 @@ import * as dialog from "@zag-js/dialog";
|
|
|
3
3
|
import { normalizeProps, Portal, useMachine } from "@zag-js/react";
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
5
|
import * as React from "react";
|
|
6
|
-
import { cx, hasOpenFloatingLayer, withDescribedBy } from "../../core/index.js";
|
|
6
|
+
import { cx, hasOpenFloatingLayer, useCrispMessage, withDescribedBy, } from "../../core/index.js";
|
|
7
7
|
import { drawerRecipe } from "./drawer.recipe.js";
|
|
8
8
|
let instanceCounter = 0;
|
|
9
9
|
function useDrawerInstanceId() {
|
|
@@ -19,7 +19,8 @@ function useDrawerInstanceId() {
|
|
|
19
19
|
* Escape / outside-click dismissal) with a panel that slides in from the edge.
|
|
20
20
|
* Compose the body with `DrawerBody` and actions with `DrawerFooter`.
|
|
21
21
|
*/
|
|
22
|
-
export function Drawer({ open, onOpenChange, side = "right", title, icon, "aria-label": ariaLabel, closeLabel
|
|
22
|
+
export function Drawer({ open, onOpenChange, side = "right", title, icon, "aria-label": ariaLabel, closeLabel: closeLabelProp, children, className, "aria-describedby": describedBy, ...rest }) {
|
|
23
|
+
const closeLabel = useCrispMessage(closeLabelProp, "Close");
|
|
23
24
|
const id = useDrawerInstanceId();
|
|
24
25
|
const service = useMachine(dialog.machine, {
|
|
25
26
|
id,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Check, ChevronDown, Search } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { FloatingPortal, formValueStyle, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useFormReset, } from "../../core/index.js";
|
|
4
|
+
import { FloatingPortal, formValueStyle, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, useFormReset, } from "../../core/index.js";
|
|
5
5
|
import { useListboxKeyboard } from "../select/index.js";
|
|
6
6
|
import { multiselectRecipe } from "./multiselect.recipe.js";
|
|
7
7
|
const TONES = [
|
|
@@ -33,7 +33,11 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
|
|
|
33
33
|
// The id belongs on the trigger, not the wrapper: `<label for>` only names a
|
|
34
34
|
// LABELABLE element, so an id left on the root div meant a FormField label
|
|
35
35
|
// above this control named nothing (#247). Select already did this.
|
|
36
|
-
id, placeholder
|
|
36
|
+
id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPlaceholderProp, emptyLabel: emptyLabelProp, disabled, invalid, className, "aria-label": ariaLabel, ...rest }) {
|
|
37
|
+
// crisp/#471: same resolution order as Select's own.
|
|
38
|
+
const placeholder = useCrispMessage(placeholderProp, "Select…");
|
|
39
|
+
const searchPlaceholder = useCrispMessage(searchPlaceholderProp, "Search…");
|
|
40
|
+
const emptyLabel = useCrispMessage(emptyLabelProp, "No results");
|
|
37
41
|
// Virtual focus: DOM focus stays on the trigger (or the search field) and the
|
|
38
42
|
// active option is named by `aria-activedescendant`. `data-highlighted` is a
|
|
39
43
|
// styling hook only -- a screen reader cannot see it.
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { QuantityPickerDecrementButton, QuantityPickerHiddenInput, QuantityPickerIncrementButton, QuantityPickerRoot, useQuantityPickerContext, } from "@seed-design/react-quantity-picker";
|
|
4
4
|
import { Minus, Plus, Trash2 } from "lucide-react";
|
|
5
5
|
import * as React from "react";
|
|
6
|
-
import { useLabelAssociation } from "../../core/index.js";
|
|
6
|
+
import { useCrispMessage, useLabelAssociation } from "../../core/index.js";
|
|
7
7
|
import { Spinner } from "../spinner/index.js";
|
|
8
8
|
import { quantitypickerRecipe } from "./quantitypicker.recipe.js";
|
|
9
9
|
/** seed's icon box per size, read from their stylesheet. */
|
|
@@ -55,7 +55,10 @@ function IncrementIcon({ size }) {
|
|
|
55
55
|
* Slider documents. Here it also decides child ORDER, since the engine
|
|
56
56
|
* reverses its visible children rather than relying on CSS.
|
|
57
57
|
*/
|
|
58
|
-
export function QuantityPicker({ size = "medium", fill = false, className, name, decrementLabel
|
|
58
|
+
export function QuantityPicker({ size = "medium", fill = false, className, name, decrementLabel: decrementLabelProp, incrementLabel: incrementLabelProp, ...props }) {
|
|
59
|
+
// crisp/#471: same resolution order as Select's own.
|
|
60
|
+
const decrementLabel = useCrispMessage(decrementLabelProp, "Decrease quantity");
|
|
61
|
+
const incrementLabel = useCrispMessage(incrementLabelProp, "Increase quantity");
|
|
59
62
|
// A `<label for>` cannot name a div, and this root is a role="group" (#247).
|
|
60
63
|
const rootRef = React.useRef(null);
|
|
61
64
|
useLabelAssociation(rootRef);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Check, ChevronDown, Search } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { FloatingPortal, formValueStyle, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useFormReset, } from "../../core/index.js";
|
|
4
|
+
import { FloatingPortal, formValueStyle, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, useFormReset, } from "../../core/index.js";
|
|
5
5
|
import { useListboxKeyboard } from "./listbox-keyboard.js";
|
|
6
6
|
import { selectRecipe } from "./select.recipe.js";
|
|
7
7
|
function optionText(o) {
|
|
@@ -16,7 +16,13 @@ function optionText(o) {
|
|
|
16
16
|
* "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
|
|
17
17
|
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
18
18
|
*/
|
|
19
|
-
export function Select({ options, value, defaultValue, onValueChange, name, required, placeholder
|
|
19
|
+
export function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant = "default", width = "full", searchable, searchPlaceholder: searchPlaceholderProp, onSearchChange, emptyLabel: emptyLabelProp, disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
20
|
+
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
21
|
+
// CrispMessagesProvider's translation of the English default, or that
|
|
22
|
+
// default itself with no provider — unchanged from before this existed.
|
|
23
|
+
const placeholder = useCrispMessage(placeholderProp, "Select…");
|
|
24
|
+
const searchPlaceholder = useCrispMessage(searchPlaceholderProp, "Search…");
|
|
25
|
+
const emptyLabel = useCrispMessage(emptyLabelProp, "No results");
|
|
20
26
|
// Virtual focus: DOM focus stays on the trigger (or the search field), and the
|
|
21
27
|
// active option is named by `aria-activedescendant`. That is the only thing a
|
|
22
28
|
// screen reader has to go on -- `data-highlighted` is invisible to it -- so the
|
|
@@ -5,8 +5,9 @@ import { statcardRecipe } from "./statcard.recipe.js";
|
|
|
5
5
|
* A single-metric KPI tile — measured from a reports number/insight card.
|
|
6
6
|
* Label, a large value, and an optional up/down delta pill with a caption.
|
|
7
7
|
*/
|
|
8
|
-
export function StatCard({ label, icon, value, delta, deltaSuffix = "%", caption, card, className, ...rest }) {
|
|
8
|
+
export function StatCard({ label, icon, value, delta, deltaSuffix = "%", caption, captionTone, card, className, ...rest }) {
|
|
9
9
|
const trend = delta == null ? null : delta > 0 ? "up" : delta < 0 ? "down" : "flat";
|
|
10
|
-
return (_jsxs("div", { className: statcardRecipe({ card, className }), ...rest, children: [_jsxs("span", { className: "crisp-statcard-label", children: [icon, label] }), _jsx("span", { className: "crisp-statcard-value", children: value }), (trend != null || caption != null) && (_jsxs("span", { className: "crisp-statcard-foot", children: [trend != null && (_jsxs("span", { className: "crisp-statcard-delta", "data-trend": trend, children: [trend === "up" && _jsx(ArrowUp, { size: 12, strokeWidth: 2.5 }), trend === "down" && _jsx(ArrowDown, { size: 12, strokeWidth: 2.5 }), Math.abs(delta), deltaSuffix] })), caption
|
|
10
|
+
return (_jsxs("div", { className: statcardRecipe({ card, className }), ...rest, children: [_jsxs("span", { className: "crisp-statcard-label", children: [icon, label] }), _jsx("span", { className: "crisp-statcard-value", children: value }), (trend != null || caption != null) && (_jsxs("span", { className: "crisp-statcard-foot", children: [trend != null && (_jsxs("span", { className: "crisp-statcard-delta", "data-trend": trend, children: [trend === "up" && _jsx(ArrowUp, { size: 12, strokeWidth: 2.5 }), trend === "down" && _jsx(ArrowDown, { size: 12, strokeWidth: 2.5 }), Math.abs(delta), deltaSuffix] })), caption != null &&
|
|
11
|
+
(captionTone ? (_jsx("span", { className: "crisp-statcard-caption", "data-tone": captionTone, children: caption })) : (caption))] }))] }));
|
|
11
12
|
}
|
|
12
13
|
StatCard.displayName = "StatCard";
|
|
@@ -11,6 +11,16 @@ const config = defineRecipe({
|
|
|
11
11
|
& .crisp-statcard-label svg { color: ${c.fgMuted}; }
|
|
12
12
|
& .crisp-statcard-value { font-size: 28px; line-height: 32px; font-weight: ${c.weight.semibold}; letter-spacing: -0.4px; color: ${c.fgPrimary}; }
|
|
13
13
|
& .crisp-statcard-foot { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; font-size: ${c.text.sm}; color: ${c.fgMuted}; }
|
|
14
|
+
/* caption's own status colour, opt-in via data-tone: not a new measured
|
|
15
|
+
value — the same fg*Soft tokens Badge's tone already uses (and Badge's
|
|
16
|
+
own doc comment records why: a dedicated soft-text token, not the
|
|
17
|
+
tone's accent colour, is the one that clears AA). No "ok" reading on
|
|
18
|
+
Badge itself; fgSuccessSoft is Badge's own "success" tone's text
|
|
19
|
+
colour, reused here under the name a caption's own good-news state
|
|
20
|
+
reads as. */
|
|
21
|
+
& .crisp-statcard-caption[data-tone="ok"] { color: ${c.fgSuccessSoft}; }
|
|
22
|
+
& .crisp-statcard-caption[data-tone="warning"] { color: ${c.fgWarningSoft}; }
|
|
23
|
+
& .crisp-statcard-caption[data-tone="critical"] { color: ${c.fgDangerSoft}; }
|
|
14
24
|
& .crisp-statcard-delta { display: inline-flex; align-items: center; gap: 2px; height: 20px; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; }
|
|
15
25
|
& .crisp-statcard-delta svg { flex-shrink: 0; }
|
|
16
26
|
& .crisp-statcard-delta[data-trend="up"] { background: color-mix(in srgb, ${c.bgSuccess} 16%, transparent); color: color-mix(in srgb, ${c.bgSuccess} 80%, ${c.fgPrimary}); }
|