@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
package/dist/breadcrumb.css
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
& a.crisp-breadcrumb-item:hover, & button.crisp-breadcrumb-item:hover { color: var(--crisp-fg-primary); background: color-mix(in srgb, var(--crisp-fg-primary) 5%, transparent); }
|
|
6
6
|
& button.crisp-breadcrumb-item { border: none; background: none; font: inherit; cursor: pointer; }
|
|
7
7
|
& .crisp-breadcrumb-item[aria-current] { color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
|
|
8
|
-
& .crisp-breadcrumb-sep { flex-shrink: 0;
|
|
8
|
+
& .crisp-breadcrumb-sep { flex-shrink: 0; display: inline-flex; }
|
|
9
|
+
& .crisp-breadcrumb-sep-dot { flex-shrink: 0; width: 3px; height: 10px; border-radius: var(--crisp-radius-full); background: color-mix(in srgb, var(--crisp-fg-primary) 28%, transparent); }
|
|
9
10
|
[dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
|
|
10
11
|
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
& a.crisp-breadcrumb-item:hover, & button.crisp-breadcrumb-item:hover { color: var(--crisp-fg-primary); background: color-mix(in srgb, var(--crisp-fg-primary) 5%, transparent); }
|
|
7
7
|
& button.crisp-breadcrumb-item { border: none; background: none; font: inherit; cursor: pointer; }
|
|
8
8
|
& .crisp-breadcrumb-item[aria-current] { color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
|
|
9
|
-
& .crisp-breadcrumb-sep { flex-shrink: 0;
|
|
9
|
+
& .crisp-breadcrumb-sep { flex-shrink: 0; display: inline-flex; }
|
|
10
|
+
& .crisp-breadcrumb-sep-dot { flex-shrink: 0; width: 3px; height: 10px; border-radius: var(--crisp-radius-full); background: color-mix(in srgb, var(--crisp-fg-primary) 28%, transparent); }
|
|
10
11
|
[dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
|
|
11
12
|
}
|
|
12
13
|
}
|
package/dist/button.css
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
&:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
4
4
|
}
|
|
5
5
|
.crisp-button--intent_primary {
|
|
6
|
-
background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); box-shadow: inset 0 0 0 1px rgba(
|
|
6
|
+
background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); box-shadow: 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);
|
|
7
7
|
&:hover { background: var(--crisp-bg-brand-solid-pressed); }
|
|
8
8
|
&:active { background: var(--crisp-bg-brand-solid-pressed); }
|
|
9
9
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
&:hover { background: var(--crisp-bg-raised); }
|
|
13
13
|
}
|
|
14
14
|
.crisp-button--intent_danger {
|
|
15
|
-
background: var(--crisp-bg-danger-solid); color: var(--crisp-fg-on-brand); box-shadow: inset 0 0 0 1px rgba(
|
|
15
|
+
background: var(--crisp-bg-danger-solid); color: var(--crisp-fg-on-brand); box-shadow: 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);
|
|
16
16
|
&:hover { filter: brightness(0.96); }
|
|
17
17
|
&:active { filter: brightness(0.92); }
|
|
18
18
|
}
|
package/dist/button.layered.css
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
&:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
5
5
|
}
|
|
6
6
|
.crisp-button--intent_primary {
|
|
7
|
-
background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); box-shadow: inset 0 0 0 1px rgba(
|
|
7
|
+
background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); box-shadow: 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);
|
|
8
8
|
&:hover { background: var(--crisp-bg-brand-solid-pressed); }
|
|
9
9
|
&:active { background: var(--crisp-bg-brand-solid-pressed); }
|
|
10
10
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
&:hover { background: var(--crisp-bg-raised); }
|
|
14
14
|
}
|
|
15
15
|
.crisp-button--intent_danger {
|
|
16
|
-
background: var(--crisp-bg-danger-solid); color: var(--crisp-fg-on-brand); box-shadow: inset 0 0 0 1px rgba(
|
|
16
|
+
background: var(--crisp-bg-danger-solid); color: var(--crisp-fg-on-brand); box-shadow: 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);
|
|
17
17
|
&:hover { filter: brightness(0.96); }
|
|
18
18
|
&:active { filter: brightness(0.92); }
|
|
19
19
|
}
|
|
@@ -35,7 +35,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.Breadcrumb = Breadcrumb;
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
|
-
const lucide_react_1 = require("lucide-react");
|
|
39
38
|
const React = __importStar(require("react"));
|
|
40
39
|
const breadcrumb_recipe_1 = require("./breadcrumb.recipe");
|
|
41
40
|
/**
|
|
@@ -43,7 +42,11 @@ const breadcrumb_recipe_1 = require("./breadcrumb.recipe");
|
|
|
43
42
|
* marked `aria-current="page"`; earlier items render as links or buttons.
|
|
44
43
|
*/
|
|
45
44
|
function Breadcrumb({ items, separator, "aria-label": ariaLabel = "Breadcrumb", className, ...rest }) {
|
|
46
|
-
|
|
45
|
+
// Live-measured (app.attio.com/<ws>/home, /companies,
|
|
46
|
+
// /company/<id>/overview): a plain rounded divider, not a chevron -- a
|
|
47
|
+
// symmetric shape needs no RTL mirroring, unlike the ChevronRight this
|
|
48
|
+
// used to default to.
|
|
49
|
+
const sep = separator ?? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-breadcrumb-sep-dot", "aria-hidden": "true" }));
|
|
47
50
|
return ((0, jsx_runtime_1.jsx)("nav", { "aria-label": ariaLabel, className: (0, breadcrumb_recipe_1.breadcrumbRecipe)({ className }), ...rest, children: items.map((item, i) => {
|
|
48
51
|
const last = i === items.length - 1;
|
|
49
52
|
const content = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [item.icon, item.label] }));
|
|
@@ -3,7 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.breadcrumbRecipe = void 0;
|
|
4
4
|
const core_1 = require("../../core");
|
|
5
5
|
const tokens_1 = require("../../tokens");
|
|
6
|
-
// A breadcrumb trail — record-page nav ("Companies
|
|
6
|
+
// A breadcrumb trail — record-page nav ("Companies · Google"), the segments
|
|
7
|
+
// joined by a small rounded divider (crisp#498), not the literal "/" this
|
|
8
|
+
// comment's own example used to imply.
|
|
7
9
|
const config = (0, core_1.defineRecipe)({
|
|
8
10
|
name: "breadcrumb",
|
|
9
11
|
base: `display: flex; align-items: center; gap: ${tokens_1.contract.space["0.5"]}; font-family: ${tokens_1.contract.font.sans}; font-size: ${tokens_1.contract.text.lg};
|
|
@@ -12,7 +14,8 @@ const config = (0, core_1.defineRecipe)({
|
|
|
12
14
|
& a.crisp-breadcrumb-item:hover, & button.crisp-breadcrumb-item:hover { color: ${tokens_1.contract.fgPrimary}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 5%, transparent); }
|
|
13
15
|
& button.crisp-breadcrumb-item { border: none; background: none; font: inherit; cursor: pointer; }
|
|
14
16
|
& .crisp-breadcrumb-item[aria-current] { color: ${tokens_1.contract.fgPrimary}; font-weight: ${tokens_1.contract.weight.medium}; }
|
|
15
|
-
& .crisp-breadcrumb-sep { flex-shrink: 0;
|
|
17
|
+
& .crisp-breadcrumb-sep { flex-shrink: 0; display: inline-flex; }
|
|
18
|
+
& .crisp-breadcrumb-sep-dot { flex-shrink: 0; width: 3px; height: 10px; border-radius: ${tokens_1.contract.radius.full}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 28%, transparent); }
|
|
16
19
|
${core_1.rtlFlipCss}
|
|
17
20
|
`,
|
|
18
21
|
variants: {},
|
|
@@ -28,7 +28,12 @@ const spec = (0, core_1.defineSpec)({
|
|
|
28
28
|
decls: {
|
|
29
29
|
background: tokens_1.contract.bgBrandSolid,
|
|
30
30
|
color: tokens_1.contract.fgOnBrand,
|
|
31
|
-
|
|
31
|
+
// crisp#494: the inset ring is a black tint, not white -- live-
|
|
32
|
+
// measured on Attio's own "New Company" button as
|
|
33
|
+
// rgba(0,0,0,0.06), not the rgba(255,255,255,0.07) both solid
|
|
34
|
+
// intents shared. The other two shadow layers already matched
|
|
35
|
+
// exactly.
|
|
36
|
+
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)",
|
|
32
37
|
},
|
|
33
38
|
parts: [
|
|
34
39
|
{ selector: "&:hover", decls: { background: tokens_1.contract.bgBrandSolidPressed } },
|
|
@@ -50,7 +55,10 @@ const spec = (0, core_1.defineSpec)({
|
|
|
50
55
|
decls: {
|
|
51
56
|
background: tokens_1.contract.bgDangerSolid,
|
|
52
57
|
color: tokens_1.contract.fgOnBrand,
|
|
53
|
-
|
|
58
|
+
// Same corrected inset ring as primary (crisp#494) -- shared
|
|
59
|
+
// literal, same solid-fill button family; the coloured layers
|
|
60
|
+
// themselves weren't independently re-measured this round.
|
|
61
|
+
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)",
|
|
54
62
|
},
|
|
55
63
|
parts: [
|
|
56
64
|
{ selector: "&:hover", decls: { filter: "brightness(0.96)" } },
|
|
@@ -201,7 +201,7 @@ function formatCell(type, v, symbol, locale = "en-US") {
|
|
|
201
201
|
// silently drop the cell's data. Each branch decides for itself where a
|
|
202
202
|
// display string belongs -- as an entity's name, a status' label -- while the
|
|
203
203
|
// raw value keeps driving structure and tone.
|
|
204
|
-
function renderTyped(col, value, avatarTone, display, locale = "en-US") {
|
|
204
|
+
function renderTyped(col, value, avatarTone, row, display, locale = "en-US") {
|
|
205
205
|
// Attio's own empty cell is genuinely blank -- no placeholder character at
|
|
206
206
|
// all, for any column type (#412).
|
|
207
207
|
const empty = null;
|
|
@@ -213,7 +213,14 @@ function renderTyped(col, value, avatarTone, display, locale = "en-US") {
|
|
|
213
213
|
const e = display ? { ...raw, name: display } : raw;
|
|
214
214
|
if (!e.name)
|
|
215
215
|
return empty;
|
|
216
|
-
return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-entity", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-avatar", "data-tone": avatarTone === "hashed" ? toneFor(raw.name) : undefined, children: e.avatar ? ((0, jsx_runtime_1.jsx)("img", { src: e.avatar, alt: "" })) : (initials(e.name).slice(0, 2)) }), (0, jsx_runtime_1.jsx)("
|
|
216
|
+
return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-entity", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-avatar", "data-tone": avatarTone === "hashed" ? toneFor(raw.name) : undefined, children: e.avatar ? ((0, jsx_runtime_1.jsx)("img", { src: e.avatar, alt: "" })) : (initials(e.name).slice(0, 2)) }), col.onEntityClick ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-entity-name crisp-datatable-entity-name--link", onClick: (ev) => {
|
|
217
|
+
// the cell's own click already just selects (see the
|
|
218
|
+
// TableCell onClick above) -- this must additionally stop
|
|
219
|
+
// the row's onRowClick from firing, since this name points
|
|
220
|
+
// at a DIFFERENT record than the row itself (#501).
|
|
221
|
+
ev.stopPropagation();
|
|
222
|
+
col.onEntityClick?.(row);
|
|
223
|
+
}, children: e.name })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-name", children: e.name }))] }));
|
|
217
224
|
}
|
|
218
225
|
case "tags": {
|
|
219
226
|
// asking for a format on a tag column means "render it your way" -- show
|
|
@@ -330,9 +337,47 @@ function DataReferenceEditor({ col, value, isPending, onCommit, onCancel, }) {
|
|
|
330
337
|
onCommit(v);
|
|
331
338
|
} }) }));
|
|
332
339
|
}
|
|
333
|
-
function DataTable({ columns: columnsProp, data, selectable = false, selectedIds, onSelectionChange, selectAllLabel
|
|
334
|
-
|
|
335
|
-
|
|
340
|
+
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 }) {
|
|
341
|
+
// crisp/#471: same resolution order as TableToolbar's own — an explicit
|
|
342
|
+
// prop wins, then the provider's translation of the built-in English
|
|
343
|
+
// default, then that default itself with no provider in the tree.
|
|
344
|
+
const crispMessages = (0, core_1.useCrispMessages)();
|
|
345
|
+
const selectAllLabel = (0, core_1.useCrispMessage)(selectAllLabelProp, "Select all rows");
|
|
346
|
+
const searchPlaceholder = (0, core_1.useCrispMessage)(searchPlaceholderProp, "Search…");
|
|
347
|
+
const searchLabel = (0, core_1.useCrispMessage)(searchLabelProp, "Search table");
|
|
348
|
+
// Also `React.ReactNode`-typed, same reasoning as `empty` below.
|
|
349
|
+
const addRowLabel = addRowLabelProp ?? crispMessages["New record"] ?? "New record";
|
|
350
|
+
// `empty` is typed `React.ReactNode`, not `string` — a consumer can pass a
|
|
351
|
+
// whole custom empty-state element, not just replacement text, so it
|
|
352
|
+
// can't go through `useCrispMessage` (string-only). A ReactNode override
|
|
353
|
+
// passes straight through unchanged (it's already whatever the consumer
|
|
354
|
+
// wants shown); only the built-in string default translates.
|
|
355
|
+
const empty = emptyProp ?? crispMessages["No results."] ?? "No results.";
|
|
356
|
+
const reorderRowLabel = (0, core_1.useCrispMessage)(reorderRowLabelProp, "Reorder row");
|
|
357
|
+
const sortAscendingLabel = (0, core_1.useCrispMessage)(sortAscendingLabelProp, "Sort ascending");
|
|
358
|
+
const sortDescendingLabel = (0, core_1.useCrispMessage)(sortDescendingLabelProp, "Sort descending");
|
|
359
|
+
const moveColumnLeftLabel = (0, core_1.useCrispMessage)(moveColumnLeftLabelProp, "Move left");
|
|
360
|
+
const moveColumnRightLabel = (0, core_1.useCrispMessage)(moveColumnRightLabelProp, "Move right");
|
|
361
|
+
const editColumnLabel = (0, core_1.useCrispMessage)(editColumnLabelProp, "Edit column label");
|
|
362
|
+
const hideColumnLabel = (0, core_1.useCrispMessage)(hideColumnLabelProp, "Hide from view");
|
|
363
|
+
const renameColumnLabel = (0, core_1.useCrispMessage)(renameColumnLabelProp, "Rename column");
|
|
364
|
+
const viewSettingsLabel = (0, core_1.useCrispMessage)(viewSettingsLabelProp, "View settings");
|
|
365
|
+
const columnsGroupLabel = (0, core_1.useCrispMessage)(columnsGroupLabelProp, "Columns");
|
|
366
|
+
// countLabel/selectionAnnouncement stay formatter props, same escape hatch
|
|
367
|
+
// as TableToolbar's sortTriggerLabel — word order and pluralisation rules
|
|
368
|
+
// differ too much across languages for a template. Their BUILT-IN default
|
|
369
|
+
// (when the consumer supplies neither) still translates its own static
|
|
370
|
+
// fragments through the provider, same as TableToolbar's "Sorted by".
|
|
371
|
+
const resolvedCountLabel = countLabel ??
|
|
372
|
+
((shown, total) => shown === total
|
|
373
|
+
? `${total} ${total === 1
|
|
374
|
+
? (crispMessages.row ?? "row")
|
|
375
|
+
: (crispMessages.rows ?? "rows")}`
|
|
376
|
+
: `${shown} ${crispMessages.of ?? "of"} ${total}`);
|
|
377
|
+
const resolvedSelectionAnnouncement = selectionAnnouncement ??
|
|
378
|
+
((count) => count === 0
|
|
379
|
+
? (crispMessages["Selection cleared"] ?? "Selection cleared")
|
|
380
|
+
: `${count} ${crispMessages.selected ?? "selected"}`);
|
|
336
381
|
// Two different defaults on purpose. FORMATTING falls back to "en-US", which
|
|
337
382
|
// is what the built-in date/number/currency renderers have always produced,
|
|
338
383
|
// so no existing output moves. SORTING falls back to `undefined`, which is
|
|
@@ -426,7 +471,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
426
471
|
const [selectionText, setSelectionText] = React.useState("");
|
|
427
472
|
if (prevSelectedCount.current !== selected.size) {
|
|
428
473
|
prevSelectedCount.current = selected.size;
|
|
429
|
-
setSelectionText(
|
|
474
|
+
setSelectionText(resolvedSelectionAnnouncement(selected.size));
|
|
430
475
|
}
|
|
431
476
|
const [editing, setEditing] = React.useState(null);
|
|
432
477
|
// Set only while an async `onEdit` is in flight (#385) -- a sync `onEdit`
|
|
@@ -772,7 +817,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
772
817
|
return dir === "asc" ? ((0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUp, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDown, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 }));
|
|
773
818
|
return ((0, jsx_runtime_1.jsx)(lucide_react_1.ChevronsUpDown, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 }));
|
|
774
819
|
}
|
|
775
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: gridRef, className: (0, datatable_recipe_1.datatableRecipe)({ className }), tabIndex: -1, onKeyDown: onGridKeyDown, ...rest, children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-live", role: "status", "aria-live": "polite", children: selectionText }), (filterable || showCount || toolbar || columnMenu) && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-datatable-toolbar", children: [columnMenu && ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "start", trigger: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datatable-viewsettings", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.SlidersHorizontal, { size: 14, strokeWidth: 1.75 }),
|
|
820
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: gridRef, className: (0, datatable_recipe_1.datatableRecipe)({ className }), tabIndex: -1, onKeyDown: onGridKeyDown, ...rest, children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-live", role: "status", "aria-live": "polite", children: selectionText }), (filterable || showCount || toolbar || columnMenu) && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-datatable-toolbar", children: [columnMenu && ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "start", trigger: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datatable-viewsettings", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.SlidersHorizontal, { size: 14, strokeWidth: 1.75 }), viewSettingsLabel] }), children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-group-label", children: columnsGroupLabel }), allColumns.map((c) => ((0, jsx_runtime_1.jsxs)(menu_1.MenuItem, { onSelect: () => c.hidden ? showColumn(c.key) : hideColumn(c.key), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 14, strokeWidth: 2, style: { opacity: c.hidden ? 0 : 1 } }), typeof c.header === "string" ? c.header : c.key] }, c.key)))] })), filterable && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("input", { type: "search", placeholder: searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), "aria-label": searchLabel })] })), showCount && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-count", children: resolvedCountLabel(rows.length, data.length) })), toolbar] })), (0, jsx_runtime_1.jsxs)(table_1.Table, { interactive: interactive, surface: surface, style: { minWidth }, scrollProps: {
|
|
776
821
|
ref: scrollRef,
|
|
777
822
|
onScroll: (e) => {
|
|
778
823
|
const el = e.currentTarget;
|
|
@@ -802,7 +847,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
802
847
|
const inner = ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-th-inner", children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 16, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-th-label", children: col.header }), col.sortable ? (0, jsx_runtime_1.jsx)(ChevronFor, { colKey: col.key }) : null] }));
|
|
803
848
|
return ((0, jsx_runtime_1.jsxs)(table_1.TableHead, { align: alignFor(col), "aria-sort": ariaSort, ...dataStickyCol(i), children: [renaming === col.key ? ((0, jsx_runtime_1.jsx)("input", {
|
|
804
849
|
// biome-ignore lint/a11y/noAutofocus: the rename item opens this field
|
|
805
|
-
autoFocus: true, className: "crisp-datatable-th-rename", defaultValue: typeof col.header === "string" ? col.header : "", "aria-label":
|
|
850
|
+
autoFocus: true, className: "crisp-datatable-th-rename", defaultValue: typeof col.header === "string" ? col.header : "", "aria-label": renameColumnLabel, onBlur: (e) => {
|
|
806
851
|
renameColumn(col.key, e.currentTarget.value);
|
|
807
852
|
setRenaming(null);
|
|
808
853
|
}, onKeyDown: (e) => {
|
|
@@ -814,7 +859,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
814
859
|
setRenaming(null);
|
|
815
860
|
} })) : columnMenu ? ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "start", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), "aria-label": `${typeof col.header === "string"
|
|
816
861
|
? col.header
|
|
817
|
-
: col.key} column options`, children: inner }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setSort({ key: col.key, dir: "asc" }), children:
|
|
862
|
+
: col.key} ${crispMessages["column options"] ?? "column options"}`, children: inner }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setSort({ key: col.key, dir: "asc" }), children: sortAscendingLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setSort({ key: col.key, dir: "desc" }), children: sortDescendingLabel }), col.managed === false ? null : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => moveColumn(col.key, -1), children: moveColumnLeftLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => moveColumn(col.key, 1), children: moveColumnRightLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setRenaming(col.key), children: editColumnLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => hideColumn(col.key), children: hideColumnLabel })] }))] })) : col.sortable ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), onClick: () => cycleSort(col.key), children: inner })) : (inner), col.resizable !== false && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-resize", "data-resizing": resizingKey === col.key ? "" : undefined, onPointerDown: (e) => startResize(col, e), onPointerMove: moveResize, onPointerUp: endResize }))] }, col.key));
|
|
818
863
|
})] }) }), (0, jsx_runtime_1.jsx)(table_1.TableBody, { children: error ? ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { colSpan: totalCols, children: (0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-empty", children: error }) }) })) : rows.length === 0 ? ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { colSpan: totalCols, children: (0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-empty", children: empty }) }) })) : (groupedRows.map(({ group, rows: groupRows }) => ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [group !== null && ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsxs)(table_1.TableCell, { colSpan: totalCols, className: "crisp-datatable-group", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-group-label", children: group }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-group-count", children: groupRows.length })] }) })), groupRows.map((row) => ((0, jsx_runtime_1.jsxs)(table_1.TableRow, { "data-selected": selected.has(row.id) ? "" : undefined, "data-drop-target": showGrip &&
|
|
819
864
|
dragRowId &&
|
|
820
865
|
dragRowId !== row.id &&
|
|
@@ -912,7 +957,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
912
957
|
? () => openEditing(row.id, col.key)
|
|
913
958
|
: undefined, children: [col.render
|
|
914
959
|
? col.render(value, row)
|
|
915
|
-
: renderTyped(col, value, avatarTone, col.format?.(value, row), locale), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
960
|
+
: renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
916
961
|
})] }, row.id)))] }, group ?? "__ungrouped")))) }), (hasSummary || onAddRow) && ((0, jsx_runtime_1.jsxs)(table_1.TableFooter, { children: [hasSummary && ((0, jsx_runtime_1.jsxs)(table_1.TableRow, { children: [showGrip && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-2) }), selectable && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-1) }), columns.map((col, i) => ((0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), ...dataStickyCol(i), children: col.summary ? col.summary(rows) : null }, col.key)))] })), onAddRow && ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { className: "crisp-datatable-addcell", colSpan: totalCols, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datatable-addrow", onClick: onAddRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), addRowLabel] }) }) }))] }))] })] }));
|
|
917
962
|
}
|
|
918
963
|
DataTable.displayName = "DataTable";
|
|
@@ -124,6 +124,8 @@ const config = (0, core_1.defineRecipe)({
|
|
|
124
124
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
125
125
|
& .crisp-datatable-entity { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; min-width: 0; color: ${tokens_1.contract.fgPrimary}; font-weight: ${tokens_1.contract.weight.medium}; }
|
|
126
126
|
& .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
127
|
+
& .crisp-datatable-entity:hover .crisp-datatable-entity-name { text-decoration: underline; }
|
|
128
|
+
& 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%; }
|
|
127
129
|
& .crisp-datatable-avatar { width: ${tokens_1.contract.size.icon}; height: ${tokens_1.contract.size.icon}; border-radius: ${tokens_1.contract.radius.sm}; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: ${tokens_1.contract.weight.semibold}; text-transform: uppercase; color: ${tokens_1.contract.fgOnBrand}; background: ${tokens_1.contract.bgBrandSolid}; }
|
|
128
130
|
& .crisp-datatable-avatar[data-tone="success"] { background: ${tokens_1.contract.bgSuccess}; } & .crisp-datatable-avatar[data-tone="warning"] { background: ${tokens_1.contract.bgWarning}; } & .crisp-datatable-avatar[data-tone="danger"] { background: ${tokens_1.contract.bgDanger}; } & .crisp-datatable-avatar[data-tone="purple"] { background: ${tokens_1.contract.bgPurple}; } & .crisp-datatable-avatar[data-tone="cyan"] { background: ${tokens_1.contract.bgCyan}; } & .crisp-datatable-avatar[data-tone="orange"] { background: ${tokens_1.contract.bgOrange}; }
|
|
129
131
|
& .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
|
|
@@ -37,6 +37,7 @@ exports.DatePickerField = DatePickerField;
|
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const lucide_react_1 = require("lucide-react");
|
|
39
39
|
const React = __importStar(require("react"));
|
|
40
|
+
const core_1 = require("../../core");
|
|
40
41
|
const datepicker_1 = require("../datepicker");
|
|
41
42
|
const popover_1 = require("../popover");
|
|
42
43
|
const datepickerfield_recipe_1 = require("./datepickerfield.recipe");
|
|
@@ -57,7 +58,10 @@ const fromDate = (d) => ({
|
|
|
57
58
|
* popover. This is what `DatePicker` used to be; `DatePicker` is now
|
|
58
59
|
* seed-design's bare calendar, and this composes it.
|
|
59
60
|
*/
|
|
60
|
-
function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder
|
|
61
|
+
function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder: placeholderProp, variant = "default", format = defaultFormat, clearable = true, clearLabel: clearLabelProp, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }) {
|
|
62
|
+
// crisp/#471: same resolution order as Select's own.
|
|
63
|
+
const placeholder = (0, core_1.useCrispMessage)(placeholderProp, "Set date…");
|
|
64
|
+
const clearLabel = (0, core_1.useCrispMessage)(clearLabelProp, "Clear date");
|
|
61
65
|
const [internal, setInternal] = React.useState(defaultValue ?? null);
|
|
62
66
|
const selected = value ?? internal;
|
|
63
67
|
const [open, setOpen] = React.useState(false);
|
|
@@ -60,7 +60,8 @@ function useDialogInstanceId() {
|
|
|
60
60
|
* Escape / outside-click dismissal) with the measured modal surface.
|
|
61
61
|
* Compose the body with `DialogBody` and actions with `DialogFooter`.
|
|
62
62
|
*/
|
|
63
|
-
function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ariaLabel, closeLabel
|
|
63
|
+
function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ariaLabel, closeLabel: closeLabelProp, children, className, style, "aria-describedby": describedBy, ...rest }) {
|
|
64
|
+
const closeLabel = (0, core_1.useCrispMessage)(closeLabelProp, "Close");
|
|
64
65
|
const id = useDialogInstanceId();
|
|
65
66
|
const service = (0, react_1.useMachine)(dialog.machine, {
|
|
66
67
|
id,
|
|
@@ -57,7 +57,8 @@ function useDrawerInstanceId() {
|
|
|
57
57
|
* Escape / outside-click dismissal) with a panel that slides in from the edge.
|
|
58
58
|
* Compose the body with `DrawerBody` and actions with `DrawerFooter`.
|
|
59
59
|
*/
|
|
60
|
-
function Drawer({ open, onOpenChange, side = "right", title, icon, "aria-label": ariaLabel, closeLabel
|
|
60
|
+
function Drawer({ open, onOpenChange, side = "right", title, icon, "aria-label": ariaLabel, closeLabel: closeLabelProp, children, className, "aria-describedby": describedBy, ...rest }) {
|
|
61
|
+
const closeLabel = (0, core_1.useCrispMessage)(closeLabelProp, "Close");
|
|
61
62
|
const id = useDrawerInstanceId();
|
|
62
63
|
const service = (0, react_1.useMachine)(dialog.machine, {
|
|
63
64
|
id,
|
|
@@ -69,7 +69,11 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, n
|
|
|
69
69
|
// The id belongs on the trigger, not the wrapper: `<label for>` only names a
|
|
70
70
|
// LABELABLE element, so an id left on the root div meant a FormField label
|
|
71
71
|
// above this control named nothing (#247). Select already did this.
|
|
72
|
-
id, placeholder
|
|
72
|
+
id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPlaceholderProp, emptyLabel: emptyLabelProp, disabled, invalid, className, "aria-label": ariaLabel, ...rest }) {
|
|
73
|
+
// crisp/#471: same resolution order as Select's own.
|
|
74
|
+
const placeholder = (0, core_1.useCrispMessage)(placeholderProp, "Select…");
|
|
75
|
+
const searchPlaceholder = (0, core_1.useCrispMessage)(searchPlaceholderProp, "Search…");
|
|
76
|
+
const emptyLabel = (0, core_1.useCrispMessage)(emptyLabelProp, "No results");
|
|
73
77
|
// Virtual focus: DOM focus stays on the trigger (or the search field) and the
|
|
74
78
|
// active option is named by `aria-activedescendant`. `data-highlighted` is a
|
|
75
79
|
// styling hook only -- a screen reader cannot see it.
|
|
@@ -91,7 +91,10 @@ function IncrementIcon({ size }) {
|
|
|
91
91
|
* Slider documents. Here it also decides child ORDER, since the engine
|
|
92
92
|
* reverses its visible children rather than relying on CSS.
|
|
93
93
|
*/
|
|
94
|
-
function QuantityPicker({ size = "medium", fill = false, className, name, decrementLabel
|
|
94
|
+
function QuantityPicker({ size = "medium", fill = false, className, name, decrementLabel: decrementLabelProp, incrementLabel: incrementLabelProp, ...props }) {
|
|
95
|
+
// crisp/#471: same resolution order as Select's own.
|
|
96
|
+
const decrementLabel = (0, core_1.useCrispMessage)(decrementLabelProp, "Decrease quantity");
|
|
97
|
+
const incrementLabel = (0, core_1.useCrispMessage)(incrementLabelProp, "Increase quantity");
|
|
95
98
|
// A `<label for>` cannot name a div, and this root is a role="group" (#247).
|
|
96
99
|
const rootRef = React.useRef(null);
|
|
97
100
|
(0, core_1.useLabelAssociation)(rootRef);
|
|
@@ -52,7 +52,13 @@ function optionText(o) {
|
|
|
52
52
|
* "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
|
|
53
53
|
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
54
54
|
*/
|
|
55
|
-
function Select({ options, value, defaultValue, onValueChange, name, required, placeholder
|
|
55
|
+
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 }) {
|
|
56
|
+
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
57
|
+
// CrispMessagesProvider's translation of the English default, or that
|
|
58
|
+
// default itself with no provider — unchanged from before this existed.
|
|
59
|
+
const placeholder = (0, core_1.useCrispMessage)(placeholderProp, "Select…");
|
|
60
|
+
const searchPlaceholder = (0, core_1.useCrispMessage)(searchPlaceholderProp, "Search…");
|
|
61
|
+
const emptyLabel = (0, core_1.useCrispMessage)(emptyLabelProp, "No results");
|
|
56
62
|
// Virtual focus: DOM focus stays on the trigger (or the search field), and the
|
|
57
63
|
// active option is named by `aria-activedescendant`. That is the only thing a
|
|
58
64
|
// screen reader has to go on -- `data-highlighted` is invisible to it -- so the
|
|
@@ -8,8 +8,9 @@ const statcard_recipe_1 = require("./statcard.recipe");
|
|
|
8
8
|
* A single-metric KPI tile — measured from a reports number/insight card.
|
|
9
9
|
* Label, a large value, and an optional up/down delta pill with a caption.
|
|
10
10
|
*/
|
|
11
|
-
function StatCard({ label, icon, value, delta, deltaSuffix = "%", caption, card, className, ...rest }) {
|
|
11
|
+
function StatCard({ label, icon, value, delta, deltaSuffix = "%", caption, captionTone, card, className, ...rest }) {
|
|
12
12
|
const trend = delta == null ? null : delta > 0 ? "up" : delta < 0 ? "down" : "flat";
|
|
13
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, statcard_recipe_1.statcardRecipe)({ card, className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-statcard-label", children: [icon, label] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-statcard-value", children: value }), (trend != null || caption != null) && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-statcard-foot", children: [trend != null && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-statcard-delta", "data-trend": trend, children: [trend === "up" && (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUp, { size: 12, strokeWidth: 2.5 }), trend === "down" && (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDown, { size: 12, strokeWidth: 2.5 }), Math.abs(delta), deltaSuffix] })), caption
|
|
13
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, statcard_recipe_1.statcardRecipe)({ card, className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-statcard-label", children: [icon, label] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-statcard-value", children: value }), (trend != null || caption != null) && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-statcard-foot", children: [trend != null && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-statcard-delta", "data-trend": trend, children: [trend === "up" && (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUp, { size: 12, strokeWidth: 2.5 }), trend === "down" && (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDown, { size: 12, strokeWidth: 2.5 }), Math.abs(delta), deltaSuffix] })), caption != null &&
|
|
14
|
+
(captionTone ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-statcard-caption", "data-tone": captionTone, children: caption })) : (caption))] }))] }));
|
|
14
15
|
}
|
|
15
16
|
StatCard.displayName = "StatCard";
|
|
@@ -14,6 +14,16 @@ const config = (0, core_1.defineRecipe)({
|
|
|
14
14
|
& .crisp-statcard-label svg { color: ${tokens_1.contract.fgMuted}; }
|
|
15
15
|
& .crisp-statcard-value { font-size: 28px; line-height: 32px; font-weight: ${tokens_1.contract.weight.semibold}; letter-spacing: -0.4px; color: ${tokens_1.contract.fgPrimary}; }
|
|
16
16
|
& .crisp-statcard-foot { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.fgMuted}; }
|
|
17
|
+
/* caption's own status colour, opt-in via data-tone: not a new measured
|
|
18
|
+
value — the same fg*Soft tokens Badge's tone already uses (and Badge's
|
|
19
|
+
own doc comment records why: a dedicated soft-text token, not the
|
|
20
|
+
tone's accent colour, is the one that clears AA). No "ok" reading on
|
|
21
|
+
Badge itself; fgSuccessSoft is Badge's own "success" tone's text
|
|
22
|
+
colour, reused here under the name a caption's own good-news state
|
|
23
|
+
reads as. */
|
|
24
|
+
& .crisp-statcard-caption[data-tone="ok"] { color: ${tokens_1.contract.fgSuccessSoft}; }
|
|
25
|
+
& .crisp-statcard-caption[data-tone="warning"] { color: ${tokens_1.contract.fgWarningSoft}; }
|
|
26
|
+
& .crisp-statcard-caption[data-tone="critical"] { color: ${tokens_1.contract.fgDangerSoft}; }
|
|
17
27
|
& .crisp-statcard-delta { display: inline-flex; align-items: center; gap: 2px; height: 20px; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.sm}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; }
|
|
18
28
|
& .crisp-statcard-delta svg { flex-shrink: 0; }
|
|
19
29
|
& .crisp-statcard-delta[data-trend="up"] { background: color-mix(in srgb, ${tokens_1.contract.bgSuccess} 16%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgSuccess} 80%, ${tokens_1.contract.fgPrimary}); }
|
|
@@ -156,7 +156,33 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
|
|
|
156
156
|
* this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
|
|
157
157
|
* Created by), which one shared list can't reproduce.
|
|
158
158
|
*/
|
|
159
|
-
function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel
|
|
159
|
+
function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, ...rest }) {
|
|
160
|
+
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
161
|
+
// CrispMessagesProvider's translation of the English default below, or
|
|
162
|
+
// that default itself with no provider — unchanged from before this
|
|
163
|
+
// existed. One resolved constant per prop, same name every existing usage
|
|
164
|
+
// below already reads, so nothing past this block changes.
|
|
165
|
+
const sortLabel = (0, core_1.useCrispMessage)(sortLabelProp, "Sort");
|
|
166
|
+
const filterLabel = (0, core_1.useCrispMessage)(filterLabelProp, "Filter");
|
|
167
|
+
const reorderSortLabel = (0, core_1.useCrispMessage)(reorderSortLabelProp, "Reorder sort");
|
|
168
|
+
const noSortsLabel = (0, core_1.useCrispMessage)(noSortsLabelProp, "No sorts applied to this view.");
|
|
169
|
+
const addSortLabel = (0, core_1.useCrispMessage)(addSortLabelProp, "Add sort");
|
|
170
|
+
const learnSortingLabel = (0, core_1.useCrispMessage)(learnSortingLabelProp, "Learn about sorting");
|
|
171
|
+
const selectConditionLabel = (0, core_1.useCrispMessage)(selectConditionLabelProp, "Select condition");
|
|
172
|
+
const sortFieldLabel = (0, core_1.useCrispMessage)(sortFieldLabelProp, "Sort field");
|
|
173
|
+
const sortDirectionLabel = (0, core_1.useCrispMessage)(sortDirectionLabelProp, "Sort direction");
|
|
174
|
+
const removeSortLabel = (0, core_1.useCrispMessage)(removeSortLabelProp, "Remove sort");
|
|
175
|
+
const filterFieldLabel = (0, core_1.useCrispMessage)(filterFieldLabelProp, "Filter by field");
|
|
176
|
+
const filterFieldPlaceholder = (0, core_1.useCrispMessage)(filterFieldPlaceholderProp, "Filter by…");
|
|
177
|
+
const filterValuePlaceholder = (0, core_1.useCrispMessage)(filterValuePlaceholderProp, "Value…");
|
|
178
|
+
const ascendingLabel = (0, core_1.useCrispMessage)(ascendingLabelProp, "Ascending");
|
|
179
|
+
const descendingLabel = (0, core_1.useCrispMessage)(descendingLabelProp, "Descending");
|
|
180
|
+
// OP_LABELS' own words ("contains", "is", "is any of", …) have no prop of
|
|
181
|
+
// their own — a whole lookup table, not one string — so they translate
|
|
182
|
+
// straight from the provider's dictionary at the two spots they render
|
|
183
|
+
// (#628, #673 below), same fallback order as every resolved constant
|
|
184
|
+
// above.
|
|
185
|
+
const crispMessages = (0, core_1.useCrispMessages)();
|
|
160
186
|
// Each side falls back to the shared `columns` independently — a caller
|
|
161
187
|
// that only overrides one (Filter's own Record/Created by, say) still
|
|
162
188
|
// gets `columns` on the other, unchanged from before either prop existed.
|
|
@@ -245,12 +271,18 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
245
271
|
};
|
|
246
272
|
const activeFilters = filters.length;
|
|
247
273
|
// The default phrase only works in English word order, so a caller can
|
|
248
|
-
// replace the whole thing rather than fill a template
|
|
274
|
+
// replace the whole thing rather than fill a template — crisp/#471's
|
|
275
|
+
// `sortTriggerLabel` formatter is exactly that escape hatch. Short of a
|
|
276
|
+
// full formatter, the "Sorted by" fragment itself still translates (the
|
|
277
|
+
// sort trigger reading "Sort" in Mongolian and "Sorted by X" in English is
|
|
278
|
+
// worse than an imperfect word order); `crispMessages` rather than a
|
|
279
|
+
// second `useCrispMessage` call, since this only fires on the branch that
|
|
280
|
+
// needs it.
|
|
249
281
|
const sortedLabel = sortTriggerLabel
|
|
250
282
|
? sortTriggerLabel(sorts, sortColumns)
|
|
251
283
|
: sorts.length
|
|
252
|
-
?
|
|
253
|
-
:
|
|
284
|
+
? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
|
|
285
|
+
: sortLabel;
|
|
254
286
|
return ((0, jsx_runtime_1.jsx)("div", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && ((0, jsx_runtime_1.jsxs)("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
255
287
|
if (dragSort !== null)
|
|
256
288
|
e.preventDefault();
|
|
@@ -276,8 +308,8 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
276
308
|
], "aria-label": sortDirectionLabel }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-foot", children: (0, jsx_runtime_1.jsxs)("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost${activeFilters ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "filter" ? null : "filter"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
|
|
277
309
|
? filterTriggerLabel(activeFilters)
|
|
278
310
|
: activeFilters
|
|
279
|
-
?
|
|
280
|
-
:
|
|
311
|
+
? `${filterLabel} · ${activeFilters}`
|
|
312
|
+
: filterLabel] }), openMenu === "filter" && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterFieldPlaceholder, "aria-label": filterFieldLabel,
|
|
281
313
|
// biome-ignore lint/a11y/noAutofocus: focus the picker on open
|
|
282
314
|
autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), filterColumns
|
|
283
315
|
.filter((col) => col.label
|
|
@@ -288,7 +320,8 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
288
320
|
const open = openMenu === `fcond:${cond.id}`;
|
|
289
321
|
const hasValue = (!opNeedsValue(cond.op) || cond.value.trim()) &&
|
|
290
322
|
(!opNeedsValues(cond.op) || (cond.values?.length ?? 0) > 0);
|
|
291
|
-
return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-fchip", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-fchip-main", "data-menu-trigger": true, "aria-haspopup": "dialog", onClick: () => setOpenMenu(open ? null : `fcond:${cond.id}`), children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-field", children: col?.label }), hasValue ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: OP_LABELS[cond.op]
|
|
323
|
+
return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-fchip", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-fchip-main", "data-menu-trigger": true, "aria-haspopup": "dialog", onClick: () => setOpenMenu(open ? null : `fcond:${cond.id}`), children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-field", children: col?.label }), hasValue ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: crispMessages[OP_LABELS[cond.op]] ??
|
|
324
|
+
OP_LABELS[cond.op] }), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: cond.value })), cond.op === "isanyof" && cond.values && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: isAnyOfSummary(cond.values) }))] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: selectConditionLabel }))] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fchip-x", "aria-label": `${crispMessages.Remove ?? "Remove"} ${col?.label} ${crispMessages.filter ?? "filter"}`, onClick: () => removeFilter(cond.id), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 2 }) })] }), open && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu-head", children: [col?.label, " ", crispMessages.condition ?? "condition"] }), opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(cond.id, { op }), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
|
|
292
325
|
// biome-ignore lint/a11y/noAutofocus: focus the value field
|
|
293
326
|
autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })), cond.op === "isanyof" && col?.values && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
|
|
294
327
|
// biome-ignore lint/a11y/noAutofocus: focus the search field
|
|
@@ -59,7 +59,9 @@ function ToastCard({ item, onClose, dismissLabel, }) {
|
|
|
59
59
|
* anywhere to push a notification. Toasts stack bottom-right and auto-dismiss
|
|
60
60
|
* after `duration` (default 4s; 0 keeps them until dismissed).
|
|
61
61
|
*/
|
|
62
|
-
function ToastProvider({ children, duration = 4000, dismissLabel
|
|
62
|
+
function ToastProvider({ children, duration = 4000, dismissLabel: dismissLabelProp, regionLabel: regionLabelProp, }) {
|
|
63
|
+
const dismissLabel = (0, core_1.useCrispMessage)(dismissLabelProp, "Dismiss");
|
|
64
|
+
const regionLabel = (0, core_1.useCrispMessage)(regionLabelProp, "Notifications");
|
|
63
65
|
const [items, setItems] = React.useState([]);
|
|
64
66
|
const idRef = React.useRef(0);
|
|
65
67
|
const timers = React.useRef(new Map());
|