@crisp-ui-kit/crisp 0.57.0 → 0.57.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 (96) hide show
  1. package/dist/accentpicker.css +5 -0
  2. package/dist/accentpicker.d.ts +7 -0
  3. package/dist/accentpicker.layered.css +7 -0
  4. package/dist/avatar.css +1 -1
  5. package/dist/avatar.layered.css +1 -1
  6. package/dist/badge.css +2 -2
  7. package/dist/badge.layered.css +2 -2
  8. package/dist/button.css +3 -3
  9. package/dist/button.layered.css +3 -3
  10. package/dist/cjs/components/accentpicker/accentpicker.js +66 -0
  11. package/dist/cjs/components/accentpicker/accentpicker.recipe.js +25 -0
  12. package/dist/cjs/components/accentpicker/index.js +7 -0
  13. package/dist/cjs/components/avatar/avatar.recipe.js +4 -1
  14. package/dist/cjs/components/badge/badge.recipe.js +16 -2
  15. package/dist/cjs/components/button/button.recipe.js +16 -6
  16. package/dist/cjs/components/input/input.js +2 -2
  17. package/dist/cjs/components/input/input.recipe.js +8 -0
  18. package/dist/cjs/components/multiselect/multiselect.js +1 -1
  19. package/dist/cjs/components/multiselect/multiselect.recipe.js +12 -3
  20. package/dist/cjs/components/progress/progress.js +21 -4
  21. package/dist/cjs/components/progress/progress.recipe.js +30 -0
  22. package/dist/cjs/components/select/select.js +2 -2
  23. package/dist/cjs/components/select/select.recipe.js +32 -5
  24. package/dist/cjs/components/text/text.recipe.js +8 -7
  25. package/dist/cjs/components/textarea/textarea.js +3 -2
  26. package/dist/cjs/components/textarea/textarea.recipe.js +2 -1
  27. package/dist/cjs/components/textlink/textlink.recipe.js +6 -3
  28. package/dist/cjs/index.js +1 -0
  29. package/dist/cjs/product/recordpage/recordpage.js +46 -5
  30. package/dist/cjs/tokens/palette.js +79 -25
  31. package/dist/cjs/tokens/scale.js +1 -0
  32. package/dist/cjs/tokens/semantic.js +24 -0
  33. package/dist/esm/components/accentpicker/accentpicker.js +30 -0
  34. package/dist/esm/components/accentpicker/accentpicker.recipe.js +22 -0
  35. package/dist/esm/components/accentpicker/index.js +2 -0
  36. package/dist/esm/components/avatar/avatar.recipe.js +4 -1
  37. package/dist/esm/components/badge/badge.recipe.js +16 -2
  38. package/dist/esm/components/button/button.recipe.js +16 -6
  39. package/dist/esm/components/input/input.js +3 -3
  40. package/dist/esm/components/input/input.recipe.js +8 -0
  41. package/dist/esm/components/multiselect/multiselect.js +1 -1
  42. package/dist/esm/components/multiselect/multiselect.recipe.js +12 -3
  43. package/dist/esm/components/progress/progress.js +22 -5
  44. package/dist/esm/components/progress/progress.recipe.js +30 -0
  45. package/dist/esm/components/select/select.js +2 -2
  46. package/dist/esm/components/select/select.recipe.js +32 -5
  47. package/dist/esm/components/text/text.recipe.js +8 -7
  48. package/dist/esm/components/textarea/textarea.js +3 -2
  49. package/dist/esm/components/textarea/textarea.recipe.js +2 -1
  50. package/dist/esm/components/textlink/textlink.recipe.js +6 -3
  51. package/dist/esm/index.js +1 -0
  52. package/dist/esm/product/recordpage/recordpage.js +47 -6
  53. package/dist/esm/tokens/palette.js +79 -25
  54. package/dist/esm/tokens/scale.js +1 -0
  55. package/dist/esm/tokens/semantic.js +24 -0
  56. package/dist/input.css +7 -0
  57. package/dist/input.layered.css +7 -0
  58. package/dist/multiselect.css +4 -0
  59. package/dist/multiselect.d.ts +2 -2
  60. package/dist/multiselect.layered.css +4 -0
  61. package/dist/native/tokens.d.ts +1 -1
  62. package/dist/native/tokens.js +33 -22
  63. package/dist/progress.css +3 -0
  64. package/dist/progress.layered.css +3 -0
  65. package/dist/select.css +22 -2
  66. package/dist/select.d.ts +2 -2
  67. package/dist/select.layered.css +22 -2
  68. package/dist/styles.css +71 -23
  69. package/dist/styles.layered.css +71 -23
  70. package/dist/text.css +1 -1
  71. package/dist/text.layered.css +1 -1
  72. package/dist/textarea.css +2 -1
  73. package/dist/textarea.layered.css +2 -1
  74. package/dist/textlink.css +2 -2
  75. package/dist/textlink.layered.css +2 -2
  76. package/dist/tokens.css +18 -11
  77. package/dist/tokens.json +33 -22
  78. package/dist/types/components/accentpicker/accentpicker.d.ts +23 -0
  79. package/dist/types/components/accentpicker/accentpicker.recipe.d.ts +3 -0
  80. package/dist/types/components/accentpicker/index.d.ts +2 -0
  81. package/dist/types/components/button/button.recipe.d.ts +2 -2
  82. package/dist/types/components/input/input.d.ts +12 -0
  83. package/dist/types/components/multiselect/multiselect.d.ts +4 -3
  84. package/dist/types/components/multiselect/multiselect.recipe.d.ts +3 -0
  85. package/dist/types/components/progress/progress.d.ts +16 -4
  86. package/dist/types/components/progress/progress.recipe.d.ts +22 -0
  87. package/dist/types/components/select/select.d.ts +6 -5
  88. package/dist/types/components/select/select.recipe.d.ts +3 -0
  89. package/dist/types/components/textarea/textarea.d.ts +16 -0
  90. package/dist/types/index.d.ts +1 -0
  91. package/dist/types/product/recordpage/recordpage.d.ts +37 -1
  92. package/dist/types/tokens/contract.d.ts +4 -1
  93. package/dist/types/tokens/palette.d.ts +19 -11
  94. package/dist/types/tokens/scale.d.ts +1 -0
  95. package/dist/types/tokens/semantic.d.ts +15 -0
  96. package/package.json +6 -1
@@ -0,0 +1,5 @@
1
+ .crisp-accentpicker {
2
+ display: flex; gap: var(--crisp-space-2);
3
+ & .crisp-accentpicker-swatch { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; margin: 0; border-radius: var(--crisp-radius-full); border: 1px solid transparent; background: none; font-family: inherit; cursor: pointer; }
4
+ & .crisp-accentpicker-swatch:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 2px; }
5
+ }
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface AccentpickerVariants {
4
+
5
+ }
6
+
7
+ export declare const accentpickerRecipe: RecipeFn<{ }>;
@@ -0,0 +1,7 @@
1
+ @layer crisp-components {
2
+ .crisp-accentpicker {
3
+ display: flex; gap: var(--crisp-space-2);
4
+ & .crisp-accentpicker-swatch { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; margin: 0; border-radius: var(--crisp-radius-full); border: 1px solid transparent; background: none; font-family: inherit; cursor: pointer; }
5
+ & .crisp-accentpicker-swatch:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 2px; }
6
+ }
7
+ }
package/dist/avatar.css CHANGED
@@ -24,7 +24,7 @@
24
24
  background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
25
25
  }
26
26
  .crisp-avatar--tone_brand {
27
- background: var(--crisp-bg-brand-solid); color: #fff;
27
+ background: var(--crisp-bg-brand-contrast); color: #fff;
28
28
  }
29
29
  .crisp-avatar--tone_orange {
30
30
  background: var(--crisp-bg-orange); color: #fff;
@@ -25,7 +25,7 @@
25
25
  background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
26
26
  }
27
27
  .crisp-avatar--tone_brand {
28
- background: var(--crisp-bg-brand-solid); color: #fff;
28
+ background: var(--crisp-bg-brand-contrast); color: #fff;
29
29
  }
30
30
  .crisp-avatar--tone_orange {
31
31
  background: var(--crisp-bg-orange); color: #fff;
package/dist/badge.css CHANGED
@@ -36,13 +36,13 @@
36
36
 
37
37
  }
38
38
  .crisp-badge--compound_0 {
39
- background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); box-shadow: none;
39
+ background: var(--crisp-bg-brand-contrast); color: var(--crisp-fg-on-brand); box-shadow: none;
40
40
  }
41
41
  .crisp-badge--compound_1 {
42
42
  background: var(--crisp-bg-success); color: var(--crisp-fg-on-tone); box-shadow: none;
43
43
  }
44
44
  .crisp-badge--compound_2 {
45
- background: var(--crisp-bg-warning); color: var(--crisp-fg-on-tone); box-shadow: none;
45
+ background: var(--crisp-bg-warning-solid); color: var(--crisp-fg-on-tone); box-shadow: none;
46
46
  }
47
47
  .crisp-badge--compound_3 {
48
48
  background: var(--crisp-bg-danger); color: var(--crisp-fg-on-tone); box-shadow: none;
@@ -37,13 +37,13 @@
37
37
 
38
38
  }
39
39
  .crisp-badge--compound_0 {
40
- background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); box-shadow: none;
40
+ background: var(--crisp-bg-brand-contrast); color: var(--crisp-fg-on-brand); box-shadow: none;
41
41
  }
42
42
  .crisp-badge--compound_1 {
43
43
  background: var(--crisp-bg-success); color: var(--crisp-fg-on-tone); box-shadow: none;
44
44
  }
45
45
  .crisp-badge--compound_2 {
46
- background: var(--crisp-bg-warning); color: var(--crisp-fg-on-tone); box-shadow: none;
46
+ background: var(--crisp-bg-warning-solid); color: var(--crisp-fg-on-tone); box-shadow: none;
47
47
  }
48
48
  .crisp-badge--compound_3 {
49
49
  background: var(--crisp-bg-danger); color: var(--crisp-fg-on-tone); box-shadow: none;
package/dist/button.css CHANGED
@@ -3,9 +3,9 @@
3
3
  &:disabled { opacity: 0.5; cursor: not-allowed; }
4
4
  }
5
5
  .crisp-button--intent_primary {
6
- background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08);
7
- &:hover { background: var(--crisp-bg-brand-solid-pressed); }
8
- &:active { background: var(--crisp-bg-brand-solid-pressed); }
6
+ background: var(--crisp-bg-brand-contrast); color: var(--crisp-fg-on-brand); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08);
7
+ &:hover { filter: brightness(0.96); }
8
+ &:active { filter: brightness(0.92); }
9
9
  }
10
10
  .crisp-button--intent_neutral {
11
11
  background: var(--crisp-bg-surface); color: var(--crisp-fg-primary); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04);
@@ -4,9 +4,9 @@
4
4
  &:disabled { opacity: 0.5; cursor: not-allowed; }
5
5
  }
6
6
  .crisp-button--intent_primary {
7
- background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08);
8
- &:hover { background: var(--crisp-bg-brand-solid-pressed); }
9
- &:active { background: var(--crisp-bg-brand-solid-pressed); }
7
+ background: var(--crisp-bg-brand-contrast); color: var(--crisp-fg-on-brand); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08);
8
+ &:hover { filter: brightness(0.96); }
9
+ &:active { filter: brightness(0.92); }
10
10
  }
11
11
  .crisp-button--intent_neutral {
12
12
  background: var(--crisp-bg-surface); color: var(--crisp-fg-primary); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04);
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AccentPicker = AccentPicker;
37
+ const jsx_runtime_1 = require("react/jsx-runtime");
38
+ const React = __importStar(require("react"));
39
+ const radiogroup_1 = require("../radiogroup");
40
+ const accentpicker_recipe_1 = require("./accentpicker.recipe");
41
+ /**
42
+ * A row of selectable colour swatches — Attio's own accent-colour picker
43
+ * (Settings > Appearance). Not a fixed palette: pass the colours (and the
44
+ * accent isn't crisp's concern to theme the app with — this only picks one).
45
+ * Same roving-focus/arrow-key model every other crisp radio-like control
46
+ * shares (`useRadioGroupKeyboard`, also used by `RadioGroup` and
47
+ * `Segmented`).
48
+ */
49
+ function AccentPicker({ colors, value, defaultValue, onValueChange, className, onKeyDown: onKeyDownProp, ...rest }) {
50
+ const [internal, setInternal] = React.useState(defaultValue);
51
+ const selected = value ?? internal;
52
+ const select = (v) => {
53
+ if (value === undefined)
54
+ setInternal(v);
55
+ onValueChange?.(v);
56
+ };
57
+ const { onKeyDown, groupRef } = (0, radiogroup_1.useRadioGroupKeyboard)({ select });
58
+ return ((0, jsx_runtime_1.jsx)("div", { ref: groupRef, role: "radiogroup", className: (0, accentpicker_recipe_1.accentpickerRecipe)({ className }), ...rest, onKeyDown: (e) => {
59
+ onKeyDown(e);
60
+ onKeyDownProp?.(e);
61
+ }, children: colors.map((color) => {
62
+ const checked = selected === color;
63
+ return ((0, jsx_runtime_1.jsx)("button", { type: "button", role: "radio", "aria-checked": checked, "aria-label": color, "data-value": color, tabIndex: checked ? 0 : -1, className: "crisp-accentpicker-swatch", style: { borderColor: checked ? color : "transparent" }, onClick: () => select(color), children: (0, jsx_runtime_1.jsx)("svg", { width: "22", height: "22", viewBox: "0 0 22 22", fill: "none", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("circle", { cx: "11", cy: "11", r: "9", fill: color }) }) }, color));
64
+ }) }));
65
+ }
66
+ AccentPicker.displayName = "AccentPicker";
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.accentpickerRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // A row of selectable colour swatches — measured live on Attio's Appearance
7
+ // settings page (crisp#635): `role="radiogroup"`, `display: flex; gap: 8px`.
8
+ // Each swatch is a 24x24 circular button, `border: 1px solid transparent`
9
+ // at rest; the CHECKED swatch's border switches to its own colour (not a
10
+ // fixed brand ring) — measured directly on the DOM (`border: 1px solid
11
+ // rgb(38,109,240)` on the checked blue swatch, `1px solid rgba(0,0,0,0)` on
12
+ // every unchecked one). That per-swatch colour is data, not a class, so it's
13
+ // set inline by the component, not here. Inside sits a 22x22 SVG with one
14
+ // solid r=9 (18px) circle -- no separate ring/dot layer, contrary to an
15
+ // earlier, less precise read of this same control.
16
+ const config = (0, core_1.defineRecipe)({
17
+ name: "accentpicker",
18
+ base: `display: flex; gap: ${tokens_1.contract.space["2"]};
19
+ & .crisp-accentpicker-swatch { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center;
20
+ width: 24px; height: 24px; padding: 0; margin: 0; border-radius: ${tokens_1.contract.radius.full}; border: 1px solid transparent;
21
+ background: none; font-family: inherit; cursor: pointer; }
22
+ & .crisp-accentpicker-swatch:focus-visible { outline: 2px solid ${tokens_1.contract.bgBrandSolid}; outline-offset: 2px; }`,
23
+ });
24
+ exports.accentpickerRecipe = (0, core_1.resolveRecipe)(config);
25
+ exports.default = config;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.accentpickerRecipe = exports.AccentPicker = void 0;
4
+ var accentpicker_1 = require("./accentpicker");
5
+ Object.defineProperty(exports, "AccentPicker", { enumerable: true, get: function () { return accentpicker_1.AccentPicker; } });
6
+ var accentpicker_recipe_1 = require("./accentpicker.recipe");
7
+ Object.defineProperty(exports, "accentpickerRecipe", { enumerable: true, get: function () { return accentpicker_recipe_1.accentpickerRecipe; } });
@@ -39,7 +39,10 @@ const spec = (0, core_1.defineSpec)({
39
39
  color: tokens_1.contract.fgNeutralMuted,
40
40
  boxShadow: `inset 0 0 0 1px ${tokens_1.contract.borderDefault}`,
41
41
  },
42
- brand: { background: tokens_1.contract.bgBrandSolid, color: "#fff" },
42
+ // your-moon/crisp#897: `bgBrandContrast`, the darker AA-safe sibling
43
+ // of the accent blue (`bgBrandSolid`, #0298d9, only 3.23:1 with
44
+ // white) -- keeps white text, matching every other tone here.
45
+ brand: { background: tokens_1.contract.bgBrandContrast, color: "#fff" },
43
46
  orange: { background: tokens_1.contract.bgOrange, color: "#fff" },
44
47
  cyan: { background: tokens_1.contract.bgCyan, color: "#fff" },
45
48
  purple: { background: tokens_1.contract.bgPurple, color: "#fff" },
@@ -129,8 +129,14 @@ const spec = (0, core_1.defineSpec)({
129
129
  {
130
130
  variant: "solid",
131
131
  tone: "brand",
132
+ // your-moon/crisp#897: `bgBrandContrast`, not `bgBrandSolid` — white
133
+ // text on the exact accent blue (#0298d9) only clears 3.23:1;
134
+ // `bgBrandContrast` is the darker sibling built to clear 4.5:1+ with
135
+ // white, so `fgOnBrand` (white) stays right rather than `fgOnTone`
136
+ // (ink read off-brand next to solid `success`/`warning`/`danger`,
137
+ // which really do need it).
132
138
  decls: {
133
- background: tokens_1.contract.bgBrandSolid,
139
+ background: tokens_1.contract.bgBrandContrast,
134
140
  color: tokens_1.contract.fgOnBrand,
135
141
  boxShadow: "none",
136
142
  },
@@ -143,7 +149,15 @@ const spec = (0, core_1.defineSpec)({
143
149
  {
144
150
  variant: "solid",
145
151
  tone: "warning",
146
- decls: { background: tokens_1.contract.bgWarning, color: tokens_1.contract.fgOnTone, boxShadow: "none" },
152
+ // seed-identity migration (your-moon/crisp#890): `bgWarningSolid`, not
153
+ // `bgWarning` — seed's own solid warning fill is a distinct, paler
154
+ // yellow from the accent every other tone's solid fill reuses
155
+ // directly (see tokens/palette.ts's `amberSolid` comment).
156
+ decls: {
157
+ background: tokens_1.contract.bgWarningSolid,
158
+ color: tokens_1.contract.fgOnTone,
159
+ boxShadow: "none",
160
+ },
147
161
  },
148
162
  {
149
163
  variant: "solid",
@@ -30,7 +30,13 @@ const spec = (0, core_1.defineSpec)({
30
30
  intent: {
31
31
  primary: {
32
32
  decls: {
33
- background: tokens_1.contract.bgBrandSolid,
33
+ // your-moon/crisp#897: `bgBrandContrast`, not `bgBrandSolid` —
34
+ // white text on the exact accent blue (#0298d9) only clears
35
+ // 3.23:1; `bgBrandContrast` is the darker sibling built to clear
36
+ // white at 4.5:1+, keeping `fgOnBrand` (white) rather than
37
+ // reaching for `fgOnTone` (ink read off-brand — seed's own solid
38
+ // buttons use white).
39
+ background: tokens_1.contract.bgBrandContrast,
34
40
  color: tokens_1.contract.fgOnBrand,
35
41
  // crisp#494: the inset ring is a black tint, not white -- live-
36
42
  // measured on Attio's own "New Company" button as
@@ -39,12 +45,16 @@ const spec = (0, core_1.defineSpec)({
39
45
  // exactly.
40
46
  boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08)",
41
47
  },
48
+ // your-moon/crisp#897: a brightness filter, not a second background
49
+ // swap — matches `danger`'s own pattern below. `bgBrandSolidPressed`
50
+ // remains the accent's own hover-darken for the many OTHER
51
+ // consumers that pair `bgBrandSolid` with white already (out of
52
+ // scope for this fix; tracked separately), so reusing it here would
53
+ // have made primary's rest-to-hover jump inconsistent with its own
54
+ // rest fill's new, darker starting point.
42
55
  parts: [
43
- { selector: "&:hover", decls: { background: tokens_1.contract.bgBrandSolidPressed } },
44
- {
45
- selector: "&:active",
46
- decls: { background: tokens_1.contract.bgBrandSolidPressed },
47
- },
56
+ { selector: "&:hover", decls: { filter: "brightness(0.96)" } },
57
+ { selector: "&:active", decls: { filter: "brightness(0.92)" } },
48
58
  ],
49
59
  },
50
60
  neutral: {
@@ -37,11 +37,11 @@ exports.Input = void 0;
37
37
  const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const React = __importStar(require("react"));
39
39
  const input_recipe_1 = require("./input.recipe");
40
- exports.Input = React.forwardRef(({ size, invalid, className, style, ...props }, ref) => {
40
+ exports.Input = React.forwardRef(({ size, invalid, className, style, suffix, ...props }, ref) => {
41
41
  // The wrapper exists only to host the ::after focus ring — an <input> is a
42
42
  // replaced element and cannot have one. Everything the consumer passes
43
43
  // still lands on the <input> itself, so `id`, `name`, `onChange`, form
44
44
  // association and every native attribute behave exactly as before.
45
- return ((0, jsx_runtime_1.jsx)("span", { className: (0, input_recipe_1.inputRecipe)({ size, invalid, className }), style: style, children: (0, jsx_runtime_1.jsx)("input", { ref: ref, className: "crisp-input-value", "aria-invalid": invalid === true || undefined, ...props }) }));
45
+ return ((0, jsx_runtime_1.jsxs)("span", { className: (0, input_recipe_1.inputRecipe)({ size, invalid, className }), style: style, "data-readonly": props.readOnly ? "" : undefined, children: [(0, jsx_runtime_1.jsx)("input", { ref: ref, className: "crisp-input-value", "aria-invalid": invalid === true || undefined, ...props }), suffix != null && (0, jsx_runtime_1.jsx)("span", { className: "crisp-input-suffix", children: suffix })] }));
46
46
  });
47
47
  exports.Input.displayName = "Input";
@@ -41,6 +41,14 @@ const config = (0, core_1.defineRecipe)({
41
41
  & .crisp-input-value::placeholder { color: ${tokens_1.contract.fgMuted}; }
42
42
  & .crisp-input-value:disabled { opacity: 0.5; cursor: not-allowed; }
43
43
  &:has(.crisp-input-value:disabled) { opacity: 1; }
44
+
45
+ /* crisp#634: measured live on Attio's own Settings > Profile read-only
46
+ primary-email field -- a tinted field, not just muted text, driven off
47
+ the native \`readOnly\` attribute. \`ringField\` (rgba(0,0,0,.06)) is an
48
+ exact match for the measured ring; the 2%-fgPrimary tint reuses the
49
+ same color-mix idiom every other soft crisp surface already uses. */
50
+ &[data-readonly] { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 2%, transparent); box-shadow: inset 0 0 0 1px ${tokens_1.contract.ringField}; }
51
+ & .crisp-input-suffix { flex-shrink: 0; margin-inline-start: ${tokens_1.contract.space["1"]}; }
44
52
  `,
45
53
  variants: {
46
54
  size: {
@@ -71,7 +71,7 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, n
71
71
  // The id belongs on the trigger, not the wrapper: `<label for>` only names a
72
72
  // LABELABLE element, so an id left on the root div meant a FormField label
73
73
  // above this control named nothing (#247). Select already did this.
74
- id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPlaceholderProp, emptyLabel: emptyLabelProp, disabled, readOnly, invalid, size = "medium", variant = "default", className, "aria-label": ariaLabel, ...rest }) {
74
+ id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPlaceholderProp, emptyLabel: emptyLabelProp, disabled, readOnly, invalid, size = "small", variant = "default", className, "aria-label": ariaLabel, ...rest }) {
75
75
  // crisp/#471: same resolution order as Select's own.
76
76
  const placeholder = (0, core_1.useCrispMessage)(placeholderProp, "Select…");
77
77
  const searchPlaceholder = (0, core_1.useCrispMessage)(searchPlaceholderProp, "Search…");
@@ -99,9 +99,18 @@ const config = (0, core_1.defineRecipe)({
99
99
  .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, ${tokens_1.contract.bgOrange} 16%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgOrange} 82%, ${tokens_1.contract.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.bgOrange} 30%, transparent); }
100
100
  `,
101
101
  variants: {
102
- // seed's two trigger sizes, same tokens as Select's. medium is the
103
- // default -- closest to crisp's previous single size.
102
+ // Owner's follow-up after #867/#879/#882 (crisp/#889): SIZE is Attio's
103
+ // again, LOOK stays seed's -- see Select's own recipe comment for the
104
+ // full reasoning (measured live in GRC's "New employee" dialog: a 40px
105
+ // trigger next to a 32px `Input` visibly mismatched). `small` is the new
106
+ // default: `c.size.menuRow` (32px) and the original, pre-#882
107
+ // Attio-measured asymmetric padding for a chip-wrapping trigger.
108
+ // `medium`/`large` (seed's own 40/52px) stay, opt-in.
104
109
  size: {
110
+ small: `
111
+ & .crisp-multiselect-trigger { min-height: ${tokens_1.contract.size.menuRow}; border-radius: ${tokens_1.contract.radius.md}; gap: ${tokens_1.contract.space["1"]}; padding: ${tokens_1.contract.space["0.5"]} ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["0.5"]} ${tokens_1.contract.space["2"]}; }
112
+ & .crisp-multiselect-chevron { width: ${tokens_1.contract.size.icon}; height: ${tokens_1.contract.size.icon}; }
113
+ `,
105
114
  medium: `
106
115
  & .crisp-multiselect-trigger { min-height: ${tokens_1.contract.size.selectTriggerMd}; border-radius: ${tokens_1.contract.radius.md}; gap: ${tokens_1.contract.space["2"]}; padding: ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["3.5"]}; }
107
116
  & .crisp-multiselect-chevron { width: ${tokens_1.contract.size.icon}; height: ${tokens_1.contract.size.icon}; }
@@ -134,7 +143,7 @@ const config = (0, core_1.defineRecipe)({
134
143
  `,
135
144
  },
136
145
  },
137
- defaultVariants: { size: "medium", invalid: false, variant: "default" },
146
+ defaultVariants: { size: "small", invalid: false, variant: "default" },
138
147
  compoundVariants: [],
139
148
  });
140
149
  exports.multiselectRecipe = Object.assign((0, core_1.resolveRecipe)(config), {
@@ -4,11 +4,12 @@ exports.Progress = Progress;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const progress_recipe_1 = require("./progress.recipe");
6
6
  /**
7
- * A progress bar — a continuous brand fill, or a segmented row of pills when
8
- * `segments` is set (the "Getting started 3/6" pattern). Reports its state via
9
- * `role="progressbar"` and the ARIA value attributes.
7
+ * A progress bar — a continuous brand fill, a segmented row of pills when
8
+ * `segments` is set (the "Getting started 3/6" pattern), or a small ring
9
+ * (`variant="circular"`, a character-counter's own progress). Reports its
10
+ * state via `role="progressbar"` and the ARIA value attributes.
10
11
  */
11
- function Progress({ value, max = 100, segments, "aria-label": ariaLabel, className, ...rest }) {
12
+ function Progress({ value, max = 100, segments, variant = "linear", size = 18, "aria-label": ariaLabel, className, ...rest }) {
12
13
  const pct = Math.max(0, Math.min(100, (value / max) * 100));
13
14
  const aria = {
14
15
  role: "progressbar",
@@ -17,6 +18,22 @@ function Progress({ value, max = 100, segments, "aria-label": ariaLabel, classNa
17
18
  "aria-valuemax": max,
18
19
  "aria-label": ariaLabel,
19
20
  };
21
+ if (variant === "circular") {
22
+ // Measured live: an 18x18 SVG, r=7.4, stroke-width 1.6 (a 0.6px margin on
23
+ // each side) — a two-circle ring (a soft-tinted track + a brand arc),
24
+ // the same track tint Badge/Callout's own `soft(bgBrandSolid, 14%)`
25
+ // formula already gives.
26
+ const strokeWidth = size * (1.6 / 18);
27
+ const r = size / 2 - strokeWidth;
28
+ const circumference = 2 * Math.PI * r;
29
+ const offset = circumference * (1 - pct / 100);
30
+ // The measured sample was at value=0 (a full, invisible offset either
31
+ // way), so the arc's start point couldn't be read off that one DOM
32
+ // snapshot -- 12 o'clock is the conventional start for this shape and
33
+ // isn't itself in tension with anything measured, but isn't independently
34
+ // confirmed at a nonzero value either.
35
+ return ((0, jsx_runtime_1.jsx)("div", { ...aria, className: (0, progress_recipe_1.progressRecipe)({ className }), ...rest, "data-variant": "circular", children: (0, jsx_runtime_1.jsxs)("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("circle", { className: "crisp-progress-circle-track", r: r, cx: size / 2, cy: size / 2, strokeWidth: strokeWidth, fill: "transparent" }), (0, jsx_runtime_1.jsx)("circle", { className: "crisp-progress-circle-fill", r: r, cx: size / 2, cy: size / 2, strokeWidth: strokeWidth, fill: "transparent", strokeDasharray: circumference, strokeDashoffset: offset, strokeLinecap: "round", transform: `rotate(-90 ${size / 2} ${size / 2})` })] }) }));
36
+ }
20
37
  if (segments && segments > 0) {
21
38
  const filled = Math.round((pct / 100) * segments);
22
39
  return ((0, jsx_runtime_1.jsx)("div", { ...aria, className: (0, progress_recipe_1.progressRecipe)({ className }), ...rest, "data-segmented": "", children: Array.from({ length: segments }, (_, i) => ((0, jsx_runtime_1.jsx)("span", { className: "crisp-progress-seg", "data-filled": i < filled ? "" : undefined }, i))) }));
@@ -52,6 +52,36 @@ const spec = (0, core_1.defineSpec)({
52
52
  selector: "&[data-segmented] .crisp-progress-seg[data-filled]",
53
53
  decls: { background: tokens_1.contract.bgBrandSolid },
54
54
  },
55
+ // crisp#634: measured live on Attio's own Settings > Profile
56
+ // character-counter ring — the track tint is the same
57
+ // `soft(bgBrandSolid, 14%)` formula Badge/Callout already use (measured
58
+ // rgb(229,238,255), within the same noise this codebase already treats
59
+ // as equivalent), the arc is the plain brand fill.
60
+ {
61
+ selector: '&[data-variant="circular"]',
62
+ decls: {
63
+ display: "inline-flex",
64
+ height: "auto",
65
+ borderRadius: "0",
66
+ background: "transparent",
67
+ overflow: "visible",
68
+ },
69
+ },
70
+ {
71
+ slot: "circleTrack",
72
+ selector: "& .crisp-progress-circle-track",
73
+ decls: {
74
+ stroke: `color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 14%, transparent)`,
75
+ },
76
+ },
77
+ {
78
+ slot: "circleFill",
79
+ selector: "& .crisp-progress-circle-fill",
80
+ decls: {
81
+ stroke: tokens_1.contract.bgBrandSolid,
82
+ transition: "stroke-dashoffset 0.25s ease",
83
+ },
84
+ },
55
85
  ],
56
86
  });
57
87
  exports.spec = spec;
@@ -55,7 +55,7 @@ function optionText(o) {
55
55
  * move, Enter/Space pick, Esc close, Home/End jump, opens upward when tight.
56
56
  * Controlled or uncontrolled.
57
57
  */
58
- function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant = "default", width = "full", size = "medium", searchable, searchPlaceholder: searchPlaceholderProp, onSearchChange, emptyLabel: emptyLabelProp, disabled, readOnly, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
58
+ function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant = "default", width = "full", size = "small", searchable, searchPlaceholder: searchPlaceholderProp, onSearchChange, emptyLabel: emptyLabelProp, disabled, readOnly, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
59
59
  // crisp/#471: an explicit prop always wins; missing, it reads the nearest
60
60
  // CrispMessagesProvider's translation of the English default, or that
61
61
  // default itself with no provider — unchanged from before this existed.
@@ -217,7 +217,7 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
217
217
  return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, size, invalid, className }), ...rest, children: [name && (selected || required) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-formvalue", style: core_1.formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
218
218
  // controlled by the listbox above; `readOnly` would bar it from
219
219
  // validation, which is the only reason this input is not hidden
220
- onChange: () => { } })), (0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, "aria-readonly": readOnly ? "true" : undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.triggerLabel ?? selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsxs)("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-select-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
220
+ onChange: () => { } })), (0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, "aria-readonly": readOnly ? "true" : undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.triggerLabel ?? selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsxs)("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-select-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 16, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
221
221
  setQuery(e.target.value);
222
222
  setHighlight(0);
223
223
  onSearchChange?.(e.target.value);
@@ -88,9 +88,19 @@ const config = (0, core_1.defineRecipe)({
88
88
  }
89
89
  .crisp-select-listbox[data-up] { transform-origin: bottom left; }
90
90
  .crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.95); } }
91
- .crisp-select-search { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${tokens_1.contract.space["2"]}) calc(-1 * ${tokens_1.contract.space["2"]}) ${tokens_1.contract.space["2"]}; padding: 0 ${tokens_1.contract.space["2.5"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; color: ${tokens_1.contract.fgNeutralMuted}; }
91
+ /* Composed from @seed-design/rootage's text-input.yaml
92
+ (variant=outline,size=medium) -- seed's Select has no search field of
93
+ its own to resolve this from (see the file header), so this reuses
94
+ the SAME tokens #867 already resolved for the trigger at this size:
95
+ minHeight x10 (${tokens_1.contract.size.selectTriggerMd}), paddingX x3_5
96
+ (${tokens_1.contract.space["3.5"]}), gap x2 (${tokens_1.contract.space["2"]}), cornerRadius r2
97
+ (${tokens_1.contract.radius.md}), strokeColor stroke.neutral-weak
98
+ (${tokens_1.contract.strokeSelectTrigger}) -- a real bordered field sitting flush
99
+ inside the listbox's own 8px padding, replacing the old bare
100
+ 34px/no-border/bleeds-to-the-edge strip (crisp/#904). */
101
+ .crisp-select-search { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: ${tokens_1.contract.size.selectTriggerMd}; flex-shrink: 0; padding: 0 ${tokens_1.contract.space["3.5"]}; border: 1px solid ${tokens_1.contract.strokeSelectTrigger}; border-radius: ${tokens_1.contract.radius.md}; color: ${tokens_1.contract.fgNeutralMuted}; }
92
102
  .crisp-select-search svg { flex-shrink: 0; }
93
- .crisp-select-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgSelectNeutral}; }
103
+ .crisp-select-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; line-height: 19px; color: ${tokens_1.contract.fgSelectNeutral}; }
94
104
  .crisp-select-search input::placeholder { color: ${tokens_1.contract.fgSelectPlaceholder}; }
95
105
  .crisp-select-empty { padding: ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["4"]}; text-align: center; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgNeutralSubtle}; }
96
106
  /* select.yaml's groupLabel -- font-size/line-height/weight are the same
@@ -124,9 +134,26 @@ const config = (0, core_1.defineRecipe)({
124
134
  full: `width: 100%;`,
125
135
  auto: `width: auto;`,
126
136
  },
127
- // seed's two trigger/item sizes (select-trigger.yaml / select-item.yaml).
128
- // medium is the default -- closest to crisp's previous single size.
137
+ // Owner's follow-up after #867/#879 (crisp/#889): the LOOK stays seed's
138
+ // (border, listbox, radius, shadow, items, states, all unchanged below),
139
+ // but the SIZE is Attio's again -- a 40px trigger next to a 32px `Input`
140
+ // in the same form visibly mismatched (measured live in GRC's own "New
141
+ // employee" dialog). `small` is the new default: Attio's original
142
+ // 32px `c.size.menuRow`, `Input`'s own radius/padding/line-height so the
143
+ // two line up exactly, not just approximately. `medium`/`large` (seed's
144
+ // own 40/52px) stay, opt-in. Item rows are IDENTICAL across all three --
145
+ // only the trigger differs; only it was ever the mismatch.
129
146
  size: {
147
+ small: `
148
+ & .crisp-select-trigger { height: ${tokens_1.contract.size.menuRow}; border-radius: ${tokens_1.contract.radius.md}; gap: ${tokens_1.contract.space["1.5"]}; padding-inline: ${tokens_1.contract.space["2.5"]}; }
149
+ & .crisp-select-value, & .crisp-select-placeholder { font-size: ${tokens_1.contract.text.lg}; line-height: 20px; }
150
+ & .crisp-select-chevron { width: ${tokens_1.contract.size.icon}; height: ${tokens_1.contract.size.icon}; }
151
+ .crisp-select-option { gap: ${tokens_1.contract.space["2"]}; }
152
+ .crisp-select-option-label { font-size: ${tokens_1.contract.text.lg}; line-height: 19px; }
153
+ .crisp-select-option-description { font-size: ${tokens_1.contract.text.sm}; line-height: 16px; }
154
+ .crisp-select-option-check { width: ${tokens_1.contract.space["3"]}; height: ${tokens_1.contract.space["3"]}; }
155
+ .crisp-select-label { padding-top: ${tokens_1.contract.space["2"]}; }
156
+ `,
130
157
  medium: `
131
158
  & .crisp-select-trigger { height: ${tokens_1.contract.size.selectTriggerMd}; border-radius: ${tokens_1.contract.radius.md}; gap: ${tokens_1.contract.space["2"]}; padding-inline: ${tokens_1.contract.space["3.5"]}; }
132
159
  & .crisp-select-value, & .crisp-select-placeholder { font-size: ${tokens_1.contract.text.lg}; line-height: 19px; }
@@ -178,7 +205,7 @@ const config = (0, core_1.defineRecipe)({
178
205
  },
179
206
  defaultVariants: {
180
207
  width: "full",
181
- size: "medium",
208
+ size: "small",
182
209
  invalid: false,
183
210
  variant: "default",
184
211
  },
@@ -36,13 +36,14 @@ const spec = (0, core_1.defineSpec)({
36
36
  muted: { color: tokens_1.contract.fgNeutralMuted },
37
37
  // #858: no new measurement -- both reuse tokens already measured
38
38
  // against Attio for the same colours in a different arrangement.
39
- // `brand` is `textlink`'s own reused brand-text colour (`bgBrandSolid`,
40
- // the same alias GRC's own `--color-fg-brand` resolves to). `danger`
41
- // is `fgDanger` ("solid critical/destructive TEXT... an overdue
42
- // due-date, a destructive menu item's label" -- see its own doc in
43
- // tokens/semantic.ts), not `fgDangerSoft` (that one's a soft Badge
44
- // pill's text, a different measured hex).
45
- brand: { color: tokens_1.contract.bgBrandSolid },
39
+ // `brand` is `textlink`'s own reused brand-text colour, now
40
+ // `fgBrandContrast` (your-moon/crisp#897: the plain accent,
41
+ // `bgBrandSolid`, only clears 3.23:1 as text on a white page).
42
+ // `danger` is `fgDanger` ("solid critical/destructive TEXT... an
43
+ // overdue due-date, a destructive menu item's label" -- see its own
44
+ // doc in tokens/semantic.ts), not `fgDangerSoft` (that one's a soft
45
+ // Badge pill's text, a different measured hex).
46
+ brand: { color: tokens_1.contract.fgBrandContrast },
46
47
  danger: { color: tokens_1.contract.fgDanger },
47
48
  },
48
49
  },
@@ -36,13 +36,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.Textarea = void 0;
37
37
  const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const React = __importStar(require("react"));
39
+ const progress_1 = require("../progress");
39
40
  const textarea_recipe_1 = require("./textarea.recipe");
40
41
  /**
41
42
  * A multiline text field — measured from settings. Forwards every native
42
43
  * `<textarea>` prop; pass `showCount` with `maxLength` to render a live
43
44
  * character counter (which turns red if the value exceeds the limit).
44
45
  */
45
- exports.Textarea = React.forwardRef(({ showCount, maxLength, className, wrapperClassName, value, defaultValue, onChange,
46
+ exports.Textarea = React.forwardRef(({ showCount, showProgress, maxLength, className, wrapperClassName, value, defaultValue, onChange,
46
47
  // `style` is pulled out and put on the ROOT, the way Input does it.
47
48
  // Everything else belongs on the <textarea> -- id, name, form
48
49
  // association, aria -- but `style` sizes the visual box, and the box is
@@ -58,6 +59,6 @@ style, ...props }, ref) => {
58
59
  if (value === undefined)
59
60
  setCount(e.target.value.length);
60
61
  onChange?.(e);
61
- }, ...props }) }), counted && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-textarea-count", "data-over": len > (maxLength ?? 0) ? "" : undefined, "aria-hidden": "true", children: [len, " / ", maxLength] }))] }));
62
+ }, ...props }) }), counted && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-textarea-count", "data-over": len > (maxLength ?? 0) ? "" : undefined, "aria-hidden": "true", children: [showProgress && ((0, jsx_runtime_1.jsx)(progress_1.Progress, { variant: "circular", value: len, max: maxLength ?? 0, className: "crisp-textarea-progress" })), len, " / ", maxLength] }))] }));
62
63
  });
63
64
  exports.Textarea.displayName = "Textarea";
@@ -49,8 +49,9 @@ const config = (0, core_1.defineRecipe)({
49
49
  &[data-invalid] .crisp-textarea-wrap::after { border-color: ${tokens_1.contract.bgDanger}; }
50
50
  &[data-invalid] .crisp-textarea-wrap:focus-within::after { border-color: ${tokens_1.contract.bgDanger}; }
51
51
  &[data-counted] .crisp-textarea-field { padding-bottom: ${tokens_1.contract.space["5"]}; }
52
- & .crisp-textarea-count { position: absolute; inset-inline-end: 14px; bottom: ${tokens_1.contract.space["2.5"]}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.fgMuted}; pointer-events: none; }
52
+ & .crisp-textarea-count { position: absolute; inset-inline-end: 14px; bottom: ${tokens_1.contract.space["2.5"]}; display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.fgMuted}; pointer-events: none; }
53
53
  & .crisp-textarea-count[data-over] { color: ${tokens_1.contract.bgDanger}; }
54
+ & .crisp-textarea-progress { flex-shrink: 0; }
54
55
  `,
55
56
  variants: {},
56
57
  defaultVariants: {},