@crisp-ui-kit/crisp 0.53.5 → 0.54.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/activityfeed.css +12 -0
- package/dist/activityfeed.d.ts +7 -0
- package/dist/activityfeed.layered.css +14 -0
- package/dist/badge.css +3 -0
- package/dist/badge.d.ts +2 -2
- package/dist/badge.layered.css +3 -0
- package/dist/cjs/components/badge/badge.recipe.js +10 -0
- package/dist/cjs/components/bulkbar/bulkbar.js +5 -1
- package/dist/cjs/components/fileupload/fileupload.js +19 -3
- package/dist/cjs/components/formfield/formfield.js +8 -1
- package/dist/cjs/components/heatmapchart/heatmapchart.js +14 -2
- package/dist/cjs/components/skiplink/index.js +17 -0
- package/dist/cjs/components/skiplink/skiplink.js +16 -0
- package/dist/cjs/components/skiplink/skiplink.recipe.js +33 -0
- package/dist/cjs/components/textlink/index.js +7 -0
- package/dist/cjs/components/textlink/textlink.js +16 -0
- package/dist/cjs/components/textlink/textlink.recipe.js +35 -0
- package/dist/cjs/components/visuallyhidden/index.js +17 -0
- package/dist/cjs/components/visuallyhidden/visuallyhidden.js +16 -0
- package/dist/cjs/components/visuallyhidden/visuallyhidden.recipe.js +20 -0
- package/dist/cjs/index.js +5 -0
- package/dist/cjs/product/activityfeed/activityfeed.js +38 -0
- package/dist/cjs/product/activityfeed/activityfeed.recipe.js +35 -0
- package/dist/cjs/product/activityfeed/index.js +10 -0
- package/dist/cjs/product/board/board.js +11 -2
- package/dist/cjs/product/command/command.js +13 -1
- package/dist/cjs/product/flowcanvas/flowcanvas.js +9 -2
- package/dist/cjs/product/headerbar/headerbar.js +15 -0
- package/dist/cjs/product/headerbar/headerbar.recipe.js +35 -0
- package/dist/cjs/product/headerbar/index.js +17 -0
- package/dist/cjs/product/inbox/inbox.js +13 -1
- package/dist/cjs/product/onboarding/onboarding.js +22 -1
- package/dist/cjs/product/recordpage/index.js +2 -1
- package/dist/cjs/product/recordpage/recordpage.js +197 -123
- package/dist/cjs/product/recordpage/recordpage.recipe.js +5 -0
- package/dist/cjs/product/richtext/richtext.js +43 -0
- package/dist/cjs/product/tasklist/tasklist.js +8 -1
- package/dist/cjs/tokens/palette.js +14 -0
- package/dist/cjs/tokens/semantic.js +5 -0
- package/dist/esm/components/badge/badge.recipe.js +10 -0
- package/dist/esm/components/bulkbar/bulkbar.js +5 -1
- package/dist/esm/components/fileupload/fileupload.js +20 -4
- package/dist/esm/components/formfield/formfield.js +8 -1
- package/dist/esm/components/heatmapchart/heatmapchart.js +14 -2
- package/dist/esm/components/skiplink/index.js +1 -0
- package/dist/esm/components/skiplink/skiplink.js +13 -0
- package/dist/esm/components/skiplink/skiplink.recipe.js +30 -0
- package/dist/esm/components/textlink/index.js +2 -0
- package/dist/esm/components/textlink/textlink.js +13 -0
- package/dist/esm/components/textlink/textlink.recipe.js +32 -0
- package/dist/esm/components/visuallyhidden/index.js +1 -0
- package/dist/esm/components/visuallyhidden/visuallyhidden.js +13 -0
- package/dist/esm/components/visuallyhidden/visuallyhidden.recipe.js +17 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/product/activityfeed/activityfeed.js +32 -0
- package/dist/esm/product/activityfeed/activityfeed.recipe.js +32 -0
- package/dist/esm/product/activityfeed/index.js +2 -0
- package/dist/esm/product/board/board.js +12 -3
- package/dist/esm/product/command/command.js +14 -2
- package/dist/esm/product/flowcanvas/flowcanvas.js +9 -2
- package/dist/esm/product/headerbar/headerbar.js +12 -0
- package/dist/esm/product/headerbar/headerbar.recipe.js +32 -0
- package/dist/esm/product/headerbar/index.js +1 -0
- package/dist/esm/product/inbox/inbox.js +13 -1
- package/dist/esm/product/onboarding/onboarding.js +22 -1
- package/dist/esm/product/recordpage/index.js +1 -1
- package/dist/esm/product/recordpage/recordpage.js +197 -124
- package/dist/esm/product/recordpage/recordpage.recipe.js +5 -0
- package/dist/esm/product/richtext/richtext.js +43 -0
- package/dist/esm/product/tasklist/tasklist.js +8 -1
- package/dist/esm/tokens/palette.js +14 -0
- package/dist/esm/tokens/semantic.js +5 -0
- package/dist/headerbar.css +7 -0
- package/dist/headerbar.d.ts +7 -0
- package/dist/headerbar.layered.css +9 -0
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +16 -4
- package/dist/recordpage.css +17 -31
- package/dist/recordpage.layered.css +17 -31
- package/dist/skiplink.css +5 -0
- package/dist/skiplink.d.ts +7 -0
- package/dist/skiplink.layered.css +7 -0
- package/dist/styles.css +69 -31
- package/dist/styles.layered.css +69 -31
- package/dist/textlink.css +11 -0
- package/dist/textlink.d.ts +7 -0
- package/dist/textlink.layered.css +13 -0
- package/dist/tokens.css +6 -0
- package/dist/tokens.json +16 -4
- package/dist/types/components/badge/badge.d.ts +2 -0
- package/dist/types/components/badge/badge.recipe.d.ts +8 -0
- package/dist/types/components/bulkbar/bulkbar.d.ts +1 -1
- package/dist/types/components/heatmapchart/heatmapchart.d.ts +1 -1
- package/dist/types/components/skiplink/index.d.ts +1 -0
- package/dist/types/components/skiplink/skiplink.d.ts +18 -0
- package/dist/types/components/skiplink/skiplink.recipe.d.ts +3 -0
- package/dist/types/components/textlink/index.d.ts +2 -0
- package/dist/types/components/textlink/textlink.d.ts +19 -0
- package/dist/types/components/textlink/textlink.recipe.d.ts +13 -0
- package/dist/types/components/visuallyhidden/index.d.ts +1 -0
- package/dist/types/components/visuallyhidden/visuallyhidden.d.ts +15 -0
- package/dist/types/components/visuallyhidden/visuallyhidden.recipe.d.ts +3 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/product/activityfeed/activityfeed.d.ts +57 -0
- package/dist/types/product/activityfeed/activityfeed.recipe.d.ts +3 -0
- package/dist/types/product/activityfeed/index.d.ts +2 -0
- package/dist/types/product/board/board.d.ts +1 -1
- package/dist/types/product/command/command.d.ts +1 -1
- package/dist/types/product/flowcanvas/flowcanvas.d.ts +1 -1
- package/dist/types/product/headerbar/headerbar.d.ts +27 -0
- package/dist/types/product/headerbar/headerbar.recipe.d.ts +3 -0
- package/dist/types/product/headerbar/index.d.ts +1 -0
- package/dist/types/product/inbox/inbox.d.ts +1 -1
- package/dist/types/product/onboarding/onboarding.d.ts +1 -1
- package/dist/types/product/recordpage/index.d.ts +1 -1
- package/dist/types/product/recordpage/recordpage.d.ts +90 -1
- package/dist/types/product/tasklist/tasklist.d.ts +1 -1
- package/dist/types/tokens/contract.d.ts +3 -0
- package/dist/types/tokens/palette.d.ts +12 -0
- package/dist/types/tokens/semantic.d.ts +5 -0
- package/dist/visuallyhidden.css +3 -0
- package/dist/visuallyhidden.d.ts +7 -0
- package/dist/visuallyhidden.layered.css +5 -0
- package/package.json +26 -1
|
@@ -183,6 +183,14 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
|
|
|
183
183
|
// (documented React behaviour), so a plain onFocus/onBlur pair on this
|
|
184
184
|
// same wrapper sees the portal's contents correctly.
|
|
185
185
|
const mentionPickerHasFocusRef = React.useRef(false);
|
|
186
|
+
// A caret position `insertMention` just established, to be re-asserted
|
|
187
|
+
// the next time `body` regains focus (#514 fixed the picker stealing
|
|
188
|
+
// focus on OPEN; this is the same "caret resets to offset 0" mechanism
|
|
189
|
+
// on SELECT — see insertMention's own comment for why one synchronous
|
|
190
|
+
// `sel.addRange` isn't enough). One-shot: consumed and cleared by the
|
|
191
|
+
// very next focus or input, whichever comes first, so a stale pointer
|
|
192
|
+
// can never later hijack an unrelated focus.
|
|
193
|
+
const pendingCaretRef = React.useRef(null);
|
|
186
194
|
const syncEmpty = (el) => {
|
|
187
195
|
if (el)
|
|
188
196
|
el.toggleAttribute("data-empty", (el.textContent ?? "") === "");
|
|
@@ -494,6 +502,20 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
|
|
|
494
502
|
after.collapse(true);
|
|
495
503
|
sel.removeAllRanges();
|
|
496
504
|
sel.addRange(after);
|
|
505
|
+
// `select`ing a mention is usually the LAST thing the consumer's
|
|
506
|
+
// picker does before it unmounts (ActorPicker-shaped consumers call
|
|
507
|
+
// `close` right after `select`, same as this file's own doc example).
|
|
508
|
+
// A picker built on a dialog primitive (crisp's own `Command`
|
|
509
|
+
// included) commonly restores focus to whatever had it before the
|
|
510
|
+
// dialog opened as part of ITS OWN unmount cleanup — a plain
|
|
511
|
+
// `.focus()` with no Range of its own, which collapses a
|
|
512
|
+
// contentEditable's caret to offset 0. That cleanup runs after this
|
|
513
|
+
// function returns (the picker only unmounts once `closeMention`
|
|
514
|
+
// below flips `mention` to null and React commits it), so the
|
|
515
|
+
// Selection set two lines up can already be correct here and still
|
|
516
|
+
// get clobbered a tick later. Re-assert it the next time `body`
|
|
517
|
+
// actually regains focus, whenever that turns out to be.
|
|
518
|
+
pendingCaretRef.current = { node: space, offset: 1 };
|
|
497
519
|
closeMention();
|
|
498
520
|
syncEmpty(bodyRef.current);
|
|
499
521
|
emit();
|
|
@@ -558,10 +580,31 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
|
|
|
558
580
|
} })), (0, jsx_runtime_1.jsx)("div", { ref: bodyRef, className: "crisp-rte-body", contentEditable: !readOnly, suppressContentEditableWarning: true, role: "textbox", tabIndex: readOnly ? -1 : 0, "aria-multiline": "true", "aria-label": bodyLabel, "aria-readonly": readOnly || undefined, "data-placeholder": placeholder, onInput: readOnly
|
|
559
581
|
? undefined
|
|
560
582
|
: () => {
|
|
583
|
+
// A real keystroke landing here means `body` already has
|
|
584
|
+
// focus on its own terms — any caret a prior mention
|
|
585
|
+
// selection was waiting to reassert (see insertMention) is
|
|
586
|
+
// moot now and must not later jump the caret backward on
|
|
587
|
+
// some unrelated future focus.
|
|
588
|
+
pendingCaretRef.current = null;
|
|
561
589
|
syncEmpty(bodyRef.current);
|
|
562
590
|
updateSlashTrigger();
|
|
563
591
|
updateMention();
|
|
564
592
|
emit();
|
|
593
|
+
}, onFocus: readOnly
|
|
594
|
+
? undefined
|
|
595
|
+
: () => {
|
|
596
|
+
const pending = pendingCaretRef.current;
|
|
597
|
+
pendingCaretRef.current = null;
|
|
598
|
+
if (!pending?.node.isConnected)
|
|
599
|
+
return;
|
|
600
|
+
const sel = window.getSelection();
|
|
601
|
+
if (!sel)
|
|
602
|
+
return;
|
|
603
|
+
const range = document.createRange();
|
|
604
|
+
range.setStart(pending.node, pending.offset);
|
|
605
|
+
range.collapse(true);
|
|
606
|
+
sel.removeAllRanges();
|
|
607
|
+
sel.addRange(range);
|
|
565
608
|
}, onMouseUp: readOnly
|
|
566
609
|
? undefined
|
|
567
610
|
: () => {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TaskList = TaskList;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const core_1 = require("../../core");
|
|
5
6
|
const tasklist_recipe_1 = require("./tasklist.recipe");
|
|
6
7
|
function Row({ task, onToggle, completeLabel, incompleteLabel, }) {
|
|
7
8
|
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tasklist-row", "data-done": task.done ? "" : undefined, children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", className: "crisp-tasklist-check", checked: !!task.done, "aria-label": task.done ? incompleteLabel : completeLabel, onChange: () => onToggle?.(task.id, !task.done) }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tasklist-title", children: task.title }), (task.due != null || task.meta != null) && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tasklist-meta", children: [task.due != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tasklist-due", "data-overdue": task.overdue ? "" : undefined, children: task.due })), task.meta] }))] }));
|
|
@@ -11,9 +12,15 @@ function Row({ task, onToggle, completeLabel, incompleteLabel, }) {
|
|
|
11
12
|
* count, then rows of a round complete-checkbox + title + trailing meta.
|
|
12
13
|
* Completed rows strike through; overdue dates go red.
|
|
13
14
|
*/
|
|
14
|
-
function TaskList({ tasks, groups, onToggle, emptyLabel
|
|
15
|
+
function TaskList({ tasks, groups, onToggle, emptyLabel: emptyLabelProp, className, completeLabel: completeLabelProp, incompleteLabel: incompleteLabelProp, ...rest }) {
|
|
15
16
|
const resolved = groups ?? (tasks?.length ? [{ label: null, tasks }] : []);
|
|
16
17
|
const total = resolved.reduce((s, g) => s + g.tasks.length, 0);
|
|
18
|
+
// crisp/#471: `emptyLabel` is typed `React.ReactNode`, so it resolves by
|
|
19
|
+
// hand rather than through `useCrispMessage`, which is string-only.
|
|
20
|
+
const crispMessages = (0, core_1.useCrispMessages)();
|
|
21
|
+
const emptyLabel = emptyLabelProp ?? crispMessages["No tasks"] ?? "No tasks";
|
|
22
|
+
const completeLabel = (0, core_1.useCrispMessage)(completeLabelProp, "Mark complete");
|
|
23
|
+
const incompleteLabel = (0, core_1.useCrispMessage)(incompleteLabelProp, "Mark incomplete");
|
|
17
24
|
return ((0, jsx_runtime_1.jsx)("div", { className: (0, tasklist_recipe_1.tasklistRecipe)({ className }), ...rest, children: total === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tasklist-empty", children: emptyLabel })) : (resolved.map((g, gi) => ((0, jsx_runtime_1.jsxs)("div", { children: [g.label != null && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tasklist-group", children: [g.label, (0, jsx_runtime_1.jsx)("span", { className: "crisp-tasklist-group-count", children: g.tasks.length })] })), g.tasks.map((t) => ((0, jsx_runtime_1.jsx)(Row, { task: t, onToggle: onToggle, completeLabel: completeLabel, incompleteLabel: incompleteLabel }, t.id)))] }, gi)))) }));
|
|
18
25
|
}
|
|
19
26
|
TaskList.displayName = "TaskList";
|
|
@@ -100,4 +100,18 @@ exports.palette = {
|
|
|
100
100
|
// as-is per build/spec-check.mjs's own note), this token exists only for
|
|
101
101
|
// Badge's soft-neutral text, so darkening it doesn't move any other surface.
|
|
102
102
|
softNeutralText: { light: "#686870", dark: "#9a9aa2" },
|
|
103
|
+
// Badge's purple/role-pill tone (crisp#506) -- measured live off Attio's
|
|
104
|
+
// own role pill ("Admin", app.attio.com/<ws>/settings/workspace/members):
|
|
105
|
+
// background rgb(245,240,255), text rgb(98,56,181). The ring re-uses a
|
|
106
|
+
// second, independent measurement from the same session (a company's
|
|
107
|
+
// Categories tag pill sharing the identical background, "Broadcasting",
|
|
108
|
+
// app.attio.com/<ws>/companies) at rgb(232,221,254) -- same hue family,
|
|
109
|
+
// cross-validated rather than guessed. Dark mode was NOT measured (no
|
|
110
|
+
// live Attio session was reachable when this tone was added) -- these
|
|
111
|
+
// three light values are reused as-is for dark rather than an invented
|
|
112
|
+
// "brightened" guess (Lesson: don't add unmeasured "robustness"). Needs a
|
|
113
|
+
// real dark-mode measurement before this note can be removed.
|
|
114
|
+
purpleTint: { light: "#f5f0ff", dark: "#f5f0ff" },
|
|
115
|
+
softPurpleText: { light: "#6238b5", dark: "#6238b5" },
|
|
116
|
+
purpleRing: { light: "#e8ddfe", dark: "#e8ddfe" },
|
|
103
117
|
};
|
|
@@ -63,4 +63,9 @@ exports.semantic = {
|
|
|
63
63
|
fgWarningSoft: "softWarningText",
|
|
64
64
|
fgDangerSoft: "softDangerText",
|
|
65
65
|
fgNeutralSoft: "softNeutralText",
|
|
66
|
+
/** Badge's purple/role-pill tone (crisp#506) — see palette.ts's comment
|
|
67
|
+
* on `purpleTint`/`softPurpleText`/`purpleRing` for the measurement. */
|
|
68
|
+
bgPurpleSoft: "purpleTint",
|
|
69
|
+
fgPurpleSoft: "softPurpleText",
|
|
70
|
+
ringPurpleSoft: "purpleRing",
|
|
66
71
|
};
|
|
@@ -60,6 +60,16 @@ const spec = defineSpec({
|
|
|
60
60
|
color: c.fgDangerSoft,
|
|
61
61
|
boxShadow: ring(c.bgDanger, 30),
|
|
62
62
|
},
|
|
63
|
+
// crisp#506: a distinct purple used for role-style labels (Attio's own
|
|
64
|
+
// "Admin" role pill), not a status/severity colour -- the other five
|
|
65
|
+
// tones don't cover this hue. Unlike those, these are the measured
|
|
66
|
+
// *solid* values directly (not a color-mix tint of a base accent):
|
|
67
|
+
// Attio's own pill background reads a flat, opaque rgb(), no alpha.
|
|
68
|
+
purple: {
|
|
69
|
+
background: c.bgPurpleSoft,
|
|
70
|
+
color: c.fgPurpleSoft,
|
|
71
|
+
boxShadow: `inset 0 0 0 1px ${c.ringPurpleSoft}`,
|
|
72
|
+
},
|
|
63
73
|
},
|
|
64
74
|
size: {
|
|
65
75
|
small: {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { X } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useCrispMessage } from "../../core/index.js";
|
|
4
5
|
import { bulkbarRecipe } from "./bulkbar.recipe.js";
|
|
5
6
|
/**
|
|
6
7
|
* A floating bulk-action bar. Rises from the bottom-centre of its nearest
|
|
7
8
|
* positioned ancestor when a selection is active; render it conditionally
|
|
8
9
|
* (e.g. `count > 0 && <BulkBar …/>`). Measured from a records table.
|
|
9
10
|
*/
|
|
10
|
-
export function BulkBar({ count, onClear, countLabel
|
|
11
|
+
export function BulkBar({ count, onClear, countLabel: countLabelProp, countText = (count, label) => (_jsxs(_Fragment, { children: [count, " ", label] })), children, className, clearLabel: clearLabelProp, ...rest }) {
|
|
12
|
+
// crisp/#471: same resolution order as every other component's own.
|
|
13
|
+
const countLabel = useCrispMessage(countLabelProp, "Selected");
|
|
14
|
+
const clearLabel = useCrispMessage(clearLabelProp, "Clear selection");
|
|
11
15
|
return (_jsx("div", { className: bulkbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-bulkbar-bar", children: [count != null && (_jsxs("button", { type: "button", className: "crisp-bulkbar-count", onClick: onClear, title: clearLabel, children: [countText(count, countLabel), _jsx("span", { className: "crisp-bulkbar-clearlabel", children: clearLabel }), _jsx(X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
|
|
12
16
|
}
|
|
13
17
|
BulkBar.displayName = "BulkBar";
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useFileUpload, } from "@seed-design/react-file-upload";
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
5
|
import * as React from "react";
|
|
6
|
-
import { useFocusAfterRemoval } from "../../core/index.js";
|
|
6
|
+
import { useCrispMessage, useCrispMessages, useFocusAfterRemoval, } from "../../core/index.js";
|
|
7
7
|
import { fileuploadRecipe } from "./fileupload.recipe.js";
|
|
8
8
|
export const fileUploadLabels = {
|
|
9
9
|
// Matches the copy on the record Files pane.
|
|
@@ -56,10 +56,26 @@ export function FileUpload({ className, labels, "aria-label": ariaLabel, id, acc
|
|
|
56
56
|
onAcceptedFileEntriesChange,
|
|
57
57
|
};
|
|
58
58
|
const [rejections, setRejections] = React.useState([]);
|
|
59
|
+
// crisp/#471: same resolution order as every other component's own —
|
|
60
|
+
// explicit `labels` entry wins, then a CrispMessagesProvider's
|
|
61
|
+
// translation of the built-in English default, then that default itself.
|
|
62
|
+
const title = useCrispMessage(labels?.title, fileUploadLabels.title);
|
|
63
|
+
const hint = useCrispMessage(labels?.hint, fileUploadLabels.hint);
|
|
64
|
+
const remove = useCrispMessage(labels?.remove, fileUploadLabels.remove);
|
|
65
|
+
const crispMessages = useCrispMessages();
|
|
59
66
|
const t = {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
title,
|
|
68
|
+
hint,
|
|
69
|
+
remove,
|
|
70
|
+
errors: Object.fromEntries(Object.keys(fileUploadLabels.errors).map((code) => {
|
|
71
|
+
const englishDefault = fileUploadLabels.errors[code];
|
|
72
|
+
return [
|
|
73
|
+
code,
|
|
74
|
+
labels?.errors?.[code] ??
|
|
75
|
+
crispMessages[englishDefault] ??
|
|
76
|
+
englishDefault,
|
|
77
|
+
];
|
|
78
|
+
})),
|
|
63
79
|
};
|
|
64
80
|
const api = useFileUpload({
|
|
65
81
|
...props,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import { useCrispMessage } from "../../core/index.js";
|
|
3
4
|
import { formfieldRecipe } from "./formfield.recipe.js";
|
|
4
5
|
/**
|
|
5
6
|
* Tell the control what the field knows about it.
|
|
@@ -60,12 +61,18 @@ function useFieldWiring({ htmlFor, messageId, invalid, required, }) {
|
|
|
60
61
|
export function FormField({ label, htmlFor, required, optional, description, error, children, className, ...rest }) {
|
|
61
62
|
const descId = React.useId();
|
|
62
63
|
const hasMsg = error != null || description != null;
|
|
64
|
+
// crisp/#471: `optional` doubles as a boolean flag and a ReactNode
|
|
65
|
+
// override — passing any node instead of `true` already renders it
|
|
66
|
+
// verbatim below, with no lookup needed. `true` reads a
|
|
67
|
+
// CrispMessagesProvider's translation of the built-in default text, same
|
|
68
|
+
// resolution order as every other component's own.
|
|
69
|
+
const optionalDefault = useCrispMessage(undefined, "(optional)");
|
|
63
70
|
useFieldWiring({
|
|
64
71
|
htmlFor,
|
|
65
72
|
messageId: hasMsg ? descId : undefined,
|
|
66
73
|
invalid: error != null,
|
|
67
74
|
required: !!required,
|
|
68
75
|
});
|
|
69
|
-
return (_jsxs("div", { className: formfieldRecipe({ className }), ...rest, children: [_jsxs("label", { className: "crisp-formfield-label", htmlFor: htmlFor, children: [label, required && (_jsx("span", { className: "crisp-formfield-required", "aria-hidden": "true", children: "*" })), optional && !required && (_jsx("span", { className: "crisp-formfield-optional", children: optional === true ?
|
|
76
|
+
return (_jsxs("div", { className: formfieldRecipe({ className }), ...rest, children: [_jsxs("label", { className: "crisp-formfield-label", htmlFor: htmlFor, children: [label, required && (_jsx("span", { className: "crisp-formfield-required", "aria-hidden": "true", children: "*" })), optional && !required && (_jsx("span", { className: "crisp-formfield-optional", children: optional === true ? optionalDefault : optional }))] }), children, error != null ? (_jsx("span", { className: "crisp-formfield-error", id: hasMsg ? descId : undefined, children: error })) : description != null ? (_jsx("span", { className: "crisp-formfield-desc", id: hasMsg ? descId : undefined, children: description })) : null] }));
|
|
70
77
|
}
|
|
71
78
|
FormField.displayName = "FormField";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCrispMessages } from "../../core/index.js";
|
|
2
3
|
import { heatmapchartRecipe } from "./heatmapchart.recipe.js";
|
|
3
4
|
/**
|
|
4
5
|
* An NxM cell-matrix chart — GRC's own risk heatmap (impact x likelihood)
|
|
@@ -7,9 +8,20 @@ import { heatmapchartRecipe } from "./heatmapchart.recipe.js";
|
|
|
7
8
|
* the recipe's own doc comment for why (and for its measurement — or lack
|
|
8
9
|
* of one — against Attio).
|
|
9
10
|
*/
|
|
10
|
-
export function HeatmapChart({ data, rowAxis, colAxis, format = (cell) => (cell.value ? cell.value : ""), cellTitle, onCellClick, isCellClickable = (cell) => cell.value > 0, empty, emptyLabel
|
|
11
|
+
export function HeatmapChart({ data, rowAxis, colAxis, format = (cell) => (cell.value ? cell.value : ""), cellTitle, onCellClick, isCellClickable = (cell) => cell.value > 0, empty, emptyLabel: emptyLabelProp, "aria-label": ariaLabel, className, ...rest }) {
|
|
11
12
|
const cols = data[0]?.length ?? 0;
|
|
12
|
-
|
|
13
|
+
// crisp/#471: `emptyLabel` is typed `React.ReactNode`, so it resolves by
|
|
14
|
+
// hand rather than through `useCrispMessage`, which is string-only. The
|
|
15
|
+
// default `aria-label` is a sentence built from two static fragments
|
|
16
|
+
// around live data (row/column counts, not translatable), so each
|
|
17
|
+
// fragment gets its own lookup — same as TableToolbar's "Sorted by"
|
|
18
|
+
// (crisp/#217).
|
|
19
|
+
const crispMessages = useCrispMessages();
|
|
20
|
+
const emptyLabel = emptyLabelProp ??
|
|
21
|
+
crispMessages["Nothing to plot yet."] ??
|
|
22
|
+
"Nothing to plot yet.";
|
|
23
|
+
const summary = ariaLabel ??
|
|
24
|
+
`${crispMessages.Heatmap ?? "Heatmap"}: ${data.length} ${crispMessages.x ?? "x"} ${cols} ${crispMessages.grid ?? "grid"}`;
|
|
13
25
|
return (_jsx("div", { className: heatmapchartRecipe({ className }), ...rest, role: "img", "aria-label": summary, children: empty ? (_jsx("div", { className: "crisp-heatmapchart-empty", children: emptyLabel })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "crisp-heatmapchart-rowaxis", children: [_jsx("span", { children: rowAxis.high }), rowAxis.title != null && (_jsx("span", { className: "crisp-heatmapchart-rowaxis-title", children: rowAxis.title })), _jsx("span", { children: rowAxis.low })] }), _jsxs("div", { className: "crisp-heatmapchart-body", children: [_jsx("div", { className: "crisp-heatmapchart-grid", style: { gridTemplateColumns: `repeat(${cols}, 1fr)` }, children: data.map((row, r) => row.map((cell, cc) => {
|
|
14
26
|
const clickable = onCellClick != null && isCellClickable(cell, r, cc);
|
|
15
27
|
const title = cellTitle?.(cell, r, cc);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./skiplink.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { skiplinkRecipe } from "./skiplink.recipe.js";
|
|
3
|
+
/**
|
|
4
|
+
* A "skip to content" link — invisible until it is the thing with focus
|
|
5
|
+
* (a sighted mouse user never sees it), so a keyboard user's first Tab from
|
|
6
|
+
* a cold load reaches it instead of walking the whole nav first. Render it
|
|
7
|
+
* as the very first thing on the page, before the sidebar/header. See the
|
|
8
|
+
* recipe's own doc comment for why there's no Attio value behind its chrome.
|
|
9
|
+
*/
|
|
10
|
+
export function SkipLink({ href = "#main-content", children = "Skip to content", className, ...rest }) {
|
|
11
|
+
return (_jsx("a", { href: href, className: skiplinkRecipe({ className }), ...rest, children: children }));
|
|
12
|
+
}
|
|
13
|
+
SkipLink.displayName = "SkipLink";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c, elevation as e } from "../../tokens/index.js";
|
|
3
|
+
// A "skip to content" link: invisible (the same sr-only clip
|
|
4
|
+
// VisuallyHidden uses) until it is the thing with focus, so a sighted
|
|
5
|
+
// mouse user never sees it but a keyboard user's first Tab from a cold
|
|
6
|
+
// load reaches it. No Attio screen ships one to measure -- this is
|
|
7
|
+
// boilerplate accessibility furniture, not a design value (crisp#462 GRC
|
|
8
|
+
// audit gap 2 of 4, crisp#540) -- so the un-hidden chrome reuses already-measured
|
|
9
|
+
// crisp values throughout: the `bgPopover` floating surface + its
|
|
10
|
+
// `elevation.floating` shadow (the same pairing Menu/Popover/ContextMenu/
|
|
11
|
+
// Toast use for "a surface that appears above everything else"), `radius.md`,
|
|
12
|
+
// and the canonical `:focus-visible` ring every other crisp control uses
|
|
13
|
+
// (`outline: 2px solid ${bgBrandSolid}`, 1px offset) -- no new value is
|
|
14
|
+
// introduced.
|
|
15
|
+
const config = defineRecipe({
|
|
16
|
+
name: "skiplink",
|
|
17
|
+
base: `position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; text-decoration: none;
|
|
18
|
+
font-family: ${c.font.sans}; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary};
|
|
19
|
+
|
|
20
|
+
&:focus {
|
|
21
|
+
position: fixed; top: ${c.space["3"]}; inset-inline-start: ${c.space["3"]}; z-index: 90; width: auto; height: auto; margin: 0; overflow: visible; clip: auto; white-space: normal;
|
|
22
|
+
padding: ${c.space["2"]} ${c.space["3"]}; border-radius: ${c.radius.md}; background: ${c.bgPopover}; box-shadow: ${e.floating};
|
|
23
|
+
}
|
|
24
|
+
&:focus-visible { outline: 2px solid ${c.bgBrandSolid}; outline-offset: 1px; }`,
|
|
25
|
+
variants: {},
|
|
26
|
+
defaultVariants: {},
|
|
27
|
+
compoundVariants: [],
|
|
28
|
+
});
|
|
29
|
+
export const skiplinkRecipe = resolveRecipe(config);
|
|
30
|
+
export default config;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { textlinkRecipe } from "./textlink.recipe.js";
|
|
3
|
+
/**
|
|
4
|
+
* Inline navigable text — a real `<a href>` always, the same href+onClick
|
|
5
|
+
* division `StatList` uses: `href` gives ordinary browser behaviour (new
|
|
6
|
+
* tab, copy link, hover status), an `onClick` that calls
|
|
7
|
+
* `event.preventDefault()` pairs it with client-side routing (e.g.
|
|
8
|
+
* react-router's own `Link`).
|
|
9
|
+
*/
|
|
10
|
+
export function TextLink({ tone = "muted", className, ...rest }) {
|
|
11
|
+
return _jsx("a", { className: textlinkRecipe({ tone, className }), ...rest });
|
|
12
|
+
}
|
|
13
|
+
TextLink.displayName = "TextLink";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// The one inline-navigable-text shape GRC's own screens kept hand-typing at
|
|
4
|
+
// every call site (internal/grc#110 / your-moon/crisp#529): a card's own
|
|
5
|
+
// trailing "View all →", and a muted in-context reference link that goes
|
|
6
|
+
// brand on hover. No new colour or size here — both variants read the exact
|
|
7
|
+
// tokens those call sites already used (`bgBrandSolid` reused as a text
|
|
8
|
+
// colour, the same alias GRC's own `--color-fg-brand` resolves to; `fgMuted`;
|
|
9
|
+
// the existing 12px/13px text sizes) rather than a fresh guess, and neither
|
|
10
|
+
// is measured against one specific Attio widget for the same reason
|
|
11
|
+
// StatList/HeatmapChart's own doc comments give: this exact "small trailing
|
|
12
|
+
// link" isn't a distinct Attio component to point a CDP session at, it's a
|
|
13
|
+
// reuse of already-measured type and colour values in a new arrangement.
|
|
14
|
+
const config = defineRecipe({
|
|
15
|
+
name: "textlink",
|
|
16
|
+
base: `display: inline; font-family: ${c.font.sans}; text-decoration: none; cursor: pointer;
|
|
17
|
+
&:hover { text-decoration: underline; }`,
|
|
18
|
+
variants: {
|
|
19
|
+
tone: {
|
|
20
|
+
// The bold, always-brand-coloured trailing link a Card's own `action`
|
|
21
|
+
// slot uses ("View all →", "All registers →").
|
|
22
|
+
brand: `font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.bgBrandSolid};`,
|
|
23
|
+
// A quieter in-context reference (a sample record under a finding)
|
|
24
|
+
// that only goes brand on hover.
|
|
25
|
+
muted: `font-size: ${c.text.md}; font-weight: ${c.weight.regular}; color: ${c.fgMuted}; &:hover { color: ${c.bgBrandSolid}; }`,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: { tone: "muted" },
|
|
29
|
+
compoundVariants: [],
|
|
30
|
+
});
|
|
31
|
+
export const textlinkRecipe = resolveRecipe(config);
|
|
32
|
+
export default config;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./visuallyhidden.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { visuallyhiddenRecipe } from "./visuallyhidden.recipe.js";
|
|
3
|
+
/**
|
|
4
|
+
* Hides content visually while keeping it in the accessibility tree and
|
|
5
|
+
* the tab order — a label for an icon-only control, extra context for a
|
|
6
|
+
* screen-reader user that a sighted one gets from layout alone. Renders a
|
|
7
|
+
* `<span>`; wrap the element that needs the treatment, or read its content
|
|
8
|
+
* as a `label`/`aria-label` source in a test.
|
|
9
|
+
*/
|
|
10
|
+
export function VisuallyHidden({ className, ...rest }) {
|
|
11
|
+
return _jsx("span", { className: visuallyhiddenRecipe({ className }), ...rest });
|
|
12
|
+
}
|
|
13
|
+
VisuallyHidden.displayName = "VisuallyHidden";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
// The standard "sr-only" clip pattern: content stays in the accessibility
|
|
3
|
+
// tree and the tab order, but occupies no visible space and is never
|
|
4
|
+
// painted. Pure CSS mechanics, not a design value -- there is nothing to
|
|
5
|
+
// measure against Attio here (crisp#462 GRC audit gap 2 of 4, crisp#540):
|
|
6
|
+
// no Attio screen ships content that is deliberately invisible-but-reachable,
|
|
7
|
+
// so this is the standard web-platform pattern, same shape `SkipLink` also
|
|
8
|
+
// starts from before it un-hides on focus.
|
|
9
|
+
const config = defineRecipe({
|
|
10
|
+
name: "visuallyhidden",
|
|
11
|
+
base: `position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;`,
|
|
12
|
+
variants: {},
|
|
13
|
+
defaultVariants: {},
|
|
14
|
+
compoundVariants: [],
|
|
15
|
+
});
|
|
16
|
+
export const visuallyhiddenRecipe = resolveRecipe(config);
|
|
17
|
+
export default config;
|
package/dist/esm/index.js
CHANGED
|
@@ -44,6 +44,7 @@ export * from "./components/resizablepanel/index.js";
|
|
|
44
44
|
export * from "./components/segmented/index.js";
|
|
45
45
|
export * from "./components/select/index.js";
|
|
46
46
|
export * from "./components/skeleton/index.js";
|
|
47
|
+
export * from "./components/skiplink/index.js";
|
|
47
48
|
export * from "./components/slider/index.js";
|
|
48
49
|
export * from "./components/spinner/index.js";
|
|
49
50
|
export * from "./components/stagebar/index.js";
|
|
@@ -53,21 +54,25 @@ export * from "./components/table/index.js";
|
|
|
53
54
|
export * from "./components/tabletoolbar/index.js";
|
|
54
55
|
export * from "./components/tabs/index.js";
|
|
55
56
|
export * from "./components/textarea/index.js";
|
|
57
|
+
export * from "./components/textlink/index.js";
|
|
56
58
|
export * from "./components/timepicker/index.js";
|
|
57
59
|
export * from "./components/toast/index.js";
|
|
58
60
|
export * from "./components/tooltip/index.js";
|
|
61
|
+
export * from "./components/visuallyhidden/index.js";
|
|
59
62
|
export * from "./components/wheelpicker/index.js";
|
|
60
63
|
export * from "./core/index.js";
|
|
61
64
|
// product surfaces (product/) — faithful, pixel-measured rebuilds of specific
|
|
62
65
|
// product screens (Sidebar, RecordsTable, RecordPage, Inbox, …). Use them as-is
|
|
63
66
|
// or as composition references for your own data; `components/` above holds
|
|
64
67
|
// the generic primitives (Table, DataTable, Button, …) to build your own UI from.
|
|
68
|
+
export * from "./product/activityfeed/index.js";
|
|
65
69
|
export * from "./product/appshell/index.js";
|
|
66
70
|
export * from "./product/authshell/index.js";
|
|
67
71
|
export * from "./product/board/index.js";
|
|
68
72
|
export * from "./product/command/index.js";
|
|
69
73
|
export * from "./product/entitycard/index.js";
|
|
70
74
|
export * from "./product/flowcanvas/index.js";
|
|
75
|
+
export * from "./product/headerbar/index.js";
|
|
71
76
|
export * from "./product/inbox/index.js";
|
|
72
77
|
export * from "./product/listpage/index.js";
|
|
73
78
|
export * from "./product/navitem/index.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { activityfeedRecipe } from "./activityfeed.recipe.js";
|
|
3
|
+
/**
|
|
4
|
+
* A record's own event timeline — a vertically stacked, bordered list of
|
|
5
|
+
* `ActivityFeedRow`s, capped at a readable content width. Built for
|
|
6
|
+
* internal/grc's per-record "Activity" tab (crisp#462 SHELL/forms gap): it
|
|
7
|
+
* replaces a hand-rolled `<div className="mx-auto max-w-3xl space-y-4
|
|
8
|
+
* p-6">`. No live Attio re-measurement behind its own chrome this pass (the
|
|
9
|
+
* CDP bridge was unreachable when this was built) — see the recipe's own
|
|
10
|
+
* doc comment for which already-measured crisp values it reuses instead of
|
|
11
|
+
* a new guess.
|
|
12
|
+
*/
|
|
13
|
+
export function ActivityFeed({ className, children, ...rest }) {
|
|
14
|
+
return (_jsx("div", { className: activityfeedRecipe({ className }), ...rest, children: children }));
|
|
15
|
+
}
|
|
16
|
+
ActivityFeed.displayName = "ActivityFeed";
|
|
17
|
+
/** One event row: actor + action + timestamp, with an optional diff list
|
|
18
|
+
* underneath. The last row in an `ActivityFeed` draws no divider. */
|
|
19
|
+
export function ActivityFeedRow({ actor, action, when, children, }) {
|
|
20
|
+
return (_jsxs("div", { className: "crisp-activityfeed-row", children: [_jsxs("div", { className: "crisp-activityfeed-row-head", children: [actor, action, _jsx("span", { className: "crisp-activityfeed-row-spacer" }), _jsx("span", { className: "crisp-activityfeed-row-when", children: when })] }), children] }));
|
|
21
|
+
}
|
|
22
|
+
ActivityFeedRow.displayName = "ActivityFeedRow";
|
|
23
|
+
/** One changed-field line under an `ActivityFeedRow` — "Field: before →
|
|
24
|
+
* after", or "Field: set to after" when there is no known prior value. */
|
|
25
|
+
export function ActivityFeedDiffItem({ field, before, after, }) {
|
|
26
|
+
return (_jsxs("li", { className: "crisp-activityfeed-diff-item", children: [_jsx("span", { className: "crisp-activityfeed-diff-field", children: field }), " ", before !== undefined ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-activityfeed-diff-before", children: before }), " ", _jsx("span", { "aria-hidden": "true", children: "\u2192" }), " ", _jsx("span", { children: after })] })) : (_jsx("span", { children: after }))] }));
|
|
27
|
+
}
|
|
28
|
+
ActivityFeedDiffItem.displayName = "ActivityFeedDiffItem";
|
|
29
|
+
export function ActivityFeedDiffList({ children, }) {
|
|
30
|
+
return _jsx("ul", { className: "crisp-activityfeed-diff", children: children });
|
|
31
|
+
}
|
|
32
|
+
ActivityFeedDiffList.displayName = "ActivityFeedDiffList";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A record's own event timeline (crisp#462 gap: internal/grc's ActivityTab
|
|
4
|
+
// hand-rolled this as a bare `<div className="mx-auto max-w-3xl space-y-4
|
|
5
|
+
// p-6">` list of bordered rows). No live Attio re-measurement behind these
|
|
6
|
+
// values this pass -- the CDP bridge to the owner's Vivaldi was unreachable
|
|
7
|
+
// when this was built (documented in the component's own doc comment) --
|
|
8
|
+
// so this reuses already-measured crisp values throughout rather than a new
|
|
9
|
+
// guess: `space["6"]` (24px) padding is RecordPanel's own content padding,
|
|
10
|
+
// the row's bottom border reuses `borderSubtle` (the same divider every
|
|
11
|
+
// other bordered row in this codebase draws with), and the 767px content
|
|
12
|
+
// cap matches SettingsSection's own measured content width.
|
|
13
|
+
const config = defineRecipe({
|
|
14
|
+
name: "activityfeed",
|
|
15
|
+
base: `box-sizing: border-box; max-width: 767px; margin-inline: auto; padding: ${c.space["6"]};
|
|
16
|
+
display: flex; flex-direction: column; gap: ${c.space["4"]};
|
|
17
|
+
& .crisp-activityfeed-row { display: flex; flex-direction: column; gap: ${c.space["2"]}; padding-bottom: ${c.space["4"]}; border-bottom: 1px solid ${c.borderSubtle}; }
|
|
18
|
+
& .crisp-activityfeed-row:last-of-type { border-bottom: none; padding-bottom: 0; }
|
|
19
|
+
& .crisp-activityfeed-row-head { display: flex; align-items: center; gap: ${c.space["2"]}; }
|
|
20
|
+
& .crisp-activityfeed-row-spacer { flex: 1; }
|
|
21
|
+
& .crisp-activityfeed-row-when { font-size: ${c.text.xs}; color: ${c.fgMuted}; white-space: nowrap; }
|
|
22
|
+
& .crisp-activityfeed-diff { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: ${c.space["1"]}; }
|
|
23
|
+
& .crisp-activityfeed-diff-item { padding-inline-start: ${c.space["8"]}; font-size: ${c.text.md}; }
|
|
24
|
+
& .crisp-activityfeed-diff-field { color: ${c.fgMuted}; }
|
|
25
|
+
& .crisp-activityfeed-diff-before { color: ${c.fgMuted}; text-decoration: line-through; }
|
|
26
|
+
`,
|
|
27
|
+
variants: {},
|
|
28
|
+
defaultVariants: {},
|
|
29
|
+
compoundVariants: [],
|
|
30
|
+
});
|
|
31
|
+
export const activityfeedRecipe = resolveRecipe(config);
|
|
32
|
+
export default config;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import { isRtlElement } from "../../core/index.js";
|
|
3
|
+
import { isRtlElement, useCrispMessage, useCrispMessages } from "../../core/index.js";
|
|
4
4
|
import { boardRecipe } from "./board.recipe.js";
|
|
5
5
|
/**
|
|
6
6
|
* A kanban board with native drag-and-drop between columns. Cards are
|
|
@@ -8,7 +8,16 @@ import { boardRecipe } from "./board.recipe.js";
|
|
|
8
8
|
* (the parent owns the cards, so the move is a controlled state update).
|
|
9
9
|
* Measured from a deals pipeline.
|
|
10
10
|
*/
|
|
11
|
-
export function Board({ columns, cards, onCardsChange, renderCard, onNewCard, newCardLabel
|
|
11
|
+
export function Board({ columns, cards, onCardsChange, renderCard, onNewCard, newCardLabel: newCardLabelProp, getCardLabel, announceMove: announceMoveProp, className, ...rest }) {
|
|
12
|
+
// crisp/#471: same resolution order as every other component's own —
|
|
13
|
+
// explicit prop, then a CrispMessagesProvider's translation of the English
|
|
14
|
+
// default, then that default itself.
|
|
15
|
+
const newCardLabel = useCrispMessage(newCardLabelProp, "New");
|
|
16
|
+
const crispMessages = useCrispMessages();
|
|
17
|
+
const announceMove = announceMoveProp ??
|
|
18
|
+
((column, card) => card
|
|
19
|
+
? `${card} ${crispMessages["moved to"] ?? "moved to"} ${column}`
|
|
20
|
+
: `${crispMessages["Moved to"] ?? "Moved to"} ${column}`);
|
|
12
21
|
const [dragId, setDragId] = React.useState(null);
|
|
13
22
|
const [overCol, setOverCol] = React.useState(null);
|
|
14
23
|
const [announcement, setAnnouncement] = React.useState("");
|
|
@@ -65,7 +74,7 @@ export function Board({ columns, cards, onCardsChange, renderCard, onNewCard, ne
|
|
|
65
74
|
move(dragId, col.id);
|
|
66
75
|
setDragId(null);
|
|
67
76
|
setOverCol(null);
|
|
68
|
-
}, children: [_jsxs("div", { className: "crisp-board-col-header", children: [col.accent && (_jsx("span", { className: "crisp-board-col-dot", style: { background: col.accent } })), _jsx("span", { className: "crisp-board-col-title", children: col.title }), _jsx("span", { className: "crisp-board-col-count", children: colCards.length }), onNewCard && (_jsx("button", { type: "button", className: "crisp-board-col-add", "aria-label":
|
|
77
|
+
}, children: [_jsxs("div", { className: "crisp-board-col-header", children: [col.accent && (_jsx("span", { className: "crisp-board-col-dot", style: { background: col.accent } })), _jsx("span", { className: "crisp-board-col-title", children: col.title }), _jsx("span", { className: "crisp-board-col-count", children: colCards.length }), onNewCard && (_jsx("button", { type: "button", className: "crisp-board-col-add", "aria-label": `${crispMessages["Add to"] ?? "Add to"} ${col.title}`, onClick: () => onNewCard(col.id), children: "+" }))] }), _jsxs("div", { className: "crisp-board-col-body", role: "list", "aria-label": col.title, children: [onNewCard && (_jsxs("button", { type: "button", className: "crisp-board-newcard", onClick: () => onNewCard(col.id), children: ["+ ", newCardLabel] })), colCards.map((card) => (_jsx("div", { className: "crisp-board-card", role: "listitem",
|
|
69
78
|
// biome-ignore lint/a11y/noNoninteractiveTabindex: the rule assumes a listitem cannot be operated. This one carries the board's only keyboard path to move a card, so it has to be reachable by Tab
|
|
70
79
|
tabIndex: 0, "aria-label": getCardLabel?.(card), "data-card-id": card.id, draggable: true, "data-dragging": dragId === card.id ? "" : undefined, onDragStart: () => setDragId(card.id), onDragEnd: () => {
|
|
71
80
|
setDragId(null);
|
|
@@ -5,7 +5,7 @@ import { normalizeProps, Portal, useMachine } from "@zag-js/react";
|
|
|
5
5
|
import { Building2, CircleDollarSign, House, ListTodo, Search, Settings, SquareCheck, User, UserPlus, } from "lucide-react";
|
|
6
6
|
import * as React from "react";
|
|
7
7
|
import { Kbd } from "../../components/kbd/index.js";
|
|
8
|
-
import { cx, withDescribedBy } from "../../core/index.js";
|
|
8
|
+
import { cx, useCrispMessage, useCrispMessages, withDescribedBy, } from "../../core/index.js";
|
|
9
9
|
import { commandRecipe } from "./command.recipe.js";
|
|
10
10
|
const IconHome = () => (_jsx(House, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }));
|
|
11
11
|
const IconTasks = () => (_jsx(SquareCheck, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }));
|
|
@@ -110,7 +110,19 @@ function useCommandInstanceId() {
|
|
|
110
110
|
* option list) so keyboard/focus correctness comes from the machines, not
|
|
111
111
|
* hand-rolled event handling.
|
|
112
112
|
*/
|
|
113
|
-
export function Command({ open, onOpenChange, items = defaultCommandItems, placeholder
|
|
113
|
+
export function Command({ open, onOpenChange, items = defaultCommandItems, placeholder: placeholderProp, "aria-label": ariaLabelProp, navigateLabel: navigateLabelProp, selectLabel: selectLabelProp, emptyLabel: emptyLabelProp, query: queryProp, onQueryChange, filterItems = true, className, "aria-describedby": describedBy, ...rest }) {
|
|
114
|
+
// crisp/#471: same resolution order as every other component's own.
|
|
115
|
+
// `navigateLabel`/`emptyLabel` are typed `React.ReactNode`, so they
|
|
116
|
+
// resolve by hand rather than through `useCrispMessage`, which is
|
|
117
|
+
// string-only.
|
|
118
|
+
const crispMessages = useCrispMessages();
|
|
119
|
+
const placeholder = useCrispMessage(placeholderProp, "Search actions and records…");
|
|
120
|
+
const ariaLabel = useCrispMessage(ariaLabelProp, "Command palette");
|
|
121
|
+
const navigateLabel = navigateLabelProp ?? crispMessages.Navigate ?? "Navigate";
|
|
122
|
+
const selectLabel = useCrispMessage(selectLabelProp, "Select");
|
|
123
|
+
const emptyLabel = emptyLabelProp ??
|
|
124
|
+
crispMessages["No matching commands"] ??
|
|
125
|
+
"No matching commands";
|
|
114
126
|
const instanceId = useCommandInstanceId();
|
|
115
127
|
const dialogId = `${instanceId}-dialog`;
|
|
116
128
|
const comboId = `${instanceId}-combobox`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Minus, Plus } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useCrispMessage } from "../../core/index.js";
|
|
4
5
|
import { flowcanvasRecipe } from "./flowcanvas.recipe.js";
|
|
5
6
|
const GRID = 20;
|
|
6
7
|
const clampZoom = (z) => Math.min(1.5, Math.max(0.5, z));
|
|
@@ -9,7 +10,13 @@ const clampZoom = (z) => Math.min(1.5, Math.max(0.5, z));
|
|
|
9
10
|
* measured from a workflows editor. Drag a node to move it (fires
|
|
10
11
|
* `onNodesChange`); drag the background to pan; use the toolbar to zoom.
|
|
11
12
|
*/
|
|
12
|
-
export function FlowCanvas({ nodes, edges, onNodesChange, renderNode, nodeWidth = 240, nodeHeight = 64, "aria-label":
|
|
13
|
+
export function FlowCanvas({ nodes, edges, onNodesChange, renderNode, nodeWidth = 240, nodeHeight = 64, "aria-label": ariaLabelProp, className, zoomOutLabel: zoomOutLabelProp, zoomInLabel: zoomInLabelProp, resetLabel: resetLabelProp, edgesLabel: edgesLabelProp, ...rest }) {
|
|
14
|
+
// crisp/#471: same resolution order as every other component's own.
|
|
15
|
+
const ariaLabel = useCrispMessage(ariaLabelProp, "Flow canvas");
|
|
16
|
+
const zoomOutLabel = useCrispMessage(zoomOutLabelProp, "Zoom out");
|
|
17
|
+
const zoomInLabel = useCrispMessage(zoomInLabelProp, "Zoom in");
|
|
18
|
+
const resetLabel = useCrispMessage(resetLabelProp, "Reset");
|
|
19
|
+
const edgesLabel = useCrispMessage(edgesLabelProp, "Edges");
|
|
13
20
|
const [pan, setPan] = React.useState({ x: 40, y: 40 });
|
|
14
21
|
const [zoom, setZoom] = React.useState(1);
|
|
15
22
|
const [panning, setPanning] = React.useState(false);
|
|
@@ -64,7 +71,7 @@ export function FlowCanvas({ nodes, edges, onNodesChange, renderNode, nodeWidth
|
|
|
64
71
|
...rest.style,
|
|
65
72
|
backgroundSize: `${GRID * zoom}px ${GRID * zoom}px`,
|
|
66
73
|
backgroundPosition: `${pan.x}px ${pan.y}px`,
|
|
67
|
-
}, role: "application", "aria-label": ariaLabel
|
|
74
|
+
}, role: "application", "aria-label": ariaLabel, onPointerDown: onBackgroundPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerUp, children: [_jsxs("div", { className: "crisp-flowcanvas-viewport", style: {
|
|
68
75
|
transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
|
|
69
76
|
}, children: [_jsxs("svg", { className: "crisp-flowcanvas-edges", "aria-hidden": "true", children: [_jsx("title", { children: edgesLabel }), edges.map((edge) => {
|
|
70
77
|
const a = byId.get(edge.from);
|