@crisp-ui-kit/crisp 0.56.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 (139) 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/callout.css +6 -0
  11. package/dist/callout.d.ts +2 -2
  12. package/dist/callout.layered.css +6 -0
  13. package/dist/cjs/components/accentpicker/accentpicker.js +66 -0
  14. package/dist/cjs/components/accentpicker/accentpicker.recipe.js +25 -0
  15. package/dist/cjs/components/accentpicker/index.js +7 -0
  16. package/dist/cjs/components/avatar/avatar.recipe.js +4 -1
  17. package/dist/cjs/components/badge/badge.recipe.js +16 -2
  18. package/dist/cjs/components/button/button.recipe.js +16 -6
  19. package/dist/cjs/components/callout/callout.js +2 -2
  20. package/dist/cjs/components/callout/callout.recipe.js +15 -2
  21. package/dist/cjs/components/input/input.js +2 -2
  22. package/dist/cjs/components/input/input.recipe.js +14 -0
  23. package/dist/cjs/components/menu/menu.js +2 -2
  24. package/dist/cjs/components/menu/menu.recipe.js +8 -0
  25. package/dist/cjs/components/multiselect/multiselect.js +23 -10
  26. package/dist/cjs/components/multiselect/multiselect.recipe.js +104 -65
  27. package/dist/cjs/components/progress/progress.js +21 -4
  28. package/dist/cjs/components/progress/progress.recipe.js +30 -0
  29. package/dist/cjs/components/select/listbox-keyboard.js +7 -1
  30. package/dist/cjs/components/select/select.js +24 -11
  31. package/dist/cjs/components/select/select.recipe.js +177 -78
  32. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +3 -3
  33. package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +26 -1
  34. package/dist/cjs/components/tabs/tabs.js +63 -4
  35. package/dist/cjs/components/tabs/tabs.recipe.js +13 -0
  36. package/dist/cjs/components/text/text.recipe.js +8 -7
  37. package/dist/cjs/components/textarea/textarea.js +3 -2
  38. package/dist/cjs/components/textarea/textarea.recipe.js +2 -1
  39. package/dist/cjs/components/textlink/textlink.js +2 -2
  40. package/dist/cjs/components/textlink/textlink.recipe.js +24 -3
  41. package/dist/cjs/index.js +1 -0
  42. package/dist/cjs/product/recordpage/recordpage.js +61 -7
  43. package/dist/cjs/tokens/elevation.js +7 -0
  44. package/dist/cjs/tokens/palette.js +120 -25
  45. package/dist/cjs/tokens/scale.js +9 -0
  46. package/dist/cjs/tokens/semantic.js +42 -0
  47. package/dist/esm/components/accentpicker/accentpicker.js +30 -0
  48. package/dist/esm/components/accentpicker/accentpicker.recipe.js +22 -0
  49. package/dist/esm/components/accentpicker/index.js +2 -0
  50. package/dist/esm/components/avatar/avatar.recipe.js +4 -1
  51. package/dist/esm/components/badge/badge.recipe.js +16 -2
  52. package/dist/esm/components/button/button.recipe.js +16 -6
  53. package/dist/esm/components/callout/callout.js +2 -2
  54. package/dist/esm/components/callout/callout.recipe.js +15 -2
  55. package/dist/esm/components/input/input.js +3 -3
  56. package/dist/esm/components/input/input.recipe.js +14 -0
  57. package/dist/esm/components/menu/menu.js +2 -2
  58. package/dist/esm/components/menu/menu.recipe.js +8 -0
  59. package/dist/esm/components/multiselect/multiselect.js +23 -10
  60. package/dist/esm/components/multiselect/multiselect.recipe.js +104 -65
  61. package/dist/esm/components/progress/progress.js +22 -5
  62. package/dist/esm/components/progress/progress.recipe.js +30 -0
  63. package/dist/esm/components/select/listbox-keyboard.js +7 -1
  64. package/dist/esm/components/select/select.js +24 -11
  65. package/dist/esm/components/select/select.recipe.js +177 -78
  66. package/dist/esm/components/tabletoolbar/tabletoolbar.js +3 -3
  67. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +26 -1
  68. package/dist/esm/components/tabs/tabs.js +63 -4
  69. package/dist/esm/components/tabs/tabs.recipe.js +13 -0
  70. package/dist/esm/components/text/text.recipe.js +8 -7
  71. package/dist/esm/components/textarea/textarea.js +3 -2
  72. package/dist/esm/components/textarea/textarea.recipe.js +2 -1
  73. package/dist/esm/components/textlink/textlink.js +2 -2
  74. package/dist/esm/components/textlink/textlink.recipe.js +24 -3
  75. package/dist/esm/index.js +1 -0
  76. package/dist/esm/product/recordpage/recordpage.js +62 -8
  77. package/dist/esm/tokens/elevation.js +7 -0
  78. package/dist/esm/tokens/palette.js +120 -25
  79. package/dist/esm/tokens/scale.js +9 -0
  80. package/dist/esm/tokens/semantic.js +42 -0
  81. package/dist/input.css +11 -0
  82. package/dist/input.d.ts +2 -2
  83. package/dist/input.layered.css +11 -0
  84. package/dist/menu.css +8 -0
  85. package/dist/menu.layered.css +8 -0
  86. package/dist/multiselect.css +57 -29
  87. package/dist/multiselect.d.ts +3 -1
  88. package/dist/multiselect.layered.css +57 -29
  89. package/dist/native/tokens.d.ts +1 -1
  90. package/dist/native/tokens.js +82 -28
  91. package/dist/progress.css +3 -0
  92. package/dist/progress.layered.css +3 -0
  93. package/dist/select.css +100 -42
  94. package/dist/select.d.ts +3 -2
  95. package/dist/select.layered.css +100 -42
  96. package/dist/styles.css +268 -93
  97. package/dist/styles.layered.css +268 -93
  98. package/dist/tabletoolbar.css +8 -1
  99. package/dist/tabletoolbar.layered.css +8 -1
  100. package/dist/tabs.css +6 -0
  101. package/dist/tabs.layered.css +6 -0
  102. package/dist/text.css +1 -1
  103. package/dist/text.layered.css +1 -1
  104. package/dist/textarea.css +2 -1
  105. package/dist/textarea.layered.css +2 -1
  106. package/dist/textlink.css +11 -2
  107. package/dist/textlink.d.ts +2 -1
  108. package/dist/textlink.layered.css +11 -2
  109. package/dist/tokens.css +43 -11
  110. package/dist/tokens.json +82 -28
  111. package/dist/types/components/accentpicker/accentpicker.d.ts +23 -0
  112. package/dist/types/components/accentpicker/accentpicker.recipe.d.ts +3 -0
  113. package/dist/types/components/accentpicker/index.d.ts +2 -0
  114. package/dist/types/components/button/button.recipe.d.ts +2 -2
  115. package/dist/types/components/callout/callout.d.ts +9 -1
  116. package/dist/types/components/callout/callout.recipe.d.ts +12 -2
  117. package/dist/types/components/input/input.d.ts +14 -0
  118. package/dist/types/components/input/input.recipe.d.ts +3 -0
  119. package/dist/types/components/menu/menu.d.ts +7 -1
  120. package/dist/types/components/multiselect/multiselect.d.ts +25 -6
  121. package/dist/types/components/multiselect/multiselect.recipe.d.ts +27 -0
  122. package/dist/types/components/progress/progress.d.ts +16 -4
  123. package/dist/types/components/progress/progress.recipe.d.ts +22 -0
  124. package/dist/types/components/select/listbox-keyboard.d.ts +10 -1
  125. package/dist/types/components/select/select.d.ts +29 -6
  126. package/dist/types/components/select/select.recipe.d.ts +25 -10
  127. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +14 -0
  128. package/dist/types/components/tabs/tabs.d.ts +9 -1
  129. package/dist/types/components/textarea/textarea.d.ts +16 -0
  130. package/dist/types/components/textlink/textlink.d.ts +8 -1
  131. package/dist/types/components/textlink/textlink.recipe.d.ts +10 -0
  132. package/dist/types/index.d.ts +1 -0
  133. package/dist/types/product/recordpage/recordpage.d.ts +61 -1
  134. package/dist/types/tokens/contract.d.ts +16 -3
  135. package/dist/types/tokens/elevation.d.ts +7 -0
  136. package/dist/types/tokens/palette.d.ts +55 -11
  137. package/dist/types/tokens/scale.d.ts +6 -0
  138. package/dist/types/tokens/semantic.d.ts +33 -0
  139. package/package.json +6 -1
@@ -3,31 +3,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.multiselectRecipe = void 0;
4
4
  const core_1 = require("../../core");
5
5
  const tokens_1 = require("../../tokens");
6
- // A multi-select / tag editor — measured from a cell tag picker: an
7
- // outlined trigger showing the selected values as coloured chips, opening a
8
- // popover with a search field over rows of "[coloured chip] … [checkbox]" that
9
- // toggle on click. The popover portals to document.body (#339) so no
10
- // ancestor's transform/backdrop-filter can capture its position: fixed box;
11
- // z-index 70 keeps it above Dialog/Drawer's 60/61.
12
- // The listbox and everything inside it (including the recordstable-tag
13
- // chips, which render inside listbox option rows as well as the trigger)
14
- // live in `globalCss` below, not nested under `&` (#357): nesting compiles
15
- // to a real descendant combinator scoped to the recipe's root class, which
16
- // needs actual DOM containment — exactly what the portal to `document.body`
17
- // breaks. Nested, these rules matched nothing once the listbox moved and it
18
- // rendered completely unstyled. The trigger and its own parts stay nested
19
- // under `&` — the trigger is NOT portaled. `font-family`/smoothing are
20
- // restated on the listbox's own rule (its children pick font up via their
21
- // own `font-family: inherit`, which still works — that inheritance is
22
- // intact WITHIN the portaled subtree, only the link back to the
23
- // trigger-holding root broke). A flat recordstable-tag selector also works
24
- // unchanged wherever it's rendered, portaled or not, so it needs no split.
25
- // This is a third, hand-pasted copy of `recordTagCss`
26
- // (product/recordstable/recordstable.tag.ts) rather than a splice of it:
27
- // that helper is written `& .crisp-recordstable-tag { ... }` for nesting
28
- // under a recipe's `base`, which doesn't compile to anything inside a flat
29
- // `globalCss` block. Keep the two in sync by hand (#380 added the measured
30
- // `box-shadow: inset 0 0 0 1px <tone ring colour>` here to match).
6
+ // MultiSelect reads as the same family as Select (crisp/#882, following
7
+ // #867/#879): the trigger/listbox/item chrome below reuses Select's
8
+ // seed-design tokens verbatim (`strokeSelectTrigger`, `bgSelectPressed`,
9
+ // `bgSelectDisabled`, `strokeSelectInvalid`, `bgSelectListbox`,
10
+ // `selectFloating`, `fgSelectNeutral`, `fgSelectPlaceholder`, `radius["3xl"]`,
11
+ // `size.selectTriggerMd/Lg`) rather than resolving a second seed spec --
12
+ // seed-design has no multi-value combobox of its own (`packages/rootage/
13
+ // components/` has no `combobox`/`multiselect`; `chip.yaml`/`tag-group*.yaml`
14
+ // are a different, standalone pill pattern, not a field's multi-value
15
+ // trigger). What stays untouched: the `.crisp-recordstable-tag` chip
16
+ // styling (shared with RecordsTable/DataTable elsewhere), the checkbox-style
17
+ // selected indicator, and the toggle-without-closing interaction.
18
+ //
19
+ // The search row's geometry (36px height, 6px gap, no divider) is an
20
+ // Attio measurement (#481) `build/spec-check.mjs` guards directly -- it is
21
+ // NOT part of the seed-parity change and must not move. Only its text/icon
22
+ // colours follow the rest of this file onto the shared tokens.
23
+ //
24
+ // The listbox portals to `document.body` (#339), so its rules live in
25
+ // `globalCss`, not nested under `&` -- see Select's own recipe for the full
26
+ // reasoning (identical here).
31
27
  const config = (0, core_1.defineRecipe)({
32
28
  name: "multiselect",
33
29
  base: `
@@ -35,53 +31,63 @@ const config = (0, core_1.defineRecipe)({
35
31
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
36
32
 
37
33
  & .crisp-multiselect-trigger {
38
- box-sizing: border-box; display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; width: 100%; min-width: 0; min-height: ${tokens_1.contract.size.menuRow};
39
- margin: 0; padding: ${tokens_1.contract.space["0.5"]} ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["0.5"]} ${tokens_1.contract.space["2"]};
40
- border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 7%, transparent);
41
- font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; cursor: pointer; text-align: start;
42
- transition: background-color .12s ease, box-shadow .12s ease;
34
+ box-sizing: border-box; display: flex; align-items: center; width: 100%; min-width: 0; margin: 0;
35
+ border: 1px solid ${tokens_1.contract.strokeSelectTrigger}; background: transparent; cursor: pointer; text-align: start;
36
+ font-family: inherit; color: ${tokens_1.contract.fgSelectNeutral};
37
+ transition: background-color .15s cubic-bezier(.35,0,.35,1), border-color .1s cubic-bezier(.35,0,.35,1), border-width .1s cubic-bezier(.35,0,.35,1);
43
38
  }
44
- & .crisp-multiselect-trigger:hover { background: ${tokens_1.contract.bgRaised}; }
45
- & .crisp-multiselect-trigger[data-open], & .crisp-multiselect-trigger:focus-visible { outline: none; box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; background: ${tokens_1.contract.bgSurface}; }
46
- & .crisp-multiselect-trigger:disabled { opacity: 0.5; cursor: not-allowed; }
39
+ /* Same pressed/disabled/readonly/focus model as Select's trigger -- see
40
+ that recipe's comment for why hover and an open/focus ring are gone. */
41
+ & .crisp-multiselect-trigger:active:not(:disabled):not([aria-readonly="true"]) { background: ${tokens_1.contract.bgSelectPressed}; }
42
+ & .crisp-multiselect-trigger:focus-visible { outline: 2px solid ${tokens_1.contract.strokeFocusRing}; outline-offset: -2px; }
43
+ & .crisp-multiselect-trigger:disabled { background: ${tokens_1.contract.bgSelectDisabled}; cursor: not-allowed; }
44
+ & .crisp-multiselect-trigger:disabled .crisp-multiselect-placeholder,
45
+ & .crisp-multiselect-trigger:disabled .crisp-multiselect-chevron { color: ${tokens_1.contract.fgDisabled}; }
46
+ & .crisp-multiselect-trigger[aria-readonly="true"] { background: ${tokens_1.contract.bgSelectDisabled}; cursor: default; }
47
47
  & .crisp-multiselect-chips { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: ${tokens_1.contract.space["1"]}; }
48
- & .crisp-multiselect-placeholder { color: ${tokens_1.contract.fgMuted}; }
49
- & .crisp-multiselect-chevron { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; transition: transform .15s ease; }
50
- & .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); }
48
+ & .crisp-multiselect-placeholder { color: ${tokens_1.contract.fgSelectPlaceholder}; }
49
+ /* closeRotateDuration/openRotateDuration, same asymmetric timing as
50
+ Select's own chevron (see that recipe's comment). */
51
+ & .crisp-multiselect-chevron { flex-shrink: 0; color: ${tokens_1.contract.fgNeutralMuted}; transition: transform .1s cubic-bezier(.35,0,.35,1); }
52
+ & .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); transition-duration: .15s; }
51
53
  `,
52
54
  globalCss: `
53
- /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
54
- on document.body while open (Zag re-enables it only on layers IT
55
- tracks), and the portaled listbox is outside Zag's knowledge -- it
56
- inherited the lock, so every option was unclickable, hit-testing
57
- right through to whatever real element sat behind it. */
55
+ /* pointer-events: auto (#375) -- see Select's matching comment. */
58
56
  .crisp-multiselect-listbox {
59
57
  font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
60
- position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
61
- max-height: 300px; overflow-y: auto; padding: ${tokens_1.contract.space["1"]};
62
- border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover};
63
- box-shadow: ${tokens_1.elevation.floating};
64
- transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease;
58
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["2"]};
59
+ max-height: 480px; overflow-y: auto; padding: ${tokens_1.contract.space["2"]};
60
+ border-radius: ${tokens_1.contract.radius["3xl"]}; background: ${tokens_1.contract.bgSelectListbox};
61
+ box-shadow: ${tokens_1.elevation.selectFloating};
62
+ transform-origin: top left; opacity: 1; transform: none;
63
+ transition: opacity .15s cubic-bezier(0,0,.15,1), transform .15s cubic-bezier(0,0,.15,1);
65
64
  }
66
65
  .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
67
- .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
68
- .crisp-multiselect-search { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: 36px; flex-shrink: 0; margin: calc(-1 * ${tokens_1.contract.space["1"]}) calc(-1 * ${tokens_1.contract.space["1"]}) ${tokens_1.contract.space["1"]}; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["3"]}; color: ${tokens_1.contract.fgMuted}; }
66
+ .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.95); } }
67
+ /* LOCKED geometry (#481, guarded by build/spec-check.mjs): height 36px,
68
+ gap space["1.5"] (6px), no border-bottom -- an Attio measurement, not
69
+ part of this seed-parity pass. Only colours below follow Select. */
70
+ .crisp-multiselect-search { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: 36px; flex-shrink: 0; margin: calc(-1 * ${tokens_1.contract.space["2"]}) calc(-1 * ${tokens_1.contract.space["2"]}) ${tokens_1.contract.space["2"]}; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["3"]}; color: ${tokens_1.contract.fgNeutralMuted}; }
69
71
  .crisp-multiselect-search svg { flex-shrink: 0; }
70
- .crisp-multiselect-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.fgPrimary}; }
71
- .crisp-multiselect-search input::placeholder { color: ${tokens_1.contract.fgMuted}; }
72
- /* #343: same measured values as Menu's own group heading
73
- (crisp-menu-label) -- reused rather than re-measured, since it's the
74
- same "small muted heading above a run of rows" treatment. */
75
- .crisp-multiselect-label { padding: ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["0.5"]}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
72
+ .crisp-multiselect-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}; }
73
+ .crisp-multiselect-search input::placeholder { color: ${tokens_1.contract.fgSelectPlaceholder}; }
74
+ /* Same groupLabel treatment as Select's (13px/regular/neutral-subtle). */
75
+ .crisp-multiselect-label { padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["4"]} 0; font-size: ${tokens_1.contract.text.md}; line-height: 18px; font-weight: ${tokens_1.contract.weight.regular}; color: ${tokens_1.contract.fgNeutralSubtle}; }
76
76
  .crisp-multiselect-option {
77
- box-sizing: border-box; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: ${tokens_1.contract.size.menuRow}; flex-shrink: 0;
78
- padding: 0 ${tokens_1.contract.space["2"]}; margin: 0; border: none; background: none; border-radius: ${tokens_1.contract.radius.md}; font-family: inherit; cursor: pointer; text-align: start;
77
+ box-sizing: border-box; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; flex-shrink: 0;
78
+ padding: ${tokens_1.contract.space["2.5"]} ${tokens_1.contract.space["4"]}; margin: 0; border: none; background: none; border-radius: ${tokens_1.contract.radius.xl};
79
+ font-family: inherit; text-align: start; cursor: pointer;
80
+ transition: background-color .15s cubic-bezier(.35,0,.35,1);
79
81
  }
82
+ /* Keyboard/hover highlight keeps crisp's own visible neutral fill, same
83
+ reasoning as Select's matching rule (seed's own pressed tint is too
84
+ faint for a keyboard-navigation indicator). */
80
85
  .crisp-multiselect-option[data-highlighted] { background: ${tokens_1.contract.bgRaised}; }
86
+ .crisp-multiselect-option:active { background: ${tokens_1.contract.bgSelectPressed}; }
81
87
  .crisp-multiselect-option-chip { flex: 0 1 auto; min-width: 0; overflow: hidden; }
82
88
  .crisp-multiselect-check { flex-shrink: 0; margin-inline-start: auto; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: ${tokens_1.contract.radius.sm}; box-shadow: inset 0 0 0 1.5px ${tokens_1.contract.borderDefault}; color: transparent; }
83
89
  .crisp-multiselect-option[aria-selected="true"] .crisp-multiselect-check { background: ${tokens_1.contract.bgBrandSolid}; box-shadow: none; color: #fff; }
84
- .crisp-multiselect-empty { padding: ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["2"]}; text-align: center; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }
90
+ .crisp-multiselect-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}; }
85
91
 
86
92
  .crisp-recordstable-tag { display: inline-flex; align-items: center; height: ${tokens_1.contract.size.chip}; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.chip}; font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.medium}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 82%, transparent); box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; }
87
93
  .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 14%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 82%, ${tokens_1.contract.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 30%, transparent); }
@@ -93,18 +99,51 @@ const config = (0, core_1.defineRecipe)({
93
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); }
94
100
  `,
95
101
  variants: {
96
- // Same ring Select's `invalid` draws — see that recipe's comment.
97
- invalid: {
98
- true: `
99
- & .crisp-multiselect-trigger,
100
- & .crisp-multiselect-trigger:hover,
101
- & .crisp-multiselect-trigger[data-open],
102
- & .crisp-multiselect-trigger:focus-visible { box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgDanger}; }
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.
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
+ `,
114
+ medium: `
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"]}; }
116
+ & .crisp-multiselect-chevron { width: ${tokens_1.contract.size.icon}; height: ${tokens_1.contract.size.icon}; }
103
117
  `,
118
+ large: `
119
+ & .crisp-multiselect-trigger { min-height: ${tokens_1.contract.size.selectTriggerLg}; border-radius: ${tokens_1.contract.radius.xl}; gap: ${tokens_1.contract.space["2.5"]}; padding: ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["4"]}; }
120
+ & .crisp-multiselect-chevron { width: ${tokens_1.contract.size.selectIconLg}; height: ${tokens_1.contract.size.selectIconLg}; }
121
+ `,
122
+ },
123
+ // Real border width+colour change, same as Select's -- see that
124
+ // recipe's comment.
125
+ invalid: {
126
+ true: `& .crisp-multiselect-trigger { border-width: 2px; border-color: ${tokens_1.contract.strokeSelectInvalid}; }`,
104
127
  false: ``,
105
128
  },
129
+ // ghost: fills whatever row it's embedded in instead of seed's fixed
130
+ // trigger height -- RecordsTable's TagsCell embeds a MultiSelect inline
131
+ // in a 36px table row, the exact regression #879 caught for Select's
132
+ // own datatable consumers. Not a seed state; crisp's own escape hatch.
133
+ //
134
+ // Declared LAST on purpose, same reasoning as Select's recipe: same-
135
+ // specificity CSS rules resolve by source order, and ghost's
136
+ // `min-height: 0; height: 100%` below must come after `size`'s fixed
137
+ // min-height to win the cascade.
138
+ variant: {
139
+ default: "",
140
+ ghost: `
141
+ & .crisp-multiselect-trigger { min-height: 0; height: 100%; border-color: transparent; }
142
+ & .crisp-multiselect-trigger:hover:not(:disabled) { background: ${tokens_1.contract.bgSelectPressed}; }
143
+ `,
144
+ },
106
145
  },
107
- defaultVariants: { invalid: false },
146
+ defaultVariants: { size: "small", invalid: false, variant: "default" },
108
147
  compoundVariants: [],
109
148
  });
110
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;
@@ -14,7 +14,7 @@ exports.useListboxKeyboard = useListboxKeyboard;
14
14
  * NOT for `WheelPicker`: that is an always-visible multi-column wheel that
15
15
  * commits on move, so Enter, Escape and Tab have no meaning in it.
16
16
  */
17
- function useListboxKeyboard({ enabledIndexes, highlight, setHighlight, commit, close, dismiss = close, }) {
17
+ function useListboxKeyboard({ enabledIndexes, highlight, setHighlight, commit, close, dismiss = close, commitOnSpace = false, }) {
18
18
  const move = (dir) => {
19
19
  if (enabledIndexes.length === 0)
20
20
  return;
@@ -53,6 +53,12 @@ function useListboxKeyboard({ enabledIndexes, highlight, setHighlight, commit, c
53
53
  e.preventDefault();
54
54
  commit();
55
55
  break;
56
+ case " ":
57
+ if (!commitOnSpace)
58
+ break;
59
+ e.preventDefault();
60
+ commit();
61
+ break;
56
62
  case "Escape":
57
63
  e.preventDefault();
58
64
  close();
@@ -46,13 +46,16 @@ function optionText(o) {
46
46
  return typeof o.label === "string" ? o.label : o.value;
47
47
  }
48
48
  /**
49
- * A select / dropdown — the settings control, measured live, with an
50
- * optional Linear-style search. A `role="combobox"` trigger opens a
51
- * `role="listbox"`; pass `searchable` to add a filter field at the top (with a
52
- * "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
53
- * Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
49
+ * A select / dropdown, built to seed-design's Select (crisp/#867) rather than
50
+ * measured from Attio — a deliberate one-component exception, the same way
51
+ * `DatePicker` follows seed's calendar structure — with an optional
52
+ * Linear-style search seed itself has no equivalent of. A `role="combobox"`
53
+ * trigger opens a `role="listbox"`; pass `searchable` to add a filter field
54
+ * at the top (with a "No results" empty state). Full keyboard model — ↑/↓
55
+ * move, Enter/Space pick, Esc close, Home/End jump, opens upward when tight.
56
+ * Controlled or uncontrolled.
54
57
  */
55
- function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant = "default", width = "full", searchable, searchPlaceholder: searchPlaceholderProp, onSearchChange, emptyLabel: emptyLabelProp, disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
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 }) {
56
59
  // crisp/#471: an explicit prop always wins; missing, it reads the nearest
57
60
  // CrispMessagesProvider's translation of the English default, or that
58
61
  // default itself with no provider — unchanged from before this existed.
@@ -99,8 +102,12 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
99
102
  const activeId = open && highlight >= 0 && highlight < filtered.length
100
103
  ? optionId(highlight)
101
104
  : undefined;
105
+ // seed-design's `interactive` (crisp/#867): disabled AND readOnly both
106
+ // refuse to open, but only `disabled` removes the button from the tab
107
+ // order and greys its text — readOnly stays focusable, full-strength ink.
108
+ const interactive = !disabled && !readOnly;
102
109
  const openMenu = () => {
103
- if (disabled)
110
+ if (!interactive)
104
111
  return;
105
112
  const rect = triggerRef.current?.getBoundingClientRect();
106
113
  if (rect) {
@@ -109,7 +116,7 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
109
116
  // direction that is (#210)
110
117
  const { style, up: flipUp } = (0, core_1.placeFloating)({
111
118
  rect,
112
- gap: 4,
119
+ gap: 8, // seed-design's select.yaml `gutter` (crisp/#867; was 4)
113
120
  flipBelow: 260,
114
121
  anchor: triggerRef.current,
115
122
  });
@@ -143,8 +150,14 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
143
150
  commit: () => pick(filtered[highlight]),
144
151
  close,
145
152
  dismiss: () => setOpen(false),
153
+ // Space also commits when the trigger itself owns navigation (crisp/#867,
154
+ // matching seed-design). Never on the search input's own wiring below —
155
+ // that one omits this option, so Space stays a normal character there.
156
+ commitOnSpace: !searchable,
146
157
  });
147
158
  const onTriggerKeyDown = (e) => {
159
+ if (!interactive)
160
+ return;
148
161
  if (!open) {
149
162
  if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
150
163
  e.preventDefault();
@@ -201,10 +214,10 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
201
214
  document.addEventListener("keydown", onKey);
202
215
  return () => document.removeEventListener("keydown", onKey);
203
216
  }, [open, close]);
204
- return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, 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",
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",
205
218
  // controlled by the listbox above; `readOnly` would bar it from
206
219
  // validation, which is the only reason this input is not hidden
207
- 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, 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) => {
208
221
  setQuery(e.target.value);
209
222
  setHighlight(0);
210
223
  onSearchChange?.(e.target.value);
@@ -215,7 +228,7 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
215
228
  // heading, not one each.
216
229
  const showGroupLabel = opt.group !== undefined &&
217
230
  opt.group !== filtered[i - 1]?.group;
218
- return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [showGroupLabel && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-select-label", children: opt.group })), (0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: opt.color } })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-label", children: opt.label }), isSel && ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-select-option-check", size: 15, strokeWidth: 2 }))] })] }, opt.value));
231
+ return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [showGroupLabel && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-select-label", children: opt.group })), (0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: opt.color } })), opt.description !== undefined ? ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-select-option-body", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-label", children: opt.label }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-description", children: opt.description })] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-label", children: opt.label })), isSel && ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-select-option-check", size: 15, strokeWidth: 2 }))] })] }, opt.value));
219
232
  }))] }) }))] }));
220
233
  }
221
234
  Select.displayName = "Select";