@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.
Files changed (124) hide show
  1. package/dist/activityfeed.css +12 -0
  2. package/dist/activityfeed.d.ts +7 -0
  3. package/dist/activityfeed.layered.css +14 -0
  4. package/dist/badge.css +3 -0
  5. package/dist/badge.d.ts +2 -2
  6. package/dist/badge.layered.css +3 -0
  7. package/dist/cjs/components/badge/badge.recipe.js +10 -0
  8. package/dist/cjs/components/bulkbar/bulkbar.js +5 -1
  9. package/dist/cjs/components/fileupload/fileupload.js +19 -3
  10. package/dist/cjs/components/formfield/formfield.js +8 -1
  11. package/dist/cjs/components/heatmapchart/heatmapchart.js +14 -2
  12. package/dist/cjs/components/skiplink/index.js +17 -0
  13. package/dist/cjs/components/skiplink/skiplink.js +16 -0
  14. package/dist/cjs/components/skiplink/skiplink.recipe.js +33 -0
  15. package/dist/cjs/components/textlink/index.js +7 -0
  16. package/dist/cjs/components/textlink/textlink.js +16 -0
  17. package/dist/cjs/components/textlink/textlink.recipe.js +35 -0
  18. package/dist/cjs/components/visuallyhidden/index.js +17 -0
  19. package/dist/cjs/components/visuallyhidden/visuallyhidden.js +16 -0
  20. package/dist/cjs/components/visuallyhidden/visuallyhidden.recipe.js +20 -0
  21. package/dist/cjs/index.js +5 -0
  22. package/dist/cjs/product/activityfeed/activityfeed.js +38 -0
  23. package/dist/cjs/product/activityfeed/activityfeed.recipe.js +35 -0
  24. package/dist/cjs/product/activityfeed/index.js +10 -0
  25. package/dist/cjs/product/board/board.js +11 -2
  26. package/dist/cjs/product/command/command.js +13 -1
  27. package/dist/cjs/product/flowcanvas/flowcanvas.js +9 -2
  28. package/dist/cjs/product/headerbar/headerbar.js +15 -0
  29. package/dist/cjs/product/headerbar/headerbar.recipe.js +35 -0
  30. package/dist/cjs/product/headerbar/index.js +17 -0
  31. package/dist/cjs/product/inbox/inbox.js +13 -1
  32. package/dist/cjs/product/onboarding/onboarding.js +22 -1
  33. package/dist/cjs/product/recordpage/index.js +2 -1
  34. package/dist/cjs/product/recordpage/recordpage.js +197 -123
  35. package/dist/cjs/product/recordpage/recordpage.recipe.js +5 -0
  36. package/dist/cjs/product/richtext/richtext.js +43 -0
  37. package/dist/cjs/product/tasklist/tasklist.js +8 -1
  38. package/dist/cjs/tokens/palette.js +14 -0
  39. package/dist/cjs/tokens/semantic.js +5 -0
  40. package/dist/esm/components/badge/badge.recipe.js +10 -0
  41. package/dist/esm/components/bulkbar/bulkbar.js +5 -1
  42. package/dist/esm/components/fileupload/fileupload.js +20 -4
  43. package/dist/esm/components/formfield/formfield.js +8 -1
  44. package/dist/esm/components/heatmapchart/heatmapchart.js +14 -2
  45. package/dist/esm/components/skiplink/index.js +1 -0
  46. package/dist/esm/components/skiplink/skiplink.js +13 -0
  47. package/dist/esm/components/skiplink/skiplink.recipe.js +30 -0
  48. package/dist/esm/components/textlink/index.js +2 -0
  49. package/dist/esm/components/textlink/textlink.js +13 -0
  50. package/dist/esm/components/textlink/textlink.recipe.js +32 -0
  51. package/dist/esm/components/visuallyhidden/index.js +1 -0
  52. package/dist/esm/components/visuallyhidden/visuallyhidden.js +13 -0
  53. package/dist/esm/components/visuallyhidden/visuallyhidden.recipe.js +17 -0
  54. package/dist/esm/index.js +5 -0
  55. package/dist/esm/product/activityfeed/activityfeed.js +32 -0
  56. package/dist/esm/product/activityfeed/activityfeed.recipe.js +32 -0
  57. package/dist/esm/product/activityfeed/index.js +2 -0
  58. package/dist/esm/product/board/board.js +12 -3
  59. package/dist/esm/product/command/command.js +14 -2
  60. package/dist/esm/product/flowcanvas/flowcanvas.js +9 -2
  61. package/dist/esm/product/headerbar/headerbar.js +12 -0
  62. package/dist/esm/product/headerbar/headerbar.recipe.js +32 -0
  63. package/dist/esm/product/headerbar/index.js +1 -0
  64. package/dist/esm/product/inbox/inbox.js +13 -1
  65. package/dist/esm/product/onboarding/onboarding.js +22 -1
  66. package/dist/esm/product/recordpage/index.js +1 -1
  67. package/dist/esm/product/recordpage/recordpage.js +197 -124
  68. package/dist/esm/product/recordpage/recordpage.recipe.js +5 -0
  69. package/dist/esm/product/richtext/richtext.js +43 -0
  70. package/dist/esm/product/tasklist/tasklist.js +8 -1
  71. package/dist/esm/tokens/palette.js +14 -0
  72. package/dist/esm/tokens/semantic.js +5 -0
  73. package/dist/headerbar.css +7 -0
  74. package/dist/headerbar.d.ts +7 -0
  75. package/dist/headerbar.layered.css +9 -0
  76. package/dist/native/tokens.d.ts +1 -1
  77. package/dist/native/tokens.js +16 -4
  78. package/dist/recordpage.css +17 -31
  79. package/dist/recordpage.layered.css +17 -31
  80. package/dist/skiplink.css +5 -0
  81. package/dist/skiplink.d.ts +7 -0
  82. package/dist/skiplink.layered.css +7 -0
  83. package/dist/styles.css +69 -31
  84. package/dist/styles.layered.css +69 -31
  85. package/dist/textlink.css +11 -0
  86. package/dist/textlink.d.ts +7 -0
  87. package/dist/textlink.layered.css +13 -0
  88. package/dist/tokens.css +6 -0
  89. package/dist/tokens.json +16 -4
  90. package/dist/types/components/badge/badge.d.ts +2 -0
  91. package/dist/types/components/badge/badge.recipe.d.ts +8 -0
  92. package/dist/types/components/bulkbar/bulkbar.d.ts +1 -1
  93. package/dist/types/components/heatmapchart/heatmapchart.d.ts +1 -1
  94. package/dist/types/components/skiplink/index.d.ts +1 -0
  95. package/dist/types/components/skiplink/skiplink.d.ts +18 -0
  96. package/dist/types/components/skiplink/skiplink.recipe.d.ts +3 -0
  97. package/dist/types/components/textlink/index.d.ts +2 -0
  98. package/dist/types/components/textlink/textlink.d.ts +19 -0
  99. package/dist/types/components/textlink/textlink.recipe.d.ts +13 -0
  100. package/dist/types/components/visuallyhidden/index.d.ts +1 -0
  101. package/dist/types/components/visuallyhidden/visuallyhidden.d.ts +15 -0
  102. package/dist/types/components/visuallyhidden/visuallyhidden.recipe.d.ts +3 -0
  103. package/dist/types/index.d.ts +5 -0
  104. package/dist/types/product/activityfeed/activityfeed.d.ts +57 -0
  105. package/dist/types/product/activityfeed/activityfeed.recipe.d.ts +3 -0
  106. package/dist/types/product/activityfeed/index.d.ts +2 -0
  107. package/dist/types/product/board/board.d.ts +1 -1
  108. package/dist/types/product/command/command.d.ts +1 -1
  109. package/dist/types/product/flowcanvas/flowcanvas.d.ts +1 -1
  110. package/dist/types/product/headerbar/headerbar.d.ts +27 -0
  111. package/dist/types/product/headerbar/headerbar.recipe.d.ts +3 -0
  112. package/dist/types/product/headerbar/index.d.ts +1 -0
  113. package/dist/types/product/inbox/inbox.d.ts +1 -1
  114. package/dist/types/product/onboarding/onboarding.d.ts +1 -1
  115. package/dist/types/product/recordpage/index.d.ts +1 -1
  116. package/dist/types/product/recordpage/recordpage.d.ts +90 -1
  117. package/dist/types/product/tasklist/tasklist.d.ts +1 -1
  118. package/dist/types/tokens/contract.d.ts +3 -0
  119. package/dist/types/tokens/palette.d.ts +12 -0
  120. package/dist/types/tokens/semantic.d.ts +5 -0
  121. package/dist/visuallyhidden.css +3 -0
  122. package/dist/visuallyhidden.d.ts +7 -0
  123. package/dist/visuallyhidden.layered.css +5 -0
  124. package/package.json +26 -1
@@ -0,0 +1,12 @@
1
+ .crisp-activityfeed {
2
+ box-sizing: border-box; max-width: 767px; margin-inline: auto; padding: var(--crisp-space-6); display: flex; flex-direction: column; gap: var(--crisp-space-4);
3
+ & .crisp-activityfeed-row { display: flex; flex-direction: column; gap: var(--crisp-space-2); padding-bottom: var(--crisp-space-4); border-bottom: 1px solid var(--crisp-border-subtle); }
4
+ & .crisp-activityfeed-row:last-of-type { border-bottom: none; padding-bottom: 0; }
5
+ & .crisp-activityfeed-row-head { display: flex; align-items: center; gap: var(--crisp-space-2); }
6
+ & .crisp-activityfeed-row-spacer { flex: 1; }
7
+ & .crisp-activityfeed-row-when { font-size: var(--crisp-text-xs); color: var(--crisp-fg-muted); white-space: nowrap; }
8
+ & .crisp-activityfeed-diff { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--crisp-space-1); }
9
+ & .crisp-activityfeed-diff-item { padding-inline-start: var(--crisp-space-8); font-size: var(--crisp-text-md); }
10
+ & .crisp-activityfeed-diff-field { color: var(--crisp-fg-muted); }
11
+ & .crisp-activityfeed-diff-before { color: var(--crisp-fg-muted); text-decoration: line-through; }
12
+ }
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface ActivityfeedVariants {
4
+
5
+ }
6
+
7
+ export declare const activityfeedRecipe: RecipeFn<{ }>;
@@ -0,0 +1,14 @@
1
+ @layer crisp-components {
2
+ .crisp-activityfeed {
3
+ box-sizing: border-box; max-width: 767px; margin-inline: auto; padding: var(--crisp-space-6); display: flex; flex-direction: column; gap: var(--crisp-space-4);
4
+ & .crisp-activityfeed-row { display: flex; flex-direction: column; gap: var(--crisp-space-2); padding-bottom: var(--crisp-space-4); border-bottom: 1px solid var(--crisp-border-subtle); }
5
+ & .crisp-activityfeed-row:last-of-type { border-bottom: none; padding-bottom: 0; }
6
+ & .crisp-activityfeed-row-head { display: flex; align-items: center; gap: var(--crisp-space-2); }
7
+ & .crisp-activityfeed-row-spacer { flex: 1; }
8
+ & .crisp-activityfeed-row-when { font-size: var(--crisp-text-xs); color: var(--crisp-fg-muted); white-space: nowrap; }
9
+ & .crisp-activityfeed-diff { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--crisp-space-1); }
10
+ & .crisp-activityfeed-diff-item { padding-inline-start: var(--crisp-space-8); font-size: var(--crisp-text-md); }
11
+ & .crisp-activityfeed-diff-field { color: var(--crisp-fg-muted); }
12
+ & .crisp-activityfeed-diff-before { color: var(--crisp-fg-muted); text-decoration: line-through; }
13
+ }
14
+ }
package/dist/badge.css CHANGED
@@ -17,6 +17,9 @@
17
17
  .crisp-badge--tone_danger {
18
18
  background: color-mix(in srgb, var(--crisp-bg-danger) 14%, transparent); color: var(--crisp-fg-danger-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-danger) 30%, transparent);
19
19
  }
20
+ .crisp-badge--tone_purple {
21
+ background: var(--crisp-bg-purple-soft); color: var(--crisp-fg-purple-soft); box-shadow: inset 0 0 0 1px var(--crisp-ring-purple-soft);
22
+ }
20
23
  .crisp-badge--size_small {
21
24
  height: 16px; padding-inline: var(--crisp-space-1-5); font-size: var(--crisp-text-xs); border-radius: var(--crisp-radius-sm);
22
25
  }
package/dist/badge.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type { RecipeFn } from "../core/types";
2
2
 
3
3
  export interface BadgeVariants {
4
- tone?: "neutral" | "brand" | "success" | "warning" | "danger";
4
+ tone?: "neutral" | "brand" | "success" | "warning" | "danger" | "purple";
5
5
  size?: "small" | "medium" | "large";
6
6
  variant?: "soft" | "solid";
7
7
  }
8
8
 
9
- export declare const badgeRecipe: RecipeFn<{ tone: { neutral: string; brand: string; success: string; warning: string; danger: string }; size: { small: string; medium: string; large: string }; variant: { soft: string; solid: string } }>;
9
+ export declare const badgeRecipe: RecipeFn<{ tone: { neutral: string; brand: string; success: string; warning: string; danger: string; purple: string }; size: { small: string; medium: string; large: string }; variant: { soft: string; solid: string } }>;
@@ -18,6 +18,9 @@
18
18
  .crisp-badge--tone_danger {
19
19
  background: color-mix(in srgb, var(--crisp-bg-danger) 14%, transparent); color: var(--crisp-fg-danger-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-danger) 30%, transparent);
20
20
  }
21
+ .crisp-badge--tone_purple {
22
+ background: var(--crisp-bg-purple-soft); color: var(--crisp-fg-purple-soft); box-shadow: inset 0 0 0 1px var(--crisp-ring-purple-soft);
23
+ }
21
24
  .crisp-badge--size_small {
22
25
  height: 16px; padding-inline: var(--crisp-space-1-5); font-size: var(--crisp-text-xs); border-radius: var(--crisp-radius-sm);
23
26
  }
@@ -63,6 +63,16 @@ const spec = (0, core_1.defineSpec)({
63
63
  color: tokens_1.contract.fgDangerSoft,
64
64
  boxShadow: ring(tokens_1.contract.bgDanger, 30),
65
65
  },
66
+ // crisp#506: a distinct purple used for role-style labels (Attio's own
67
+ // "Admin" role pill), not a status/severity colour -- the other five
68
+ // tones don't cover this hue. Unlike those, these are the measured
69
+ // *solid* values directly (not a color-mix tint of a base accent):
70
+ // Attio's own pill background reads a flat, opaque rgb(), no alpha.
71
+ purple: {
72
+ background: tokens_1.contract.bgPurpleSoft,
73
+ color: tokens_1.contract.fgPurpleSoft,
74
+ boxShadow: `inset 0 0 0 1px ${tokens_1.contract.ringPurpleSoft}`,
75
+ },
66
76
  },
67
77
  size: {
68
78
  small: {
@@ -38,13 +38,17 @@ exports.BulkBar = BulkBar;
38
38
  const jsx_runtime_1 = require("react/jsx-runtime");
39
39
  const lucide_react_1 = require("lucide-react");
40
40
  const React = __importStar(require("react"));
41
+ const core_1 = require("../../core");
41
42
  const bulkbar_recipe_1 = require("./bulkbar.recipe");
42
43
  /**
43
44
  * A floating bulk-action bar. Rises from the bottom-centre of its nearest
44
45
  * positioned ancestor when a selection is active; render it conditionally
45
46
  * (e.g. `count > 0 && <BulkBar …/>`). Measured from a records table.
46
47
  */
47
- function BulkBar({ count, onClear, countLabel = "Selected", countText = (count, label) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [count, " ", label] })), children, className, clearLabel = "Clear selection", ...rest }) {
48
+ function BulkBar({ count, onClear, countLabel: countLabelProp, countText = (count, label) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [count, " ", label] })), children, className, clearLabel: clearLabelProp, ...rest }) {
49
+ // crisp/#471: same resolution order as every other component's own.
50
+ const countLabel = (0, core_1.useCrispMessage)(countLabelProp, "Selected");
51
+ const clearLabel = (0, core_1.useCrispMessage)(clearLabelProp, "Clear selection");
48
52
  return ((0, jsx_runtime_1.jsx)("div", { className: (0, bulkbar_recipe_1.bulkbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-bulkbar-bar", children: [count != null && ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-bulkbar-count", onClick: onClear, title: clearLabel, children: [countText(count, countLabel), (0, jsx_runtime_1.jsx)("span", { className: "crisp-bulkbar-clearlabel", children: clearLabel }), (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
49
53
  }
50
54
  BulkBar.displayName = "BulkBar";
@@ -93,10 +93,26 @@ function FileUpload({ className, labels, "aria-label": ariaLabel, id, accept, di
93
93
  onAcceptedFileEntriesChange,
94
94
  };
95
95
  const [rejections, setRejections] = React.useState([]);
96
+ // crisp/#471: same resolution order as every other component's own —
97
+ // explicit `labels` entry wins, then a CrispMessagesProvider's
98
+ // translation of the built-in English default, then that default itself.
99
+ const title = (0, core_1.useCrispMessage)(labels?.title, exports.fileUploadLabels.title);
100
+ const hint = (0, core_1.useCrispMessage)(labels?.hint, exports.fileUploadLabels.hint);
101
+ const remove = (0, core_1.useCrispMessage)(labels?.remove, exports.fileUploadLabels.remove);
102
+ const crispMessages = (0, core_1.useCrispMessages)();
96
103
  const t = {
97
- ...exports.fileUploadLabels,
98
- ...labels,
99
- errors: { ...exports.fileUploadLabels.errors, ...labels?.errors },
104
+ title,
105
+ hint,
106
+ remove,
107
+ errors: Object.fromEntries(Object.keys(exports.fileUploadLabels.errors).map((code) => {
108
+ const englishDefault = exports.fileUploadLabels.errors[code];
109
+ return [
110
+ code,
111
+ labels?.errors?.[code] ??
112
+ crispMessages[englishDefault] ??
113
+ englishDefault,
114
+ ];
115
+ })),
100
116
  };
101
117
  const api = (0, react_file_upload_1.useFileUpload)({
102
118
  ...props,
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.FormField = FormField;
37
37
  const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const React = __importStar(require("react"));
39
+ const core_1 = require("../../core");
39
40
  const formfield_recipe_1 = require("./formfield.recipe");
40
41
  /**
41
42
  * Tell the control what the field knows about it.
@@ -96,12 +97,18 @@ function useFieldWiring({ htmlFor, messageId, invalid, required, }) {
96
97
  function FormField({ label, htmlFor, required, optional, description, error, children, className, ...rest }) {
97
98
  const descId = React.useId();
98
99
  const hasMsg = error != null || description != null;
100
+ // crisp/#471: `optional` doubles as a boolean flag and a ReactNode
101
+ // override — passing any node instead of `true` already renders it
102
+ // verbatim below, with no lookup needed. `true` reads a
103
+ // CrispMessagesProvider's translation of the built-in default text, same
104
+ // resolution order as every other component's own.
105
+ const optionalDefault = (0, core_1.useCrispMessage)(undefined, "(optional)");
99
106
  useFieldWiring({
100
107
  htmlFor,
101
108
  messageId: hasMsg ? descId : undefined,
102
109
  invalid: error != null,
103
110
  required: !!required,
104
111
  });
105
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, formfield_recipe_1.formfieldRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("label", { className: "crisp-formfield-label", htmlFor: htmlFor, children: [label, required && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-formfield-required", "aria-hidden": "true", children: "*" })), optional && !required && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-formfield-optional", children: optional === true ? "(optional)" : optional }))] }), children, error != null ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-formfield-error", id: hasMsg ? descId : undefined, children: error })) : description != null ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-formfield-desc", id: hasMsg ? descId : undefined, children: description })) : null] }));
112
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, formfield_recipe_1.formfieldRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("label", { className: "crisp-formfield-label", htmlFor: htmlFor, children: [label, required && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-formfield-required", "aria-hidden": "true", children: "*" })), optional && !required && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-formfield-optional", children: optional === true ? optionalDefault : optional }))] }), children, error != null ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-formfield-error", id: hasMsg ? descId : undefined, children: error })) : description != null ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-formfield-desc", id: hasMsg ? descId : undefined, children: description })) : null] }));
106
113
  }
107
114
  FormField.displayName = "FormField";
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HeatmapChart = HeatmapChart;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const core_1 = require("../../core");
5
6
  const heatmapchart_recipe_1 = require("./heatmapchart.recipe");
6
7
  /**
7
8
  * An NxM cell-matrix chart — GRC's own risk heatmap (impact x likelihood)
@@ -10,9 +11,20 @@ const heatmapchart_recipe_1 = require("./heatmapchart.recipe");
10
11
  * the recipe's own doc comment for why (and for its measurement — or lack
11
12
  * of one — against Attio).
12
13
  */
13
- function HeatmapChart({ data, rowAxis, colAxis, format = (cell) => (cell.value ? cell.value : ""), cellTitle, onCellClick, isCellClickable = (cell) => cell.value > 0, empty, emptyLabel = "Nothing to plot yet.", "aria-label": ariaLabel, className, ...rest }) {
14
+ 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 }) {
14
15
  const cols = data[0]?.length ?? 0;
15
- const summary = ariaLabel ?? `Heatmap: ${data.length} x ${cols} grid`;
16
+ // crisp/#471: `emptyLabel` is typed `React.ReactNode`, so it resolves by
17
+ // hand rather than through `useCrispMessage`, which is string-only. The
18
+ // default `aria-label` is a sentence built from two static fragments
19
+ // around live data (row/column counts, not translatable), so each
20
+ // fragment gets its own lookup — same as TableToolbar's "Sorted by"
21
+ // (crisp/#217).
22
+ const crispMessages = (0, core_1.useCrispMessages)();
23
+ const emptyLabel = emptyLabelProp ??
24
+ crispMessages["Nothing to plot yet."] ??
25
+ "Nothing to plot yet.";
26
+ const summary = ariaLabel ??
27
+ `${crispMessages.Heatmap ?? "Heatmap"}: ${data.length} ${crispMessages.x ?? "x"} ${cols} ${crispMessages.grid ?? "grid"}`;
16
28
  return ((0, jsx_runtime_1.jsx)("div", { className: (0, heatmapchart_recipe_1.heatmapchartRecipe)({ className }), ...rest, role: "img", "aria-label": summary, children: empty ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-heatmapchart-empty", children: emptyLabel })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-heatmapchart-rowaxis", children: [(0, jsx_runtime_1.jsx)("span", { children: rowAxis.high }), rowAxis.title != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-heatmapchart-rowaxis-title", children: rowAxis.title })), (0, jsx_runtime_1.jsx)("span", { children: rowAxis.low })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-heatmapchart-body", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-heatmapchart-grid", style: { gridTemplateColumns: `repeat(${cols}, 1fr)` }, children: data.map((row, r) => row.map((cell, cc) => {
17
29
  const clickable = onCellClick != null && isCellClickable(cell, r, cc);
18
30
  const title = cellTitle?.(cell, r, cc);
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./skiplink"), exports);
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SkipLink = SkipLink;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const skiplink_recipe_1 = require("./skiplink.recipe");
6
+ /**
7
+ * A "skip to content" link — invisible until it is the thing with focus
8
+ * (a sighted mouse user never sees it), so a keyboard user's first Tab from
9
+ * a cold load reaches it instead of walking the whole nav first. Render it
10
+ * as the very first thing on the page, before the sidebar/header. See the
11
+ * recipe's own doc comment for why there's no Attio value behind its chrome.
12
+ */
13
+ function SkipLink({ href = "#main-content", children = "Skip to content", className, ...rest }) {
14
+ return ((0, jsx_runtime_1.jsx)("a", { href: href, className: (0, skiplink_recipe_1.skiplinkRecipe)({ className }), ...rest, children: children }));
15
+ }
16
+ SkipLink.displayName = "SkipLink";
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.skiplinkRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // A "skip to content" link: invisible (the same sr-only clip
7
+ // VisuallyHidden uses) until it is the thing with focus, so a sighted
8
+ // mouse user never sees it but a keyboard user's first Tab from a cold
9
+ // load reaches it. No Attio screen ships one to measure -- this is
10
+ // boilerplate accessibility furniture, not a design value (crisp#462 GRC
11
+ // audit gap 2 of 4, crisp#540) -- so the un-hidden chrome reuses already-measured
12
+ // crisp values throughout: the `bgPopover` floating surface + its
13
+ // `elevation.floating` shadow (the same pairing Menu/Popover/ContextMenu/
14
+ // Toast use for "a surface that appears above everything else"), `radius.md`,
15
+ // and the canonical `:focus-visible` ring every other crisp control uses
16
+ // (`outline: 2px solid ${bgBrandSolid}`, 1px offset) -- no new value is
17
+ // introduced.
18
+ const config = (0, core_1.defineRecipe)({
19
+ name: "skiplink",
20
+ 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;
21
+ font-family: ${tokens_1.contract.font.sans}; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary};
22
+
23
+ &:focus {
24
+ position: fixed; top: ${tokens_1.contract.space["3"]}; inset-inline-start: ${tokens_1.contract.space["3"]}; z-index: 90; width: auto; height: auto; margin: 0; overflow: visible; clip: auto; white-space: normal;
25
+ padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["3"]}; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgPopover}; box-shadow: ${tokens_1.elevation.floating};
26
+ }
27
+ &:focus-visible { outline: 2px solid ${tokens_1.contract.bgBrandSolid}; outline-offset: 1px; }`,
28
+ variants: {},
29
+ defaultVariants: {},
30
+ compoundVariants: [],
31
+ });
32
+ exports.skiplinkRecipe = (0, core_1.resolveRecipe)(config);
33
+ exports.default = config;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.textlinkRecipe = exports.TextLink = void 0;
4
+ var textlink_1 = require("./textlink");
5
+ Object.defineProperty(exports, "TextLink", { enumerable: true, get: function () { return textlink_1.TextLink; } });
6
+ var textlink_recipe_1 = require("./textlink.recipe");
7
+ Object.defineProperty(exports, "textlinkRecipe", { enumerable: true, get: function () { return textlink_recipe_1.textlinkRecipe; } });
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TextLink = TextLink;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const textlink_recipe_1 = require("./textlink.recipe");
6
+ /**
7
+ * Inline navigable text — a real `<a href>` always, the same href+onClick
8
+ * division `StatList` uses: `href` gives ordinary browser behaviour (new
9
+ * tab, copy link, hover status), an `onClick` that calls
10
+ * `event.preventDefault()` pairs it with client-side routing (e.g.
11
+ * react-router's own `Link`).
12
+ */
13
+ function TextLink({ tone = "muted", className, ...rest }) {
14
+ return (0, jsx_runtime_1.jsx)("a", { className: (0, textlink_recipe_1.textlinkRecipe)({ tone, className }), ...rest });
15
+ }
16
+ TextLink.displayName = "TextLink";
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.textlinkRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // The one inline-navigable-text shape GRC's own screens kept hand-typing at
7
+ // every call site (internal/grc#110 / your-moon/crisp#529): a card's own
8
+ // trailing "View all →", and a muted in-context reference link that goes
9
+ // brand on hover. No new colour or size here — both variants read the exact
10
+ // tokens those call sites already used (`bgBrandSolid` reused as a text
11
+ // colour, the same alias GRC's own `--color-fg-brand` resolves to; `fgMuted`;
12
+ // the existing 12px/13px text sizes) rather than a fresh guess, and neither
13
+ // is measured against one specific Attio widget for the same reason
14
+ // StatList/HeatmapChart's own doc comments give: this exact "small trailing
15
+ // link" isn't a distinct Attio component to point a CDP session at, it's a
16
+ // reuse of already-measured type and colour values in a new arrangement.
17
+ const config = (0, core_1.defineRecipe)({
18
+ name: "textlink",
19
+ base: `display: inline; font-family: ${tokens_1.contract.font.sans}; text-decoration: none; cursor: pointer;
20
+ &:hover { text-decoration: underline; }`,
21
+ variants: {
22
+ tone: {
23
+ // The bold, always-brand-coloured trailing link a Card's own `action`
24
+ // slot uses ("View all →", "All registers →").
25
+ brand: `font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.bgBrandSolid};`,
26
+ // A quieter in-context reference (a sample record under a finding)
27
+ // that only goes brand on hover.
28
+ muted: `font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.regular}; color: ${tokens_1.contract.fgMuted}; &:hover { color: ${tokens_1.contract.bgBrandSolid}; }`,
29
+ },
30
+ },
31
+ defaultVariants: { tone: "muted" },
32
+ compoundVariants: [],
33
+ });
34
+ exports.textlinkRecipe = (0, core_1.resolveRecipe)(config);
35
+ exports.default = config;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./visuallyhidden"), exports);
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VisuallyHidden = VisuallyHidden;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const visuallyhidden_recipe_1 = require("./visuallyhidden.recipe");
6
+ /**
7
+ * Hides content visually while keeping it in the accessibility tree and
8
+ * the tab order — a label for an icon-only control, extra context for a
9
+ * screen-reader user that a sighted one gets from layout alone. Renders a
10
+ * `<span>`; wrap the element that needs the treatment, or read its content
11
+ * as a `label`/`aria-label` source in a test.
12
+ */
13
+ function VisuallyHidden({ className, ...rest }) {
14
+ return (0, jsx_runtime_1.jsx)("span", { className: (0, visuallyhidden_recipe_1.visuallyhiddenRecipe)({ className }), ...rest });
15
+ }
16
+ VisuallyHidden.displayName = "VisuallyHidden";
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.visuallyhiddenRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ // The standard "sr-only" clip pattern: content stays in the accessibility
6
+ // tree and the tab order, but occupies no visible space and is never
7
+ // painted. Pure CSS mechanics, not a design value -- there is nothing to
8
+ // measure against Attio here (crisp#462 GRC audit gap 2 of 4, crisp#540):
9
+ // no Attio screen ships content that is deliberately invisible-but-reachable,
10
+ // so this is the standard web-platform pattern, same shape `SkipLink` also
11
+ // starts from before it un-hides on focus.
12
+ const config = (0, core_1.defineRecipe)({
13
+ name: "visuallyhidden",
14
+ base: `position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;`,
15
+ variants: {},
16
+ defaultVariants: {},
17
+ compoundVariants: [],
18
+ });
19
+ exports.visuallyhiddenRecipe = (0, core_1.resolveRecipe)(config);
20
+ exports.default = config;
package/dist/cjs/index.js CHANGED
@@ -60,6 +60,7 @@ __exportStar(require("./components/resizablepanel"), exports);
60
60
  __exportStar(require("./components/segmented"), exports);
61
61
  __exportStar(require("./components/select"), exports);
62
62
  __exportStar(require("./components/skeleton"), exports);
63
+ __exportStar(require("./components/skiplink"), exports);
63
64
  __exportStar(require("./components/slider"), exports);
64
65
  __exportStar(require("./components/spinner"), exports);
65
66
  __exportStar(require("./components/stagebar"), exports);
@@ -69,21 +70,25 @@ __exportStar(require("./components/table"), exports);
69
70
  __exportStar(require("./components/tabletoolbar"), exports);
70
71
  __exportStar(require("./components/tabs"), exports);
71
72
  __exportStar(require("./components/textarea"), exports);
73
+ __exportStar(require("./components/textlink"), exports);
72
74
  __exportStar(require("./components/timepicker"), exports);
73
75
  __exportStar(require("./components/toast"), exports);
74
76
  __exportStar(require("./components/tooltip"), exports);
77
+ __exportStar(require("./components/visuallyhidden"), exports);
75
78
  __exportStar(require("./components/wheelpicker"), exports);
76
79
  __exportStar(require("./core"), exports);
77
80
  // product surfaces (product/) — faithful, pixel-measured rebuilds of specific
78
81
  // product screens (Sidebar, RecordsTable, RecordPage, Inbox, …). Use them as-is
79
82
  // or as composition references for your own data; `components/` above holds
80
83
  // the generic primitives (Table, DataTable, Button, …) to build your own UI from.
84
+ __exportStar(require("./product/activityfeed"), exports);
81
85
  __exportStar(require("./product/appshell"), exports);
82
86
  __exportStar(require("./product/authshell"), exports);
83
87
  __exportStar(require("./product/board"), exports);
84
88
  __exportStar(require("./product/command"), exports);
85
89
  __exportStar(require("./product/entitycard"), exports);
86
90
  __exportStar(require("./product/flowcanvas"), exports);
91
+ __exportStar(require("./product/headerbar"), exports);
87
92
  __exportStar(require("./product/inbox"), exports);
88
93
  __exportStar(require("./product/listpage"), exports);
89
94
  __exportStar(require("./product/navitem"), exports);
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ActivityFeed = ActivityFeed;
4
+ exports.ActivityFeedRow = ActivityFeedRow;
5
+ exports.ActivityFeedDiffItem = ActivityFeedDiffItem;
6
+ exports.ActivityFeedDiffList = ActivityFeedDiffList;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const activityfeed_recipe_1 = require("./activityfeed.recipe");
9
+ /**
10
+ * A record's own event timeline — a vertically stacked, bordered list of
11
+ * `ActivityFeedRow`s, capped at a readable content width. Built for
12
+ * internal/grc's per-record "Activity" tab (crisp#462 SHELL/forms gap): it
13
+ * replaces a hand-rolled `<div className="mx-auto max-w-3xl space-y-4
14
+ * p-6">`. No live Attio re-measurement behind its own chrome this pass (the
15
+ * CDP bridge was unreachable when this was built) — see the recipe's own
16
+ * doc comment for which already-measured crisp values it reuses instead of
17
+ * a new guess.
18
+ */
19
+ function ActivityFeed({ className, children, ...rest }) {
20
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, activityfeed_recipe_1.activityfeedRecipe)({ className }), ...rest, children: children }));
21
+ }
22
+ ActivityFeed.displayName = "ActivityFeed";
23
+ /** One event row: actor + action + timestamp, with an optional diff list
24
+ * underneath. The last row in an `ActivityFeed` draws no divider. */
25
+ function ActivityFeedRow({ actor, action, when, children, }) {
26
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-activityfeed-row", children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-activityfeed-row-head", children: [actor, action, (0, jsx_runtime_1.jsx)("span", { className: "crisp-activityfeed-row-spacer" }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-activityfeed-row-when", children: when })] }), children] }));
27
+ }
28
+ ActivityFeedRow.displayName = "ActivityFeedRow";
29
+ /** One changed-field line under an `ActivityFeedRow` — "Field: before →
30
+ * after", or "Field: set to after" when there is no known prior value. */
31
+ function ActivityFeedDiffItem({ field, before, after, }) {
32
+ return ((0, jsx_runtime_1.jsxs)("li", { className: "crisp-activityfeed-diff-item", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-activityfeed-diff-field", children: field }), " ", before !== undefined ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-activityfeed-diff-before", children: before }), " ", (0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", children: "\u2192" }), " ", (0, jsx_runtime_1.jsx)("span", { children: after })] })) : ((0, jsx_runtime_1.jsx)("span", { children: after }))] }));
33
+ }
34
+ ActivityFeedDiffItem.displayName = "ActivityFeedDiffItem";
35
+ function ActivityFeedDiffList({ children, }) {
36
+ return (0, jsx_runtime_1.jsx)("ul", { className: "crisp-activityfeed-diff", children: children });
37
+ }
38
+ ActivityFeedDiffList.displayName = "ActivityFeedDiffList";
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.activityfeedRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // A record's own event timeline (crisp#462 gap: internal/grc's ActivityTab
7
+ // hand-rolled this as a bare `<div className="mx-auto max-w-3xl space-y-4
8
+ // p-6">` list of bordered rows). No live Attio re-measurement behind these
9
+ // values this pass -- the CDP bridge to the owner's Vivaldi was unreachable
10
+ // when this was built (documented in the component's own doc comment) --
11
+ // so this reuses already-measured crisp values throughout rather than a new
12
+ // guess: `space["6"]` (24px) padding is RecordPanel's own content padding,
13
+ // the row's bottom border reuses `borderSubtle` (the same divider every
14
+ // other bordered row in this codebase draws with), and the 767px content
15
+ // cap matches SettingsSection's own measured content width.
16
+ const config = (0, core_1.defineRecipe)({
17
+ name: "activityfeed",
18
+ base: `box-sizing: border-box; max-width: 767px; margin-inline: auto; padding: ${tokens_1.contract.space["6"]};
19
+ display: flex; flex-direction: column; gap: ${tokens_1.contract.space["4"]};
20
+ & .crisp-activityfeed-row { display: flex; flex-direction: column; gap: ${tokens_1.contract.space["2"]}; padding-bottom: ${tokens_1.contract.space["4"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; }
21
+ & .crisp-activityfeed-row:last-of-type { border-bottom: none; padding-bottom: 0; }
22
+ & .crisp-activityfeed-row-head { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; }
23
+ & .crisp-activityfeed-row-spacer { flex: 1; }
24
+ & .crisp-activityfeed-row-when { font-size: ${tokens_1.contract.text.xs}; color: ${tokens_1.contract.fgMuted}; white-space: nowrap; }
25
+ & .crisp-activityfeed-diff { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["1"]}; }
26
+ & .crisp-activityfeed-diff-item { padding-inline-start: ${tokens_1.contract.space["8"]}; font-size: ${tokens_1.contract.text.md}; }
27
+ & .crisp-activityfeed-diff-field { color: ${tokens_1.contract.fgMuted}; }
28
+ & .crisp-activityfeed-diff-before { color: ${tokens_1.contract.fgMuted}; text-decoration: line-through; }
29
+ `,
30
+ variants: {},
31
+ defaultVariants: {},
32
+ compoundVariants: [],
33
+ });
34
+ exports.activityfeedRecipe = (0, core_1.resolveRecipe)(config);
35
+ exports.default = config;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.activityfeedRecipe = exports.ActivityFeedRow = exports.ActivityFeedDiffList = exports.ActivityFeedDiffItem = exports.ActivityFeed = void 0;
4
+ var activityfeed_1 = require("./activityfeed");
5
+ Object.defineProperty(exports, "ActivityFeed", { enumerable: true, get: function () { return activityfeed_1.ActivityFeed; } });
6
+ Object.defineProperty(exports, "ActivityFeedDiffItem", { enumerable: true, get: function () { return activityfeed_1.ActivityFeedDiffItem; } });
7
+ Object.defineProperty(exports, "ActivityFeedDiffList", { enumerable: true, get: function () { return activityfeed_1.ActivityFeedDiffList; } });
8
+ Object.defineProperty(exports, "ActivityFeedRow", { enumerable: true, get: function () { return activityfeed_1.ActivityFeedRow; } });
9
+ var activityfeed_recipe_1 = require("./activityfeed.recipe");
10
+ Object.defineProperty(exports, "activityfeedRecipe", { enumerable: true, get: function () { return activityfeed_recipe_1.activityfeedRecipe; } });
@@ -44,7 +44,16 @@ const board_recipe_1 = require("./board.recipe");
44
44
  * (the parent owns the cards, so the move is a controlled state update).
45
45
  * Measured from a deals pipeline.
46
46
  */
47
- function Board({ columns, cards, onCardsChange, renderCard, onNewCard, newCardLabel = "New", getCardLabel, announceMove = (column, card) => card ? `${card} moved to ${column}` : `Moved to ${column}`, className, ...rest }) {
47
+ function Board({ columns, cards, onCardsChange, renderCard, onNewCard, newCardLabel: newCardLabelProp, getCardLabel, announceMove: announceMoveProp, className, ...rest }) {
48
+ // crisp/#471: same resolution order as every other component's own —
49
+ // explicit prop, then a CrispMessagesProvider's translation of the English
50
+ // default, then that default itself.
51
+ const newCardLabel = (0, core_1.useCrispMessage)(newCardLabelProp, "New");
52
+ const crispMessages = (0, core_1.useCrispMessages)();
53
+ const announceMove = announceMoveProp ??
54
+ ((column, card) => card
55
+ ? `${card} ${crispMessages["moved to"] ?? "moved to"} ${column}`
56
+ : `${crispMessages["Moved to"] ?? "Moved to"} ${column}`);
48
57
  const [dragId, setDragId] = React.useState(null);
49
58
  const [overCol, setOverCol] = React.useState(null);
50
59
  const [announcement, setAnnouncement] = React.useState("");
@@ -101,7 +110,7 @@ function Board({ columns, cards, onCardsChange, renderCard, onNewCard, newCardLa
101
110
  move(dragId, col.id);
102
111
  setDragId(null);
103
112
  setOverCol(null);
104
- }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-board-col-header", children: [col.accent && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-board-col-dot", style: { background: col.accent } })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-board-col-title", children: col.title }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-board-col-count", children: colCards.length }), onNewCard && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-board-col-add", "aria-label": `Add to ${col.title}`, onClick: () => onNewCard(col.id), children: "+" }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-board-col-body", role: "list", "aria-label": col.title, children: [onNewCard && ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-board-newcard", onClick: () => onNewCard(col.id), children: ["+ ", newCardLabel] })), colCards.map((card) => ((0, jsx_runtime_1.jsx)("div", { className: "crisp-board-card", role: "listitem",
113
+ }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-board-col-header", children: [col.accent && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-board-col-dot", style: { background: col.accent } })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-board-col-title", children: col.title }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-board-col-count", children: colCards.length }), onNewCard && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-board-col-add", "aria-label": `${crispMessages["Add to"] ?? "Add to"} ${col.title}`, onClick: () => onNewCard(col.id), children: "+" }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-board-col-body", role: "list", "aria-label": col.title, children: [onNewCard && ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-board-newcard", onClick: () => onNewCard(col.id), children: ["+ ", newCardLabel] })), colCards.map((card) => ((0, jsx_runtime_1.jsx)("div", { className: "crisp-board-card", role: "listitem",
105
114
  // 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
106
115
  tabIndex: 0, "aria-label": getCardLabel?.(card), "data-card-id": card.id, draggable: true, "data-dragging": dragId === card.id ? "" : undefined, onDragStart: () => setDragId(card.id), onDragEnd: () => {
107
116
  setDragId(null);
@@ -147,7 +147,19 @@ function useCommandInstanceId() {
147
147
  * option list) so keyboard/focus correctness comes from the machines, not
148
148
  * hand-rolled event handling.
149
149
  */
150
- function Command({ open, onOpenChange, items = exports.defaultCommandItems, placeholder = "Search actions and records…", "aria-label": ariaLabel = "Command palette", navigateLabel = "Navigate", selectLabel = "Select", emptyLabel = "No matching commands", query: queryProp, onQueryChange, filterItems = true, className, "aria-describedby": describedBy, ...rest }) {
150
+ function Command({ open, onOpenChange, items = exports.defaultCommandItems, placeholder: placeholderProp, "aria-label": ariaLabelProp, navigateLabel: navigateLabelProp, selectLabel: selectLabelProp, emptyLabel: emptyLabelProp, query: queryProp, onQueryChange, filterItems = true, className, "aria-describedby": describedBy, ...rest }) {
151
+ // crisp/#471: same resolution order as every other component's own.
152
+ // `navigateLabel`/`emptyLabel` are typed `React.ReactNode`, so they
153
+ // resolve by hand rather than through `useCrispMessage`, which is
154
+ // string-only.
155
+ const crispMessages = (0, core_1.useCrispMessages)();
156
+ const placeholder = (0, core_1.useCrispMessage)(placeholderProp, "Search actions and records…");
157
+ const ariaLabel = (0, core_1.useCrispMessage)(ariaLabelProp, "Command palette");
158
+ const navigateLabel = navigateLabelProp ?? crispMessages.Navigate ?? "Navigate";
159
+ const selectLabel = (0, core_1.useCrispMessage)(selectLabelProp, "Select");
160
+ const emptyLabel = emptyLabelProp ??
161
+ crispMessages["No matching commands"] ??
162
+ "No matching commands";
151
163
  const instanceId = useCommandInstanceId();
152
164
  const dialogId = `${instanceId}-dialog`;
153
165
  const comboId = `${instanceId}-combobox`;
@@ -37,6 +37,7 @@ exports.FlowCanvas = FlowCanvas;
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 flowcanvas_recipe_1 = require("./flowcanvas.recipe");
41
42
  const GRID = 20;
42
43
  const clampZoom = (z) => Math.min(1.5, Math.max(0.5, z));
@@ -45,7 +46,13 @@ const clampZoom = (z) => Math.min(1.5, Math.max(0.5, z));
45
46
  * measured from a workflows editor. Drag a node to move it (fires
46
47
  * `onNodesChange`); drag the background to pan; use the toolbar to zoom.
47
48
  */
48
- function FlowCanvas({ nodes, edges, onNodesChange, renderNode, nodeWidth = 240, nodeHeight = 64, "aria-label": ariaLabel, className, zoomOutLabel = "Zoom out", zoomInLabel = "Zoom in", resetLabel = "Reset", edgesLabel = "Edges", ...rest }) {
49
+ function FlowCanvas({ nodes, edges, onNodesChange, renderNode, nodeWidth = 240, nodeHeight = 64, "aria-label": ariaLabelProp, className, zoomOutLabel: zoomOutLabelProp, zoomInLabel: zoomInLabelProp, resetLabel: resetLabelProp, edgesLabel: edgesLabelProp, ...rest }) {
50
+ // crisp/#471: same resolution order as every other component's own.
51
+ const ariaLabel = (0, core_1.useCrispMessage)(ariaLabelProp, "Flow canvas");
52
+ const zoomOutLabel = (0, core_1.useCrispMessage)(zoomOutLabelProp, "Zoom out");
53
+ const zoomInLabel = (0, core_1.useCrispMessage)(zoomInLabelProp, "Zoom in");
54
+ const resetLabel = (0, core_1.useCrispMessage)(resetLabelProp, "Reset");
55
+ const edgesLabel = (0, core_1.useCrispMessage)(edgesLabelProp, "Edges");
49
56
  const [pan, setPan] = React.useState({ x: 40, y: 40 });
50
57
  const [zoom, setZoom] = React.useState(1);
51
58
  const [panning, setPanning] = React.useState(false);
@@ -100,7 +107,7 @@ function FlowCanvas({ nodes, edges, onNodesChange, renderNode, nodeWidth = 240,
100
107
  ...rest.style,
101
108
  backgroundSize: `${GRID * zoom}px ${GRID * zoom}px`,
102
109
  backgroundPosition: `${pan.x}px ${pan.y}px`,
103
- }, role: "application", "aria-label": ariaLabel ?? "Flow canvas", onPointerDown: onBackgroundPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerUp, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-flowcanvas-viewport", style: {
110
+ }, role: "application", "aria-label": ariaLabel, onPointerDown: onBackgroundPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerUp, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-flowcanvas-viewport", style: {
104
111
  transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
105
112
  }, children: [(0, jsx_runtime_1.jsxs)("svg", { className: "crisp-flowcanvas-edges", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("title", { children: edgesLabel }), edges.map((edge) => {
106
113
  const a = byId.get(edge.from);